@ilya-lesikov/pi-pi 0.8.0 → 0.10.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 +71 -124
- package/3p/pi-ask-user/single-select-layout.ts +3 -14
- package/3p/pi-subagents/package.json +13 -7
- package/3p/pi-subagents/src/agent-manager.ts +243 -79
- package/3p/pi-subagents/src/agent-runner.ts +501 -365
- package/3p/pi-subagents/src/agent-types.ts +46 -57
- package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
- package/3p/pi-subagents/src/custom-agents.ts +30 -6
- package/3p/pi-subagents/src/default-agents.ts +25 -43
- package/3p/pi-subagents/src/enabled-models.ts +180 -0
- package/3p/pi-subagents/src/index.ts +599 -839
- package/3p/pi-subagents/src/model-resolver.ts +26 -7
- package/3p/pi-subagents/src/output-file.ts +21 -2
- package/3p/pi-subagents/src/prompts.ts +17 -3
- package/3p/pi-subagents/src/schedule-store.ts +153 -0
- package/3p/pi-subagents/src/schedule.ts +365 -0
- package/3p/pi-subagents/src/settings.ts +261 -0
- package/3p/pi-subagents/src/skill-loader.ts +77 -54
- package/3p/pi-subagents/src/status-note.ts +25 -0
- package/3p/pi-subagents/src/types.ts +97 -6
- package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
- package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
- package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
- package/3p/pi-subagents/src/usage.ts +60 -0
- package/3p/pi-subagents/src/worktree.ts +49 -20
- package/extensions/orchestrator/agents/advisor.ts +13 -8
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/code-reviewer.ts +29 -8
- package/extensions/orchestrator/agents/constraints.test.ts +65 -1
- package/extensions/orchestrator/agents/constraints.ts +73 -2
- package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
- package/extensions/orchestrator/agents/explore.ts +12 -6
- package/extensions/orchestrator/agents/librarian.ts +13 -5
- package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/planner.ts +8 -3
- package/extensions/orchestrator/agents/pools.test.ts +56 -0
- package/extensions/orchestrator/agents/prompts.test.ts +52 -10
- package/extensions/orchestrator/agents/registry.test.ts +245 -0
- package/extensions/orchestrator/agents/registry.ts +145 -10
- package/extensions/orchestrator/agents/reviewer.ts +14 -8
- package/extensions/orchestrator/agents/task.ts +12 -6
- package/extensions/orchestrator/agents/tool-routing.ts +213 -69
- package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
- package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
- package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
- package/extensions/orchestrator/ast-search.test.ts +124 -0
- package/extensions/orchestrator/billing-spoof.test.ts +67 -0
- package/extensions/orchestrator/billing-spoof.ts +95 -0
- package/extensions/orchestrator/cbm.more.test.ts +358 -0
- package/extensions/orchestrator/command-handlers.test.ts +47 -0
- package/extensions/orchestrator/command-handlers.ts +49 -27
- package/extensions/orchestrator/commands.test.ts +15 -2
- package/extensions/orchestrator/commands.ts +1 -1
- package/extensions/orchestrator/config.test.ts +129 -2
- package/extensions/orchestrator/config.ts +115 -19
- package/extensions/orchestrator/context.test.ts +46 -0
- package/extensions/orchestrator/context.ts +34 -5
- package/extensions/orchestrator/custom-footer.test.ts +105 -0
- package/extensions/orchestrator/custom-footer.ts +22 -20
- package/extensions/orchestrator/doctor.more.test.ts +315 -0
- package/extensions/orchestrator/doctor.ts +6 -2
- package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
- package/extensions/orchestrator/event-handlers.test.ts +411 -10
- package/extensions/orchestrator/event-handlers.ts +738 -349
- package/extensions/orchestrator/exa.more.test.ts +118 -0
- package/extensions/orchestrator/flant-infra.more.test.ts +326 -0
- package/extensions/orchestrator/flant-infra.test.ts +127 -0
- package/extensions/orchestrator/flant-infra.ts +113 -39
- package/extensions/orchestrator/index.test.ts +76 -0
- package/extensions/orchestrator/index.ts +2 -0
- package/extensions/orchestrator/integration.test.ts +488 -74
- package/extensions/orchestrator/model-registry.test.ts +2 -1
- package/extensions/orchestrator/model-registry.ts +12 -2
- package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/orchestrator/orchestrator.test.ts +67 -0
- package/extensions/orchestrator/orchestrator.ts +165 -85
- package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
- package/extensions/orchestrator/phases/brainstorm.ts +53 -12
- package/extensions/orchestrator/phases/implementation.ts +2 -0
- package/extensions/orchestrator/phases/machine.test.ts +53 -1
- package/extensions/orchestrator/phases/machine.ts +15 -2
- package/extensions/orchestrator/phases/planning.test.ts +47 -1
- package/extensions/orchestrator/phases/planning.ts +18 -3
- package/extensions/orchestrator/phases/review-task.test.ts +20 -0
- package/extensions/orchestrator/phases/review-task.ts +47 -14
- package/extensions/orchestrator/phases/review.test.ts +36 -0
- package/extensions/orchestrator/phases/review.ts +62 -10
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
- package/extensions/orchestrator/phases/verdict.ts +6 -5
- package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
- package/extensions/orchestrator/plannotator.test.ts +38 -1
- package/extensions/orchestrator/plannotator.ts +50 -3
- package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
- package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
- package/extensions/orchestrator/pp-menu.test.ts +314 -1
- package/extensions/orchestrator/pp-menu.ts +937 -450
- package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
- package/extensions/orchestrator/pp-state-tools.ts +65 -0
- package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
- package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
- package/extensions/orchestrator/review-files.test.ts +26 -0
- package/extensions/orchestrator/review-files.ts +3 -0
- package/extensions/orchestrator/state.test.ts +82 -1
- package/extensions/orchestrator/state.ts +110 -23
- package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
- package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
- package/extensions/orchestrator/transition-controller.test.ts +100 -0
- package/extensions/orchestrator/transition-controller.ts +61 -3
- package/extensions/orchestrator/validate-artifacts.ts +1 -1
- package/package.json +6 -2
- package/scripts/test-3p.sh +52 -0
- package/AGENTS.md +0 -28
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync
|
|
1
|
+
import { existsSync, readdirSync } from "fs";
|
|
2
2
|
import { join, relative, basename } from "path";
|
|
3
3
|
import { isDeepStrictEqual } from "util";
|
|
4
4
|
import { askUser, isCancel, type CancelReason } from "../../3p/pi-ask-user/index.js";
|
|
@@ -14,20 +14,23 @@ import {
|
|
|
14
14
|
readRawConfig,
|
|
15
15
|
removeConfigValue,
|
|
16
16
|
resolvePreset,
|
|
17
|
+
reviewPresetGroupForPhase,
|
|
17
18
|
writeConfigValue,
|
|
19
|
+
MAX_CONCURRENT_SUBAGENTS_CEILING,
|
|
18
20
|
type PiPiConfig,
|
|
19
21
|
type PresetGroup,
|
|
22
|
+
type PoolKey,
|
|
23
|
+
type PoolEntry,
|
|
20
24
|
type OrchestratorRole,
|
|
21
25
|
type VariantConfig,
|
|
22
26
|
} from "./config.js";
|
|
23
27
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
loadPlanReviewOutputs,
|
|
28
|
+
loadPhaseReviewOutputs,
|
|
29
|
+
hasFinalPassAnchors,
|
|
27
30
|
} from "./context.js";
|
|
28
|
-
import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle } from "./event-handlers.js";
|
|
31
|
+
import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle, isReviewCycleLive, registerFeatureToolsAndAgents } from "./event-handlers.js";
|
|
29
32
|
import { Orchestrator } from "./orchestrator.js";
|
|
30
|
-
import { cancelPendingPlannotatorWait, openPlannotator, waitForPlannotatorResult } from "./plannotator.js";
|
|
33
|
+
import { cancelPendingPlannotatorWait, openPlannotator, openAnnotateReview, waitForPlannotatorResult } from "./plannotator.js";
|
|
31
34
|
import { advanceBanner } from "./messages.js";
|
|
32
35
|
import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
|
|
33
36
|
import { spawnCodeReviewers } from "./phases/review.js";
|
|
@@ -46,13 +49,17 @@ import {
|
|
|
46
49
|
taskAge,
|
|
47
50
|
taskName,
|
|
48
51
|
taskNameFromState,
|
|
52
|
+
taskFullName,
|
|
49
53
|
taskShortId,
|
|
50
54
|
type AutonomousConfig,
|
|
51
55
|
type TaskMode,
|
|
52
56
|
type TaskInfo,
|
|
53
57
|
type TaskType,
|
|
58
|
+
type TaskState,
|
|
59
|
+
type Phase,
|
|
54
60
|
} from "./state.js";
|
|
55
61
|
import {
|
|
62
|
+
SUB_MODEL_PREFIX,
|
|
56
63
|
clearFlantGeneratedConfig,
|
|
57
64
|
getFlantGeneratedConfig,
|
|
58
65
|
loadFlantSettings,
|
|
@@ -85,6 +92,7 @@ type TimeoutKey =
|
|
|
85
92
|
| "performance.commands.afterEdit"
|
|
86
93
|
| "performance.commands.afterImplement"
|
|
87
94
|
| "performance.internals.subagentStale"
|
|
95
|
+
| "performance.internals.mainTurnStale"
|
|
88
96
|
| "performance.internals.taskLockStale"
|
|
89
97
|
| "performance.internals.taskLockRefresh";
|
|
90
98
|
|
|
@@ -105,16 +113,22 @@ async function selectOptionCancelable(
|
|
|
105
113
|
question: string,
|
|
106
114
|
options: OptionInput[],
|
|
107
115
|
): Promise<{ choice?: string; cancelReason?: CancelReason }> {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const orchestrator = Orchestrator.current;
|
|
117
|
+
if (orchestrator) orchestrator.interactivePromptOpen = true;
|
|
118
|
+
try {
|
|
119
|
+
const result = await askUser(ctx, {
|
|
120
|
+
question,
|
|
121
|
+
options,
|
|
122
|
+
allowFreeform: false,
|
|
123
|
+
allowComment: false,
|
|
124
|
+
allowMultiple: false,
|
|
125
|
+
});
|
|
126
|
+
if (result && isCancel(result)) return { cancelReason: result.reason };
|
|
127
|
+
if (!result || result.kind !== "selection") return {};
|
|
128
|
+
return { choice: result.selections[0] };
|
|
129
|
+
} finally {
|
|
130
|
+
if (orchestrator) orchestrator.interactivePromptOpen = false;
|
|
131
|
+
}
|
|
118
132
|
}
|
|
119
133
|
|
|
120
134
|
function opt(title: string, description: string): OptionInput {
|
|
@@ -151,51 +165,7 @@ function formatRepoList(repos: RepoInfo[]): string {
|
|
|
151
165
|
.join("\n");
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
function appendSection(content: string, heading: string, body: string): string {
|
|
155
|
-
const normalized = content.trimEnd();
|
|
156
|
-
if (normalized.includes(`${heading}\n`)) return normalized + "\n";
|
|
157
|
-
return `${normalized}\n\n${heading}\n${body}\n`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function appendToSection(content: string, heading: string, body: string): string {
|
|
161
|
-
const normalized = content.trimEnd();
|
|
162
|
-
if (normalized.includes(body)) return normalized + "\n";
|
|
163
|
-
|
|
164
|
-
const lines = normalized.split("\n");
|
|
165
|
-
const sectionIndex = lines.findIndex((line) => line.trim() === heading);
|
|
166
|
-
if (sectionIndex === -1) {
|
|
167
|
-
return appendSection(normalized, heading, body);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
let insertIndex = lines.length;
|
|
171
|
-
for (let i = sectionIndex + 1; i < lines.length; i += 1) {
|
|
172
|
-
if (lines[i]?.startsWith("## ")) {
|
|
173
|
-
insertIndex = i;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const insertLines = body.split("\n");
|
|
179
|
-
if (insertIndex > sectionIndex + 1 && lines[insertIndex - 1]?.trim() !== "") {
|
|
180
|
-
insertLines.unshift("");
|
|
181
|
-
}
|
|
182
|
-
lines.splice(insertIndex, 0, ...insertLines);
|
|
183
|
-
return lines.join("\n") + "\n";
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function appendRepoContext(content: string, repos: RepoInfo[]): string {
|
|
187
|
-
if (repos.length === 0) return content;
|
|
188
|
-
const lines = repos.map((repo) => `- ${formatRepoLabel(repo)}${repo.baseBranch ? ` (base: ${repo.baseBranch})` : ""}`);
|
|
189
|
-
return appendToSection(content, "## Constraints", `Registered repositories:\n${lines.join("\n")}`);
|
|
190
|
-
}
|
|
191
168
|
|
|
192
|
-
function appendResearchOpenQuestions(content: string, text: string): string {
|
|
193
|
-
const normalized = content.trimEnd();
|
|
194
|
-
if (normalized.includes("## Open Questions\n")) {
|
|
195
|
-
return `${normalized}\n${text}\n`;
|
|
196
|
-
}
|
|
197
|
-
return `${normalized}\n\n## Open Questions\n${text}\n`;
|
|
198
|
-
}
|
|
199
169
|
|
|
200
170
|
async function pickCommitForRepo(orchestrator: Orchestrator, ctx: any, repo: RepoInfo): Promise<string | null> {
|
|
201
171
|
let commits: Array<{ hash: string; message: string; age: string }> = [];
|
|
@@ -276,6 +246,84 @@ function setStep(orchestrator: Orchestrator, step: string): void {
|
|
|
276
246
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
277
247
|
}
|
|
278
248
|
|
|
249
|
+
// Publish the latest synthesized review findings to a target. The agent (not the
|
|
250
|
+
// extension) performs the writes: file comments = insert `AI_COMMENT:` markers at
|
|
251
|
+
// each finding's line; PR comments = run `gh` to post line-anchored comments from
|
|
252
|
+
// the user's own account. Both are idempotent — the agent checks for markers/
|
|
253
|
+
// comments it already published and skips duplicates, so re-publishing is safe.
|
|
254
|
+
const PRIVACY_INSTRUCTION =
|
|
255
|
+
"PRIVACY: comment bodies MUST be self-contained observations about the code itself. Do NOT reference " +
|
|
256
|
+
"private or internal details, \"the ticket\", issue trackers, or internal design docs. Say what is wrong " +
|
|
257
|
+
"in the code, not that it \"violates the design goal\" of some private document.";
|
|
258
|
+
|
|
259
|
+
export function publishFileCommentsBanner(taskDir: string): string {
|
|
260
|
+
const reviewsDir = join(taskDir, "code-reviews");
|
|
261
|
+
return advanceBanner(
|
|
262
|
+
"[PI-PI] Publish the synthesized review findings as FILE COMMENTS now.\n\n" +
|
|
263
|
+
`Use the \`ANCHORS:\` block in the latest \`${reviewsDir}/*_final_pass-*.md\` as the file:line source — do NOT invent locations.\n\n` +
|
|
264
|
+
"For each accepted finding, insert an `AI_COMMENT:` marker (inside each file's native comment syntax, e.g. `// AI_COMMENT: ...`, `# AI_COMMENT: ...`, `<!-- AI_COMMENT: ... -->`) on or immediately above the cited line, briefly stating the finding. " +
|
|
265
|
+
"This is the ONLY source edit permitted — no fixes, no other changes.\n\n" +
|
|
266
|
+
PRIVACY_INSTRUCTION + "\n\n" +
|
|
267
|
+
"IDEMPOTENT: before inserting, check whether an equivalent `AI_COMMENT:` marker for that finding is already present at that location (e.g. from an earlier publish). If so, skip it — do NOT add a duplicate.\n\n" +
|
|
268
|
+
"When done, report how many markers you inserted and how many you skipped as already-present, then end your turn.",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function publishPrCommentsBanner(taskDir: string): string {
|
|
273
|
+
const reviewsDir = join(taskDir, "code-reviews");
|
|
274
|
+
return advanceBanner(
|
|
275
|
+
"[PI-PI] Publish the synthesized review findings as GITHUB PR COMMENTS now, from the user's own `gh`-authenticated account.\n\n" +
|
|
276
|
+
`Use the \`ANCHORS:\` block in the latest \`${reviewsDir}/*_final_pass-*.md\` as the file:line source — do NOT invent locations.\n\n` +
|
|
277
|
+
"For each registered repo, resolve the branch's PR with `gh pr view --json number,headRefName,headRefOid,url` (skip the repo if there is no PR or `gh auth status` fails). Derive owner/repo from the PR `url` (the base repo).\n\n" +
|
|
278
|
+
"PRE-VALIDATE each anchor against the PR diff BEFORE building the review: fetch the diff (e.g. `gh pr diff <number>`) and keep, in a `comments` array, ONLY findings whose path+line are part of that diff. A single GitHub review is all-or-nothing — one invalid line comment rejects the WHOLE review — so an unvalidated anchor must NEVER go into `comments`.\n\n" +
|
|
279
|
+
"Post ONE bundled review per repo via `gh api --method POST repos/<owner>/<repo>/pulls/<number>/reviews` with `commit_id=<headRefOid>`, `event=COMMENT` (neutral — never APPROVE or REQUEST_CHANGES), a summary `body`, and a `comments` array of `{path, line, side: RIGHT, body}` for the validated findings.\n\n" +
|
|
280
|
+
"NON-DIFF findings (anchors not in the diff) are NEVER dropped: list them in the review `body` under a `Findings not anchorable to the diff:` heading, one per line ending with the exact ` (generated by pi-pi)` footer, using `file:—` (never an invented line). Do NOT rely on GitHub rejecting them.\n\n" +
|
|
281
|
+
"The LAST line of every finding's comment body MUST be exactly:\n(generated by pi-pi)\n\n" +
|
|
282
|
+
PRIVACY_INSTRUCTION + "\n\n" +
|
|
283
|
+
"IDEMPOTENT (line comments): first list existing PR comments (`gh api repos/<owner>/<repo>/pulls/<number>/comments`, which also returns comments from earlier per-comment publishes) and do NOT re-post a finding whose path, line, `(generated by pi-pi)` footer, AND body text all match one already present. Two distinct findings on the same line are NOT duplicates. Skip only true duplicates.\n\n" +
|
|
284
|
+
"IDEMPOTENT (body findings): also list existing reviews (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`) and read their `body`. A GitHub review body cannot be deduped per-line, so before creating a new review, drop any non-diff finding whose `<file:—> — <text> (generated by pi-pi)` body line already appears in a prior pi-pi review body. If, after that, there are NO new line comments AND NO new body findings for a repo, do NOT create an empty duplicate review — report it as fully-published instead.\n\n" +
|
|
285
|
+
"When done, report per repo how many comments you posted, how many you skipped as duplicates, and how many were unanchorable (in the body), then end your turn.",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Guard for the Publish menu: both banners consume the `ANCHORS:` block from the
|
|
290
|
+
// latest `code-reviews/*_final_pass-*.md`. If no such file exists, or the newest one
|
|
291
|
+
// carries no `ANCHORS:` block, publishing would spawn an agent that immediately fails.
|
|
292
|
+
// Returns a user-facing message to show instead, or undefined when publishing can proceed.
|
|
293
|
+
export const MISSING_FINAL_PASS_ANCHORS =
|
|
294
|
+
"No ANCHORS-bearing final review file exists yet. Run or finish a review pass first " +
|
|
295
|
+
"(/pp → Review) so the findings are synthesized into `code-reviews/*_final_pass-*.md`.";
|
|
296
|
+
|
|
297
|
+
export function publishGuard(taskDir: string): string | undefined {
|
|
298
|
+
return hasFinalPassAnchors(taskDir) ? undefined : MISSING_FINAL_PASS_ANCHORS;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const AI_REVIEW_MARKER_SYNTAX =
|
|
302
|
+
"(inside each file's native comment syntax, e.g. `// AI_REVIEW: ...`, `# AI_REVIEW: ...`, `<!-- AI_REVIEW: ... -->`)";
|
|
303
|
+
|
|
304
|
+
const AI_REVIEW_MARKER_LOOP =
|
|
305
|
+
"For each marker: address the request, then remove that marker in the SAME edit. After a pass, re-scan the target files and " +
|
|
306
|
+
"repeat until no `AI_REVIEW:` markers remain. Then verify your work and report what you changed per marker. When complete, " +
|
|
307
|
+
"call pp_phase_complete.";
|
|
308
|
+
|
|
309
|
+
// Read-only phases (brainstorm/debug/review) and plan produce markdown state files rather than
|
|
310
|
+
// source changes, so their AI_REVIEW scan targets those artifacts. Missing targets are skipped,
|
|
311
|
+
// not errors. Kept as literal-token guidance (no regexp/parser), mirroring the implement banner.
|
|
312
|
+
function readOnlyReviewBanner(phase: string, taskDir: string): string {
|
|
313
|
+
const targets =
|
|
314
|
+
phase === "plan"
|
|
315
|
+
? "the synthesized plan(s) at `" + taskDir + "/plans/*_synthesized.md` ONLY (ignore raw planner outputs, " +
|
|
316
|
+
"`review_*` files, and the `plan-reviews/`, `brainstorm-reviews/`, `code-reviews/` directories)"
|
|
317
|
+
: "`" + taskDir + "/USER_REQUEST.md`, `" + taskDir + "/RESEARCH.md`, and `" + taskDir + "/artifacts/*.md`";
|
|
318
|
+
return advanceBanner(
|
|
319
|
+
`[PI-PI] The user reviewed this phase's state files in their editor and left inline \`AI_REVIEW:\` markers ` +
|
|
320
|
+
`${AI_REVIEW_MARKER_SYNTAX}.\n\n` +
|
|
321
|
+
`Search WITHIN ${targets} for \`AI_REVIEW:\`. Only scan those files — skip any target that does not exist ` +
|
|
322
|
+
`(a missing \`artifacts/*.md\` or a not-yet-produced state file is fine, not an error).\n\n` +
|
|
323
|
+
AI_REVIEW_MARKER_LOOP,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
279
327
|
function showStatus(orchestrator: Orchestrator, ctx: any): void {
|
|
280
328
|
if (!orchestrator.active) {
|
|
281
329
|
ctx.ui.notify("No active task.", "info");
|
|
@@ -316,6 +364,9 @@ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
316
364
|
const type = orchestrator.active.type;
|
|
317
365
|
|
|
318
366
|
orchestrator.active.state.reviewCycle = null;
|
|
367
|
+
// Drop any in-progress per-repo Plannotator cursor so a resumed task does not
|
|
368
|
+
// auto-reopen Plannotator; the user re-enters Review explicitly.
|
|
369
|
+
orchestrator.active.state.plannotatorCursor = undefined;
|
|
319
370
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
320
371
|
unregisterAgentDefinitions(orchestrator.pi);
|
|
321
372
|
await orchestrator.cleanupActive();
|
|
@@ -340,6 +391,13 @@ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
340
391
|
async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string> {
|
|
341
392
|
if (!orchestrator.active) return "No active task.";
|
|
342
393
|
|
|
394
|
+
// The review phase must produce the ANCHORS-bearing final_pass file Publish
|
|
395
|
+
// consumes; block the direct Complete path too (it bypasses validateExitCriteria).
|
|
396
|
+
if (orchestrator.active.state.phase === "review" && !hasFinalPassAnchors(orchestrator.active.dir)) {
|
|
397
|
+
ctx.ui.notify(MISSING_FINAL_PASS_ANCHORS, "warning");
|
|
398
|
+
return MISSING_FINAL_PASS_ANCHORS;
|
|
399
|
+
}
|
|
400
|
+
|
|
343
401
|
cancelPendingPlannotatorWait(orchestrator);
|
|
344
402
|
orchestrator.abortAllSubagents();
|
|
345
403
|
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
@@ -351,7 +409,11 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
351
409
|
|
|
352
410
|
orchestrator.lastCtx = ctx;
|
|
353
411
|
|
|
412
|
+
// Record the phase we finished FROM so a later Resume can reopen the task at
|
|
413
|
+
// its real last working phase (done carries no phase history of its own).
|
|
414
|
+
orchestrator.active.state.completedFrom = orchestrator.active.state.phase;
|
|
354
415
|
orchestrator.active.state.phase = "done";
|
|
416
|
+
orchestrator.active.state.step = null;
|
|
355
417
|
orchestrator.active.state.reviewCycle = null;
|
|
356
418
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
357
419
|
unregisterAgentDefinitions(orchestrator.pi);
|
|
@@ -366,7 +428,8 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
366
428
|
// is over, nothing awaits this compaction).
|
|
367
429
|
void orchestrator.transitionController.requestTransition({
|
|
368
430
|
kind: "done",
|
|
369
|
-
|
|
431
|
+
discard: true,
|
|
432
|
+
summary: `Task "${name}" (${type}) is finished — DISCARD its entire conversation. Do NOT carry forward, reference, or act on any of this task's messages, phase, plan, or aborted turns; the next task starts from a clean slate.`,
|
|
370
433
|
});
|
|
371
434
|
|
|
372
435
|
const urExists = existsSync(join(dir, "USER_REQUEST.md"));
|
|
@@ -385,16 +448,8 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
385
448
|
return `Task "${name}" completed.`;
|
|
386
449
|
}
|
|
387
450
|
|
|
388
|
-
function loadPhaseReviewOutputs(taskDir: string, phase: string, pass: number): { name: string; content: string }[] {
|
|
389
|
-
if (phase === "brainstorm") return loadBrainstormReviewOutputs(taskDir, pass);
|
|
390
|
-
if (phase === "plan") return loadPlanReviewOutputs(taskDir, pass);
|
|
391
|
-
return loadCodeReviewOutputs(taskDir, pass);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
451
|
function getDefaultReviewPresetName(config: PiPiConfig, phase: string): string {
|
|
395
|
-
|
|
396
|
-
if (phase === "plan") return config.agents.subagents.presetGroups.planReviewers.default;
|
|
397
|
-
return config.agents.subagents.presetGroups.codeReviewers.default;
|
|
452
|
+
return config.agents.subagents.presetGroups[reviewPresetGroupForPhase(phase)].default;
|
|
398
453
|
}
|
|
399
454
|
|
|
400
455
|
function isPresetEnabled(preset: { enabled?: boolean } | undefined): boolean {
|
|
@@ -402,9 +457,7 @@ function isPresetEnabled(preset: { enabled?: boolean } | undefined): boolean {
|
|
|
402
457
|
}
|
|
403
458
|
|
|
404
459
|
function getReviewPresetGroup(phase: string): PresetGroup {
|
|
405
|
-
|
|
406
|
-
if (phase === "plan") return "planReviewers";
|
|
407
|
-
return "codeReviewers";
|
|
460
|
+
return reviewPresetGroupForPhase(phase);
|
|
408
461
|
}
|
|
409
462
|
|
|
410
463
|
export async function pickPreset(
|
|
@@ -440,6 +493,18 @@ export async function pickPreset(
|
|
|
440
493
|
return byTitle.get(choice) ?? null;
|
|
441
494
|
}
|
|
442
495
|
|
|
496
|
+
// The phase to restore when reopening a done task (#2). Prefer the recorded
|
|
497
|
+
// completedFrom; for legacy done tasks that lack it, fall back to the phase whose
|
|
498
|
+
// transition target is "done" (approximate — assumes the task ran to its terminal
|
|
499
|
+
// phase, which is wrong only for tasks completed early).
|
|
500
|
+
function reopenPhaseForDoneTask(type: TaskType, state: TaskState): Phase {
|
|
501
|
+
if (state.completedFrom) return state.completedFrom;
|
|
502
|
+
for (const phase of ["implement", "plan", "review", "debug", "brainstorm", "quick"] as Phase[]) {
|
|
503
|
+
if (nextPhase(type, phase) === "done") return phase;
|
|
504
|
+
}
|
|
505
|
+
return "implement" as Phase;
|
|
506
|
+
}
|
|
507
|
+
|
|
443
508
|
export async function resumeTask(
|
|
444
509
|
orchestrator: Orchestrator,
|
|
445
510
|
ctx: any,
|
|
@@ -470,6 +535,19 @@ export async function resumeTask(
|
|
|
470
535
|
orchestrator.resetTaskScopedState();
|
|
471
536
|
orchestrator.activeTaskToken++;
|
|
472
537
|
|
|
538
|
+
// Reopen a done task (#2): now that the lock is held, reload under the lock and
|
|
539
|
+
// restore the phase the task was completed FROM so it re-enters at a sensible
|
|
540
|
+
// working phase (done leaves step=null). Lock-first, then mutate, then save.
|
|
541
|
+
if (task.state.phase === "done") {
|
|
542
|
+
task.state = loadTask(task.dir);
|
|
543
|
+
if (task.state.phase === "done") {
|
|
544
|
+
task.state.phase = reopenPhaseForDoneTask(task.type, task.state);
|
|
545
|
+
task.state.step = "llm_work";
|
|
546
|
+
task.state.completedFrom = undefined;
|
|
547
|
+
saveTask(task.dir, task.state);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
473
551
|
const normalizedRoot = normalizeRepoPath(orchestrator.cwd);
|
|
474
552
|
if (!task.state.repos || task.state.repos.length === 0) {
|
|
475
553
|
task.state.repos = [{ path: normalizedRoot, isRoot: true }];
|
|
@@ -612,11 +690,7 @@ export async function resumeTask(
|
|
|
612
690
|
"warning",
|
|
613
691
|
);
|
|
614
692
|
}
|
|
615
|
-
const reviewers =
|
|
616
|
-
? resolvePreset(orchestrator.config, "brainstormReviewers", presetName)
|
|
617
|
-
: phase === "plan"
|
|
618
|
-
? resolvePreset(orchestrator.config, "planReviewers", presetName)
|
|
619
|
-
: resolvePreset(orchestrator.config, "codeReviewers", presetName);
|
|
693
|
+
const reviewers = resolvePreset(orchestrator.config, group, presetName);
|
|
620
694
|
const reviewerCount = Object.values(reviewers).filter((v) => isEnabled(v)).length;
|
|
621
695
|
|
|
622
696
|
if (cycle.kind === "auto" && (cycle.step === "spawn_reviewers" || cycle.step === "await_reviewers")) {
|
|
@@ -665,7 +739,7 @@ export async function resumeTask(
|
|
|
665
739
|
orchestrator.active.state.activeReviewPreset = presetName;
|
|
666
740
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
667
741
|
orchestrator.pendingSubagentSpawns = missingVariants.length;
|
|
668
|
-
const spawnFn = phase === "
|
|
742
|
+
const spawnFn = reviewPresetGroupForPhase(phase) === "brainstormReviewers"
|
|
669
743
|
? () => spawnBrainstormReviewers(
|
|
670
744
|
pi,
|
|
671
745
|
orchestrator.cwd,
|
|
@@ -677,7 +751,7 @@ export async function resumeTask(
|
|
|
677
751
|
missingReviewerConfig,
|
|
678
752
|
orchestrator.active?.state.repos ?? [],
|
|
679
753
|
)
|
|
680
|
-
: phase === "
|
|
754
|
+
: reviewPresetGroupForPhase(phase) === "planReviewers"
|
|
681
755
|
? () => spawnPlanReviewers(
|
|
682
756
|
pi,
|
|
683
757
|
orchestrator.cwd,
|
|
@@ -788,18 +862,25 @@ function listCompletedFromTasks(cwd: string): TaskInfo[] {
|
|
|
788
862
|
}
|
|
789
863
|
|
|
790
864
|
async function showSubagentsMenu(ctx: any): Promise<void> {
|
|
791
|
-
const api = (globalThis as any)[Symbol.for("pi-subagents:menu")] as {
|
|
792
|
-
|
|
865
|
+
const api = (globalThis as any)[Symbol.for("pi-subagents:menu")] as {
|
|
866
|
+
showFleet?: (menuCtx: any) => Promise<void>;
|
|
867
|
+
} | undefined;
|
|
868
|
+
// showFleet opens the navigable running-agents list (the FleetView replacement).
|
|
869
|
+
if (!api?.showFleet) {
|
|
793
870
|
ctx.ui.notify("Subagents menu API is not available.", "warning");
|
|
794
871
|
return;
|
|
795
872
|
}
|
|
796
|
-
await api.
|
|
873
|
+
await api.showFleet(ctx);
|
|
797
874
|
}
|
|
798
875
|
|
|
799
|
-
function countFlantProviders(settings: FlantSettings): { anthropic: number; openai: number } {
|
|
876
|
+
function countFlantProviders(settings: FlantSettings): { anthropic: number; openai: number; sub: number } {
|
|
800
877
|
const models = settings.cachedFlantModels ?? [];
|
|
801
878
|
const anthropic = models.filter((m) => m.startsWith("claude-")).length;
|
|
802
|
-
|
|
879
|
+
const subConfirmed = models.filter((m) => m.startsWith(SUB_MODEL_PREFIX)).length;
|
|
880
|
+
// Model lists cached before the gateway exposed sub/ ids have none — the
|
|
881
|
+
// sub provider then registers all claude models (see registerFlantProviders).
|
|
882
|
+
const sub = subConfirmed > 0 ? subConfirmed : anthropic;
|
|
883
|
+
return { anthropic, openai: Math.max(0, models.length - anthropic - subConfirmed), sub };
|
|
803
884
|
}
|
|
804
885
|
|
|
805
886
|
function collectRoleAssignments(config: Partial<PiPiConfig> | null): string[] {
|
|
@@ -839,9 +920,13 @@ function collectRoleAssignments(config: Partial<PiPiConfig> | null): string[] {
|
|
|
839
920
|
add("agents.subagents.simple.explore", config.agents?.subagents?.simple?.explore?.model);
|
|
840
921
|
add("agents.subagents.simple.librarian", config.agents?.subagents?.simple?.librarian?.model);
|
|
841
922
|
add("agents.subagents.simple.task", config.agents?.subagents?.simple?.task?.model);
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
923
|
+
for (const poolKey of ["advisors", "reviewers", "deepDebuggers"] as const) {
|
|
924
|
+
const pool = config.agents?.subagents?.pools?.[poolKey];
|
|
925
|
+
if (!Array.isArray(pool)) continue;
|
|
926
|
+
pool.forEach((entry: any, i: number) => {
|
|
927
|
+
if (entry?.enabled !== false) add(`agents.subagents.pools.${poolKey}[${i}]`, entry?.model);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
845
930
|
return out;
|
|
846
931
|
}
|
|
847
932
|
|
|
@@ -859,7 +944,7 @@ function flantStatusText(settings: FlantSettings): string {
|
|
|
859
944
|
const hasGatewayKey = !!readGatewayApiKey();
|
|
860
945
|
const subActive = hasOAuth && hasGatewayKey;
|
|
861
946
|
lines.push(
|
|
862
|
-
`Personal subscription: on (${subActive ? `active — pp-flant-anthropic-sub, ${providers.
|
|
947
|
+
`Personal subscription: on (${subActive ? `active — pp-flant-anthropic-sub, ${providers.sub} models` : "inactive"})`,
|
|
863
948
|
);
|
|
864
949
|
lines.push(`Rate-limit switch-back check: every ${settings.switchBackIntervalMinutes} min`);
|
|
865
950
|
if (!subActive) {
|
|
@@ -917,7 +1002,7 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
917
1002
|
}
|
|
918
1003
|
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
919
1004
|
|
|
920
|
-
const choice = await selectOption(ctx, "Flant
|
|
1005
|
+
const choice = await selectOption(ctx, "Flant", options);
|
|
921
1006
|
if (!choice || choice === "Back") return BACK;
|
|
922
1007
|
|
|
923
1008
|
if (choice === enableLabel) {
|
|
@@ -926,7 +1011,7 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
926
1011
|
saveFlantSettings(next);
|
|
927
1012
|
unregisterFlantProviders(orchestrator.pi);
|
|
928
1013
|
clearFlantGeneratedConfig();
|
|
929
|
-
ctx.ui.notify("Flant
|
|
1014
|
+
ctx.ui.notify("Flant disabled.", "info");
|
|
930
1015
|
} else {
|
|
931
1016
|
if (!process.env.FLANT_API_KEY) {
|
|
932
1017
|
ctx.ui.notify("Set FLANT_API_KEY environment variable first.", "warning");
|
|
@@ -1207,7 +1292,6 @@ export function showUsage(ctx: any): void {
|
|
|
1207
1292
|
async function showInfoMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
1208
1293
|
while (true) {
|
|
1209
1294
|
const options: OptionInput[] = [];
|
|
1210
|
-
options.push({ title: "Subagents", description: "Manage running agents" });
|
|
1211
1295
|
options.push({ title: "Usage", description: "Show session token usage and cost breakdown" });
|
|
1212
1296
|
options.push({ title: "Doctor", description: "Run diagnostic checks" });
|
|
1213
1297
|
if (orchestrator.active) {
|
|
@@ -1218,10 +1302,6 @@ async function showInfoMenu(orchestrator: Orchestrator, ctx: any): Promise<typeo
|
|
|
1218
1302
|
|
|
1219
1303
|
const choice = await selectOption(ctx, "Info", options);
|
|
1220
1304
|
if (!choice || choice === "Back") return BACK;
|
|
1221
|
-
if (choice === "Subagents") {
|
|
1222
|
-
await showSubagentsMenu(ctx);
|
|
1223
|
-
continue;
|
|
1224
|
-
}
|
|
1225
1305
|
if (choice === "Usage") {
|
|
1226
1306
|
showUsage(ctx);
|
|
1227
1307
|
continue;
|
|
@@ -1270,9 +1350,12 @@ const SUBAGENT_ROLES: Array<{ role: AgentRole; label: string; description: strin
|
|
|
1270
1350
|
{ role: "explore", label: "Explore", description: "agents.subagents.simple.explore" },
|
|
1271
1351
|
{ role: "librarian", label: "Librarian", description: "agents.subagents.simple.librarian" },
|
|
1272
1352
|
{ role: "task", label: "Task", description: "agents.subagents.simple.task" },
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1353
|
+
];
|
|
1354
|
+
|
|
1355
|
+
const POOL_ITEMS: Array<{ pool: PoolKey; label: string }> = [
|
|
1356
|
+
{ pool: "advisors", label: "Advisors" },
|
|
1357
|
+
{ pool: "reviewers", label: "Reviewers" },
|
|
1358
|
+
{ pool: "deepDebuggers", label: "Deep debuggers" },
|
|
1276
1359
|
];
|
|
1277
1360
|
|
|
1278
1361
|
const PRESET_GROUP_ITEMS: Array<{ group: PresetGroup; label: string }> = [
|
|
@@ -1286,6 +1369,7 @@ const TIMEOUT_LABELS: Record<TimeoutKey, string> = {
|
|
|
1286
1369
|
"performance.commands.afterEdit": "Command after file edit",
|
|
1287
1370
|
"performance.commands.afterImplement": "Command after implementation",
|
|
1288
1371
|
"performance.internals.subagentStale": "Subagent stale",
|
|
1372
|
+
"performance.internals.mainTurnStale": "Main turn stale",
|
|
1289
1373
|
"performance.internals.taskLockStale": "Lock stale",
|
|
1290
1374
|
"performance.internals.taskLockRefresh": "Lock update",
|
|
1291
1375
|
};
|
|
@@ -1692,6 +1776,12 @@ function refreshSubagentDefinitions(orchestrator: Orchestrator, keyPath: string[
|
|
|
1692
1776
|
orchestrator.registerAgents();
|
|
1693
1777
|
}
|
|
1694
1778
|
|
|
1779
|
+
function applyConcurrencyIfChanged(orchestrator: Orchestrator, keyPath: string[]): void {
|
|
1780
|
+
if (keyPath[0] === "agents" && keyPath[1] === "maxConcurrentSubagents") {
|
|
1781
|
+
orchestrator.applySubagentConcurrency();
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1695
1785
|
function applyConfigChange(orchestrator: Orchestrator, scope: Scope, keyPath: string[], value: any): void {
|
|
1696
1786
|
const result = tryApplyConfigChange(orchestrator, scope, keyPath, value);
|
|
1697
1787
|
if (!result.ok) {
|
|
@@ -1710,6 +1800,7 @@ function applyConfigChange(orchestrator: Orchestrator, scope: Scope, keyPath: st
|
|
|
1710
1800
|
updateRegistryFromAvailableModels(modelIds);
|
|
1711
1801
|
}
|
|
1712
1802
|
refreshSubagentDefinitions(orchestrator, keyPath);
|
|
1803
|
+
applyConcurrencyIfChanged(orchestrator, keyPath);
|
|
1713
1804
|
}
|
|
1714
1805
|
|
|
1715
1806
|
function clearConfigOverride(orchestrator: Orchestrator, scope: Scope, keyPath: string[]): void {
|
|
@@ -1719,6 +1810,7 @@ function clearConfigOverride(orchestrator: Orchestrator, scope: Scope, keyPath:
|
|
|
1719
1810
|
return;
|
|
1720
1811
|
}
|
|
1721
1812
|
refreshSubagentDefinitions(orchestrator, keyPath);
|
|
1813
|
+
applyConcurrencyIfChanged(orchestrator, keyPath);
|
|
1722
1814
|
}
|
|
1723
1815
|
|
|
1724
1816
|
function thinkingLabel(value: string): string {
|
|
@@ -1827,8 +1919,8 @@ async function showOrchestratorEditor(
|
|
|
1827
1919
|
const current = orchestrator.config.agents.orchestrators[role];
|
|
1828
1920
|
const basePath = ["agents", "orchestrators", role];
|
|
1829
1921
|
const choice = await selectOption(ctx, label, [
|
|
1830
|
-
opt(`Model: ${current.model}`, "
|
|
1831
|
-
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "
|
|
1922
|
+
opt(`Model: ${current.model}`, "Choose the model for this agent"),
|
|
1923
|
+
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "Choose how much this agent thinks before acting"),
|
|
1832
1924
|
...buildResetOptions(orchestrator, basePath),
|
|
1833
1925
|
opt("Back", "Return to the previous menu"),
|
|
1834
1926
|
]);
|
|
@@ -1874,8 +1966,8 @@ async function showSimpleSubagentEditor(
|
|
|
1874
1966
|
const current = orchestrator.config.agents.subagents.simple[role];
|
|
1875
1967
|
const basePath = ["agents", "subagents", "simple", role];
|
|
1876
1968
|
const choice = await selectOption(ctx, label, [
|
|
1877
|
-
opt(`Model: ${current.model}`, "
|
|
1878
|
-
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "
|
|
1969
|
+
opt(`Model: ${current.model}`, "Choose the model for this agent"),
|
|
1970
|
+
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "Choose how much this agent thinks before acting"),
|
|
1879
1971
|
...buildResetOptions(orchestrator, basePath),
|
|
1880
1972
|
opt("Back", "Return to the previous menu"),
|
|
1881
1973
|
]);
|
|
@@ -1976,8 +2068,8 @@ async function showPresetVariantEditor(
|
|
|
1976
2068
|
const variantPath = ["agents", "subagents", "presetGroups", group, "presets", presetName, "agents", variantName];
|
|
1977
2069
|
const options: OptionInput[] = [
|
|
1978
2070
|
opt(`Enabled: ${isEnabled(variant) ? "Yes" : "No"}`, "Toggle enabled state"),
|
|
1979
|
-
opt(`Model: ${variant.model}`, "
|
|
1980
|
-
opt(`Thinking: ${thinkingLabel(variant.thinking)}`, "
|
|
2071
|
+
opt(`Model: ${variant.model}`, "Choose the model for this agent"),
|
|
2072
|
+
opt(`Thinking: ${thinkingLabel(variant.thinking)}`, "Choose how much this agent thinks before acting"),
|
|
1981
2073
|
];
|
|
1982
2074
|
if (getOwnedScopes(orchestrator, variantPath).length > 0) {
|
|
1983
2075
|
options.push(opt("Delete", "Delete this agent override"));
|
|
@@ -2029,7 +2121,7 @@ async function showPresetAgentsMenu(
|
|
|
2029
2121
|
options.push(opt(title, `${variant.model} / ${thinkingLabel(variant.thinking)}`));
|
|
2030
2122
|
byTitle.set(title, variantName);
|
|
2031
2123
|
}
|
|
2032
|
-
options.push(opt("New agent", "Add
|
|
2124
|
+
options.push(opt("New agent", "Add an agent variant to this preset"));
|
|
2033
2125
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2034
2126
|
const choice = await selectOption(ctx, "Agents", options);
|
|
2035
2127
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2198,7 +2290,7 @@ async function showPresetSettings(
|
|
|
2198
2290
|
options.push(opt(optionTitle, enabledPresetSummary(preset.agents)));
|
|
2199
2291
|
byTitle.set(optionTitle, presetName);
|
|
2200
2292
|
}
|
|
2201
|
-
options.push(opt("New preset", "Create preset"));
|
|
2293
|
+
options.push(opt("New preset", "Create a preset in this group"));
|
|
2202
2294
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2203
2295
|
const choice = await selectOption(ctx, title, options);
|
|
2204
2296
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2212,12 +2304,118 @@ async function showPresetSettings(
|
|
|
2212
2304
|
}
|
|
2213
2305
|
}
|
|
2214
2306
|
|
|
2307
|
+
// The dynamic on-demand pools (advisors/reviewers/deepDebuggers) are arrays, not
|
|
2308
|
+
// keyed records. writeConfigValue treats arrays as opaque leaves, so every edit
|
|
2309
|
+
// rewrites the WHOLE array into the chosen scope (deepMerge replaces arrays).
|
|
2310
|
+
function poolModelLabel(pool: PoolKey): string {
|
|
2311
|
+
return pool === "deepDebuggers" ? "deep-debugger" : pool.replace(/s$/, "");
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
async function writePool(orchestrator: Orchestrator, ctx: any, pool: PoolKey, next: PoolEntry[]): Promise<boolean> {
|
|
2315
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2316
|
+
if (!scope) return false;
|
|
2317
|
+
applyConfigChange(orchestrator, scope, ["agents", "subagents", "pools", pool], next);
|
|
2318
|
+
return true;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
async function showPoolEntryEditor(
|
|
2322
|
+
orchestrator: Orchestrator,
|
|
2323
|
+
ctx: any,
|
|
2324
|
+
pool: PoolKey,
|
|
2325
|
+
index: number,
|
|
2326
|
+
): Promise<typeof BACK> {
|
|
2327
|
+
while (true) {
|
|
2328
|
+
const entries = orchestrator.config.agents.subagents.pools[pool] ?? [];
|
|
2329
|
+
const entry = entries[index];
|
|
2330
|
+
if (!entry) return BACK;
|
|
2331
|
+
const options: OptionInput[] = [
|
|
2332
|
+
opt(`Enabled: ${entry.enabled === false ? "No" : "Yes"}`, "Toggle whether this model registers as a subagent"),
|
|
2333
|
+
opt(`Model: ${entry.model}`, "Choose the model for this pool entry"),
|
|
2334
|
+
opt(`Thinking: ${thinkingLabel(entry.thinking)}`, "Choose how much this agent thinks before acting"),
|
|
2335
|
+
opt("Delete", "Remove this entry from the pool"),
|
|
2336
|
+
opt("Back", "Return to the previous menu"),
|
|
2337
|
+
];
|
|
2338
|
+
const choice = await selectOption(ctx, `${poolModelLabel(pool)} entry`, options);
|
|
2339
|
+
if (!choice || choice === "Back") return BACK;
|
|
2340
|
+
if (choice.startsWith("Model:")) {
|
|
2341
|
+
const model = await pickModel(ctx, entry.model);
|
|
2342
|
+
if (!model) continue;
|
|
2343
|
+
const next = structuredClone(entries);
|
|
2344
|
+
next[index] = { ...next[index], model };
|
|
2345
|
+
await writePool(orchestrator, ctx, pool, next);
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
if (choice.startsWith("Thinking:")) {
|
|
2349
|
+
const thinking = await pickThinking(ctx, true);
|
|
2350
|
+
if (!thinking) continue;
|
|
2351
|
+
const next = structuredClone(entries);
|
|
2352
|
+
next[index] = { ...next[index], thinking };
|
|
2353
|
+
await writePool(orchestrator, ctx, pool, next);
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
if (choice.startsWith("Enabled:")) {
|
|
2357
|
+
const next = structuredClone(entries);
|
|
2358
|
+
next[index] = { ...next[index], enabled: entry.enabled === false };
|
|
2359
|
+
await writePool(orchestrator, ctx, pool, next);
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
2363
|
+
opt("Yes, delete", "This cannot be undone"),
|
|
2364
|
+
opt("Back", "Cancel"),
|
|
2365
|
+
]);
|
|
2366
|
+
if (confirm !== "Yes, delete") continue;
|
|
2367
|
+
const next = structuredClone(entries);
|
|
2368
|
+
next.splice(index, 1);
|
|
2369
|
+
if (await writePool(orchestrator, ctx, pool, next)) return BACK;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
async function addPoolEntry(orchestrator: Orchestrator, ctx: any, pool: PoolKey): Promise<void> {
|
|
2374
|
+
const model = await pickModel(ctx);
|
|
2375
|
+
if (!model) return;
|
|
2376
|
+
const thinking = await pickThinking(ctx, true);
|
|
2377
|
+
if (!thinking) return;
|
|
2378
|
+
const next = structuredClone(orchestrator.config.agents.subagents.pools[pool] ?? []);
|
|
2379
|
+
next.push({ enabled: true, model, thinking });
|
|
2380
|
+
await writePool(orchestrator, ctx, pool, next);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
async function showPoolSettings(orchestrator: Orchestrator, ctx: any, pool: PoolKey, label: string): Promise<typeof BACK> {
|
|
2384
|
+
while (true) {
|
|
2385
|
+
const entries = orchestrator.config.agents.subagents.pools[pool] ?? [];
|
|
2386
|
+
const options: OptionInput[] = [];
|
|
2387
|
+
const byTitle = new Map<string, number>();
|
|
2388
|
+
entries.forEach((entry, i) => {
|
|
2389
|
+
const tag = entry.enabled === false ? " (disabled)" : "";
|
|
2390
|
+
const title = `${entry.model}${tag}`;
|
|
2391
|
+
options.push(opt(title, `thinking ${thinkingLabel(entry.thinking)}`));
|
|
2392
|
+
byTitle.set(title, i);
|
|
2393
|
+
});
|
|
2394
|
+
options.push(opt("New entry", `Add a model to the ${label.toLowerCase()} pool`));
|
|
2395
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
2396
|
+
const choice = await selectOption(ctx, label, options);
|
|
2397
|
+
if (!choice || choice === "Back") return BACK;
|
|
2398
|
+
if (choice === "New entry") {
|
|
2399
|
+
await addPoolEntry(orchestrator, ctx, pool);
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
const idx = byTitle.get(choice);
|
|
2403
|
+
if (idx === undefined) continue;
|
|
2404
|
+
await showPoolEntryEditor(orchestrator, ctx, pool, idx);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2215
2408
|
async function showSubagentSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2216
2409
|
while (true) {
|
|
2217
2410
|
const options: OptionInput[] = SUBAGENT_ROLES.map(({ role, label, description }) => {
|
|
2218
2411
|
const current = orchestrator.config.agents.subagents.simple[role];
|
|
2219
2412
|
return opt(label, `${current.model} / ${thinkingLabel(current.thinking)} — ${description}`);
|
|
2220
2413
|
});
|
|
2414
|
+
for (const item of POOL_ITEMS) {
|
|
2415
|
+
const pool = orchestrator.config.agents.subagents.pools[item.pool] ?? [];
|
|
2416
|
+
const enabled = pool.filter((e) => e.enabled !== false).length;
|
|
2417
|
+
options.push(opt(item.label, `${enabled}/${pool.length} enabled — on-demand model pool`));
|
|
2418
|
+
}
|
|
2221
2419
|
for (const item of PRESET_GROUP_ITEMS) {
|
|
2222
2420
|
options.push(opt(item.label, `${Object.keys(orchestrator.config.agents.subagents.presetGroups[item.group].presets ?? {}).length} presets`));
|
|
2223
2421
|
}
|
|
@@ -2229,6 +2427,11 @@ async function showSubagentSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2229
2427
|
await showSimpleSubagentEditor(orchestrator, ctx, simple.role, simple.label);
|
|
2230
2428
|
continue;
|
|
2231
2429
|
}
|
|
2430
|
+
const pool = POOL_ITEMS.find((item) => item.label === choice);
|
|
2431
|
+
if (pool) {
|
|
2432
|
+
await showPoolSettings(orchestrator, ctx, pool.pool, pool.label);
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2232
2435
|
const group = PRESET_GROUP_ITEMS.find((item) => item.label === choice);
|
|
2233
2436
|
if (!group) continue;
|
|
2234
2437
|
await showPresetSettings(orchestrator, ctx, group.group, group.label);
|
|
@@ -2272,7 +2475,7 @@ async function showAfterEditCommands(orchestrator: Orchestrator, ctx: any): Prom
|
|
|
2272
2475
|
options.push(opt(`${title}${enabledTag}`, `${globsCount} patterns — ${cmd.run}`));
|
|
2273
2476
|
byTitle.set(`${title}${enabledTag}`, id);
|
|
2274
2477
|
}
|
|
2275
|
-
options.push(opt("New command", "Add command"));
|
|
2478
|
+
options.push(opt("New command", "Add a command to run after a file edit"));
|
|
2276
2479
|
options.push(...buildResetOptions(orchestrator, ["commands", "afterEdit"]));
|
|
2277
2480
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2278
2481
|
const choice = await selectOption(ctx, `After file edit: ${entries.length} commands`, options);
|
|
@@ -2401,7 +2604,7 @@ async function showAfterImplementCommands(orchestrator: Orchestrator, ctx: any):
|
|
|
2401
2604
|
options.push(opt(title, cmd.run));
|
|
2402
2605
|
byTitle.set(title, id);
|
|
2403
2606
|
}
|
|
2404
|
-
options.push(opt("New command", "Add command"));
|
|
2607
|
+
options.push(opt("New command", "Add a command to run after implementation"));
|
|
2405
2608
|
options.push(...buildResetOptions(orchestrator, ["commands", "afterImplement"]));
|
|
2406
2609
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2407
2610
|
const choice = await selectOption(ctx, `After implementation: ${entries.length} commands`, options);
|
|
@@ -2461,11 +2664,11 @@ async function showCommandsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2461
2664
|
const choice = await selectOption(ctx, "Commands", [
|
|
2462
2665
|
opt(
|
|
2463
2666
|
`After file edit: ${afterEditCount} commands`,
|
|
2464
|
-
|
|
2667
|
+
"Commands run when a matching file is edited (e.g. format, lint)",
|
|
2465
2668
|
),
|
|
2466
2669
|
opt(
|
|
2467
2670
|
`After implementation: ${afterImplementCount} commands`,
|
|
2468
|
-
|
|
2671
|
+
"Commands run once the implement phase completes (e.g. build, test)",
|
|
2469
2672
|
),
|
|
2470
2673
|
opt("Back", "Return to the previous menu"),
|
|
2471
2674
|
]);
|
|
@@ -2496,6 +2699,11 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2496
2699
|
path: ["performance", "internals", "subagentStale"],
|
|
2497
2700
|
value: orchestrator.config.performance.internals.subagentStale,
|
|
2498
2701
|
},
|
|
2702
|
+
{
|
|
2703
|
+
key: "performance.internals.mainTurnStale",
|
|
2704
|
+
path: ["performance", "internals", "mainTurnStale"],
|
|
2705
|
+
value: orchestrator.config.performance.internals.mainTurnStale,
|
|
2706
|
+
},
|
|
2499
2707
|
{
|
|
2500
2708
|
key: "performance.internals.taskLockStale",
|
|
2501
2709
|
path: ["performance", "internals", "taskLockStale"],
|
|
@@ -2510,7 +2718,7 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2510
2718
|
const options: OptionInput[] = timeoutEntries.map((entry) => {
|
|
2511
2719
|
const value = entry.value;
|
|
2512
2720
|
const key = entry.key;
|
|
2513
|
-
return opt(`${TIMEOUT_LABELS[key]}: ${formatDuration(value)}`, "
|
|
2721
|
+
return opt(`${TIMEOUT_LABELS[key]}: ${formatDuration(value)}`, "Change this time limit");
|
|
2514
2722
|
});
|
|
2515
2723
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2516
2724
|
const choice = await selectOption(ctx, "Timeouts", options);
|
|
@@ -2548,7 +2756,7 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2548
2756
|
async function showPerformanceSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2549
2757
|
while (true) {
|
|
2550
2758
|
const choice = await selectOption(ctx, "Performance", [
|
|
2551
|
-
opt("Timeouts", "
|
|
2759
|
+
opt("Timeouts", "Adjust per-operation time limits"),
|
|
2552
2760
|
opt("Back", "Return to the previous menu"),
|
|
2553
2761
|
]);
|
|
2554
2762
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2649,10 +2857,10 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2649
2857
|
while (true) {
|
|
2650
2858
|
const choice = await selectOption(ctx, "General", [
|
|
2651
2859
|
opt(`Commit automatically: ${orchestrator.config.general.autoCommit ? "Yes" : "No"}`, "Enable or disable auto commits"),
|
|
2860
|
+
opt(`Inject root AGENTS.md: ${orchestrator.config.general.injectAgentsMd ? "Yes" : "No"}`, "Inject the working repo's root AGENTS.md into the agent system prompt"),
|
|
2652
2861
|
opt(`Ignore configs from other repos: ${orchestrator.config.general.loadExtraRepoConfigs ? "No" : "Yes"}`, "Load only root repo config"),
|
|
2653
2862
|
opt(`Log level: ${logLevelLabel(orchestrator.config.general.logLevel)}`, "Logging verbosity"),
|
|
2654
2863
|
opt(`Tracing: ${orchestrator.config.general.tracing ? "Yes" : "No"}`, "Capture full session traces to .pp/logs/traces/"),
|
|
2655
|
-
opt("Flant AI Infrastructure", "Configure corporate AI model provider"),
|
|
2656
2864
|
opt("Back", "Return to the previous menu"),
|
|
2657
2865
|
]);
|
|
2658
2866
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2660,6 +2868,10 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2660
2868
|
await showBooleanSetting(orchestrator, ctx, "Commit automatically", ["general", "autoCommit"], "Commit changes automatically as work progresses", "Leave committing to you");
|
|
2661
2869
|
continue;
|
|
2662
2870
|
}
|
|
2871
|
+
if (choice.startsWith("Inject root AGENTS.md:")) {
|
|
2872
|
+
await showBooleanSetting(orchestrator, ctx, "Inject root AGENTS.md", ["general", "injectAgentsMd"], "Inject the working repo's root AGENTS.md into the agent system prompt", "Do not inject AGENTS.md");
|
|
2873
|
+
continue;
|
|
2874
|
+
}
|
|
2663
2875
|
if (choice.startsWith("Ignore configs from other repos:")) {
|
|
2664
2876
|
await showInvertedBooleanSetting(orchestrator, ctx, "Ignore configs from other repos", ["general", "loadExtraRepoConfigs"], "Use only the root repo config and ignore configs from other registered repos", "Also load configs from the other registered repos");
|
|
2665
2877
|
continue;
|
|
@@ -2672,15 +2884,19 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2672
2884
|
await showBooleanSetting(orchestrator, ctx, "Tracing", ["general", "tracing"], "Capture full session traces to .pp/logs/traces/", "Do not record session traces");
|
|
2673
2885
|
continue;
|
|
2674
2886
|
}
|
|
2675
|
-
await showFlantInfraMenu(orchestrator, ctx);
|
|
2676
2887
|
}
|
|
2677
2888
|
}
|
|
2678
2889
|
|
|
2890
|
+
const MAX_CONCURRENT_SUBAGENTS_LABEL = "Max concurrent subagents";
|
|
2891
|
+
const MAX_CONCURRENT_SUBAGENTS_PATH = ["agents", "maxConcurrentSubagents"];
|
|
2892
|
+
|
|
2679
2893
|
async function showAgentsSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2680
2894
|
while (true) {
|
|
2895
|
+
const limit = orchestrator.config.agents.maxConcurrentSubagents;
|
|
2681
2896
|
const choice = await selectOption(ctx, "Agents", [
|
|
2682
2897
|
opt("Orchestrators", "Orchestrator and subagent configuration"),
|
|
2683
2898
|
opt("Subagents", "Simple subagents and preset groups"),
|
|
2899
|
+
opt(`${MAX_CONCURRENT_SUBAGENTS_LABEL}: ${limit}`, "Max background/orchestrated subagents run at once; extras queue"),
|
|
2684
2900
|
opt("Back", "Return to the previous menu"),
|
|
2685
2901
|
]);
|
|
2686
2902
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2688,10 +2904,56 @@ async function showAgentsSettings(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
2688
2904
|
await showOrchestratorsSettings(orchestrator, ctx);
|
|
2689
2905
|
continue;
|
|
2690
2906
|
}
|
|
2907
|
+
if (choice.startsWith(`${MAX_CONCURRENT_SUBAGENTS_LABEL}:`)) {
|
|
2908
|
+
await showMaxConcurrentSubagentsSetting(orchestrator, ctx);
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2691
2911
|
await showSubagentSettings(orchestrator, ctx);
|
|
2692
2912
|
}
|
|
2693
2913
|
}
|
|
2694
2914
|
|
|
2915
|
+
async function showMaxConcurrentSubagentsSetting(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2916
|
+
while (true) {
|
|
2917
|
+
const current = getNestedValue(orchestrator.config, MAX_CONCURRENT_SUBAGENTS_PATH);
|
|
2918
|
+
if (typeof current !== "number") break;
|
|
2919
|
+
const action = await selectOption(ctx, `${MAX_CONCURRENT_SUBAGENTS_LABEL}: ${current}`, [
|
|
2920
|
+
opt("Edit", "Set the maximum number of concurrent background subagents"),
|
|
2921
|
+
...buildResetOptions(orchestrator, MAX_CONCURRENT_SUBAGENTS_PATH),
|
|
2922
|
+
opt("Back", "Return to the previous menu"),
|
|
2923
|
+
]);
|
|
2924
|
+
if (!action || action === "Back") break;
|
|
2925
|
+
if (action === "Edit") {
|
|
2926
|
+
const value = await pickMaxConcurrentSubagents(ctx, current);
|
|
2927
|
+
if (value === null) continue;
|
|
2928
|
+
applyScopeChoice(orchestrator, MAX_CONCURRENT_SUBAGENTS_PATH, value, await pickScope(ctx, orchestrator));
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
await maybeHandleResetChoice(orchestrator, ctx, action, MAX_CONCURRENT_SUBAGENTS_PATH);
|
|
2932
|
+
}
|
|
2933
|
+
return BACK;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
async function pickMaxConcurrentSubagents(ctx: any, current: number): Promise<number | null> {
|
|
2937
|
+
while (true) {
|
|
2938
|
+
const input = await ctx.ui.input(
|
|
2939
|
+
`Max concurrent subagents (positive integer, 1-${MAX_CONCURRENT_SUBAGENTS_CEILING}) [${current}]`,
|
|
2940
|
+
);
|
|
2941
|
+
if (input === undefined || input === null) return null;
|
|
2942
|
+
const trimmed = String(input).trim();
|
|
2943
|
+
if (trimmed === "") return null;
|
|
2944
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
2945
|
+
ctx.ui.notify(`Please enter a positive integer between 1 and ${MAX_CONCURRENT_SUBAGENTS_CEILING}.`, "warning");
|
|
2946
|
+
continue;
|
|
2947
|
+
}
|
|
2948
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
2949
|
+
if (parsed < 1 || parsed > MAX_CONCURRENT_SUBAGENTS_CEILING) {
|
|
2950
|
+
ctx.ui.notify(`Please enter a positive integer between 1 and ${MAX_CONCURRENT_SUBAGENTS_CEILING}.`, "warning");
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
return parsed;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2695
2957
|
async function showReposSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2696
2958
|
while (true) {
|
|
2697
2959
|
const repos = orchestrator.active?.state.repos ?? [];
|
|
@@ -2760,11 +3022,13 @@ async function showLspSettings(ctx: any): Promise<typeof BACK> {
|
|
|
2760
3022
|
async function showSettingsMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2761
3023
|
while (true) {
|
|
2762
3024
|
const options: OptionInput[] = [
|
|
2763
|
-
opt("General", "Commit, log level,
|
|
3025
|
+
opt("General", "Commit, log level, repos"),
|
|
2764
3026
|
opt("Agents", "Orchestrator and subagent configuration"),
|
|
2765
3027
|
opt("Commands", "After file edit and after implementation"),
|
|
2766
|
-
opt("Performance", "
|
|
3028
|
+
opt("Performance", "Per-operation timeout limits"),
|
|
2767
3029
|
opt("LSP", "Language server controls"),
|
|
3030
|
+
opt("Flant", "Configure corporate AI model provider"),
|
|
3031
|
+
opt("Info", "Usage and task status"),
|
|
2768
3032
|
opt("Back", "Return to the previous menu"),
|
|
2769
3033
|
];
|
|
2770
3034
|
|
|
@@ -2776,12 +3040,14 @@ async function showSettingsMenu(orchestrator: Orchestrator, ctx: any): Promise<t
|
|
|
2776
3040
|
else if (choice === "Commands") await showCommandsSettings(orchestrator, ctx);
|
|
2777
3041
|
else if (choice === "Performance") await showPerformanceSettings(orchestrator, ctx);
|
|
2778
3042
|
else if (choice === "LSP") await showLspSettings(ctx);
|
|
3043
|
+
else if (choice === "Flant") await showFlantInfraMenu(orchestrator, ctx);
|
|
3044
|
+
else if (choice === "Info") await showInfoMenu(orchestrator, ctx);
|
|
2779
3045
|
}
|
|
2780
3046
|
}
|
|
2781
3047
|
|
|
2782
3048
|
// First phases (brainstorm/debug/review) are always interactive and can never run
|
|
2783
3049
|
// autonomously — autonomous configs only ever cover plan/implement.
|
|
2784
|
-
function autonomousPhasesForTask(type: TaskType): string[] {
|
|
3050
|
+
export function autonomousPhasesForTask(type: TaskType): string[] {
|
|
2785
3051
|
if (type === "implement" || type === "debug" || type === "review") return ["plan", "implement"];
|
|
2786
3052
|
return [];
|
|
2787
3053
|
}
|
|
@@ -2855,10 +3121,10 @@ async function showAutonomousPhaseSettings(
|
|
|
2855
3121
|
const reviewPreset = phaseConfig.reviewPreset ?? defaultAutonomousReviewPreset(type, phase);
|
|
2856
3122
|
const maxReview = phaseConfig.maxReviewPasses >= 999 ? "No limit" : String(phaseConfig.maxReviewPasses);
|
|
2857
3123
|
const options: OptionInput[] = [
|
|
2858
|
-
opt(`Review preset: ${reviewPreset}`, "
|
|
3124
|
+
opt(`Review preset: ${reviewPreset}`, "Pick which reviewer group runs in this phase"),
|
|
2859
3125
|
];
|
|
2860
3126
|
if (phase === "plan") {
|
|
2861
|
-
options.push(opt(`Planner preset: ${phaseConfig.plannerPreset ?? "regular"}`, "
|
|
3127
|
+
options.push(opt(`Planner preset: ${phaseConfig.plannerPreset ?? "regular"}`, "Pick which planner group runs in this phase"));
|
|
2862
3128
|
}
|
|
2863
3129
|
options.push(opt(`Max review passes: ${maxReview}`, "Safety cap for autonomous review loops"));
|
|
2864
3130
|
options.push(opt("Back", "Return to autonomous settings"));
|
|
@@ -2927,7 +3193,8 @@ async function pickModeForTaskStart(
|
|
|
2927
3193
|
// uniqueness across the menu (see buildResumeOptions) so the option->task
|
|
2928
3194
|
// mapping stays stable.
|
|
2929
3195
|
function resumeOptionTitle(t: TaskInfo): string {
|
|
2930
|
-
|
|
3196
|
+
const marker = t.state.phase === "done" ? "✓ done · " : "";
|
|
3197
|
+
return `${marker}${taskNameFromState(t.dir, t.state)} — ${taskAge(t.state)}`;
|
|
2931
3198
|
}
|
|
2932
3199
|
|
|
2933
3200
|
// Description: rich per-entry detail sourced entirely from the already-loaded
|
|
@@ -2955,7 +3222,12 @@ function resumeOptionDescription(t: TaskInfo, cwd: string): string {
|
|
|
2955
3222
|
}
|
|
2956
3223
|
|
|
2957
3224
|
parts.push(`id ${taskShortId(t.dir)}`);
|
|
2958
|
-
|
|
3225
|
+
|
|
3226
|
+
// Metadata line first, then a blank line, then the full (untrimmed) intent, so
|
|
3227
|
+
// the right pane leads with phase/mode/id and still shows the real task below.
|
|
3228
|
+
const full = taskFullName(t.dir, s);
|
|
3229
|
+
const meta = parts.join(" · ");
|
|
3230
|
+
return full && full !== meta ? `${meta}\n\n${full}` : meta;
|
|
2959
3231
|
}
|
|
2960
3232
|
|
|
2961
3233
|
// Build menu options with a stable option->task index. Titles are made unique
|
|
@@ -2978,24 +3250,114 @@ function buildResumeOptions(tasks: TaskInfo[], cwd: string): { options: OptionIn
|
|
|
2978
3250
|
return { options, byTitle };
|
|
2979
3251
|
}
|
|
2980
3252
|
|
|
3253
|
+
type StatusFilter = "Active" | "Active + Done" | "Done only" | "All";
|
|
3254
|
+
interface ResumeFilters {
|
|
3255
|
+
status: StatusFilter;
|
|
3256
|
+
type: "All" | TaskType;
|
|
3257
|
+
mode: "All" | TaskMode;
|
|
3258
|
+
repo: string;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
const STATUS_CYCLE: StatusFilter[] = ["Active", "Active + Done", "Done only", "All"];
|
|
3262
|
+
const TYPE_CYCLE: Array<"All" | TaskType> = ["All", "implement", "debug", "brainstorm", "review", "quick"];
|
|
3263
|
+
const MODE_CYCLE: Array<"All" | TaskMode> = ["All", "guided", "autonomous"];
|
|
3264
|
+
|
|
3265
|
+
function defaultResumeFilters(): ResumeFilters {
|
|
3266
|
+
return { status: "Active", type: "All", mode: "All", repo: "All" };
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
function filtersSummary(f: ResumeFilters): string {
|
|
3270
|
+
return `Status: ${f.status} · Type: ${f.type} · Mode: ${f.mode} · Repo: ${f.repo}`;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
function applyResumeFilters(tasks: TaskInfo[], f: ResumeFilters): TaskInfo[] {
|
|
3274
|
+
return tasks.filter((t) => {
|
|
3275
|
+
const isDone = t.state.phase === "done";
|
|
3276
|
+
if (f.status === "Active" && isDone) return false;
|
|
3277
|
+
if (f.status === "Done only" && !isDone) return false;
|
|
3278
|
+
if (f.type !== "All" && t.type !== f.type) return false;
|
|
3279
|
+
if (f.mode !== "All" && (t.state.effectiveMode ?? t.state.mode) !== f.mode) return false;
|
|
3280
|
+
if (f.repo !== "All") {
|
|
3281
|
+
const rootRepo = t.state.repos?.find((r) => r.isRoot) ?? t.state.repos?.[0];
|
|
3282
|
+
if (!rootRepo || basename(rootRepo.path) !== f.repo) return false;
|
|
3283
|
+
}
|
|
3284
|
+
return true;
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
function cycle<T>(values: readonly T[], current: T): T {
|
|
3289
|
+
const idx = values.indexOf(current);
|
|
3290
|
+
return values[(idx + 1) % values.length]!;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
async function showResumeFilters(
|
|
3294
|
+
orchestrator: Orchestrator,
|
|
3295
|
+
ctx: any,
|
|
3296
|
+
filters: ResumeFilters,
|
|
3297
|
+
lockToType: TaskType | undefined,
|
|
3298
|
+
): Promise<void> {
|
|
3299
|
+
const repoNames = Array.from(
|
|
3300
|
+
new Set(
|
|
3301
|
+
listTasks(orchestrator.cwd, { type: lockToType, includeDone: true })
|
|
3302
|
+
.map((t) => {
|
|
3303
|
+
const rootRepo = t.state.repos?.find((r) => r.isRoot) ?? t.state.repos?.[0];
|
|
3304
|
+
return rootRepo ? basename(rootRepo.path) : null;
|
|
3305
|
+
})
|
|
3306
|
+
.filter((n): n is string => !!n),
|
|
3307
|
+
),
|
|
3308
|
+
);
|
|
3309
|
+
const repoCycle = ["All", ...repoNames];
|
|
3310
|
+
while (true) {
|
|
3311
|
+
const options: OptionInput[] = [
|
|
3312
|
+
opt(`Status: ${filters.status}`, "Cycle: Active / Active + Done / Done only / All"),
|
|
3313
|
+
];
|
|
3314
|
+
if (!lockToType) options.push(opt(`Type: ${filters.type}`, "Cycle task type"));
|
|
3315
|
+
options.push(opt(`Mode: ${filters.mode}`, "Cycle: All / guided / autonomous"));
|
|
3316
|
+
options.push(opt(`Repo: ${filters.repo}`, "Cycle registered repo"));
|
|
3317
|
+
options.push(opt("Clear filters", "Reset to defaults"));
|
|
3318
|
+
options.push(opt("Back", "Return to the Resume list"));
|
|
3319
|
+
|
|
3320
|
+
const choice = await selectOption(ctx, "Filters", options);
|
|
3321
|
+
if (!choice || choice === "Back") return;
|
|
3322
|
+
if (choice.startsWith("Status:")) filters.status = cycle(STATUS_CYCLE, filters.status);
|
|
3323
|
+
else if (choice.startsWith("Type:")) filters.type = cycle(TYPE_CYCLE, filters.type);
|
|
3324
|
+
else if (choice.startsWith("Mode:")) filters.mode = cycle(MODE_CYCLE, filters.mode);
|
|
3325
|
+
else if (choice.startsWith("Repo:")) filters.repo = cycle(repoCycle, filters.repo);
|
|
3326
|
+
else if (choice === "Clear filters") {
|
|
3327
|
+
const cleared = defaultResumeFilters();
|
|
3328
|
+
filters.status = cleared.status;
|
|
3329
|
+
filters.type = cleared.type;
|
|
3330
|
+
filters.mode = cleared.mode;
|
|
3331
|
+
filters.repo = cleared.repo;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
2981
3336
|
async function showResumeMenu(
|
|
2982
3337
|
orchestrator: Orchestrator,
|
|
2983
3338
|
ctx: any,
|
|
2984
3339
|
type: TaskType | undefined,
|
|
2985
3340
|
emptyMessage: string,
|
|
2986
3341
|
): Promise<typeof BACK | "started"> {
|
|
3342
|
+
const filters = defaultResumeFilters();
|
|
2987
3343
|
while (true) {
|
|
2988
|
-
const
|
|
2989
|
-
if (
|
|
3344
|
+
const all = listTasks(orchestrator.cwd, { type, includeDone: true });
|
|
3345
|
+
if (all.length === 0) {
|
|
2990
3346
|
ctx.ui.notify(emptyMessage, "info");
|
|
2991
3347
|
return BACK;
|
|
2992
3348
|
}
|
|
3349
|
+
const tasks = applyResumeFilters(all, filters);
|
|
2993
3350
|
|
|
2994
3351
|
const { options, byTitle } = buildResumeOptions(tasks, orchestrator.cwd);
|
|
3352
|
+
options.unshift({ title: "⚙ Filters", description: filtersSummary(filters) });
|
|
2995
3353
|
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
2996
3354
|
|
|
2997
3355
|
const choice = await selectOption(ctx, "Resume", options);
|
|
2998
3356
|
if (!choice || choice === "Back") return BACK;
|
|
3357
|
+
if (choice === "⚙ Filters") {
|
|
3358
|
+
await showResumeFilters(orchestrator, ctx, filters, type);
|
|
3359
|
+
continue;
|
|
3360
|
+
}
|
|
2999
3361
|
|
|
3000
3362
|
const task = byTitle.get(choice);
|
|
3001
3363
|
if (!task) continue;
|
|
@@ -3036,7 +3398,10 @@ async function showFromMenu(orchestrator: Orchestrator, ctx: any): Promise<typeo
|
|
|
3036
3398
|
|
|
3037
3399
|
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "implement");
|
|
3038
3400
|
if (!modeSelection) continue;
|
|
3039
|
-
|
|
3401
|
+
// Carry the source task's resolved name so the new implement task shows a
|
|
3402
|
+
// real name instead of the literal "implement" (#7).
|
|
3403
|
+
const inheritedName = taskFullName(selected.dir, selected.state);
|
|
3404
|
+
await orchestrator.startTask(ctx, "implement", inheritedName, selected.dir, true, modeSelection.mode);
|
|
3040
3405
|
if (orchestrator.active) {
|
|
3041
3406
|
orchestrator.active.state.autonomousConfig = modeSelection.autonomousConfig;
|
|
3042
3407
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
@@ -3134,9 +3499,14 @@ async function openCodeReviewInPlannotator(
|
|
|
3134
3499
|
if (payload.diffType) requestPayload.diffType = payload.diffType;
|
|
3135
3500
|
if (payload.defaultBranch) requestPayload.defaultBranch = payload.defaultBranch;
|
|
3136
3501
|
|
|
3137
|
-
const { opened, reviewId } = await openPlannotator(orchestrator.pi, "code-review", requestPayload);
|
|
3502
|
+
const { opened, reviewId, outcome } = await openPlannotator(orchestrator.pi, "code-review", requestPayload);
|
|
3138
3503
|
if (!opened) {
|
|
3139
|
-
return {
|
|
3504
|
+
return {
|
|
3505
|
+
status: "error",
|
|
3506
|
+
error: outcome === "timeout"
|
|
3507
|
+
? "Plannotator did not respond within 30s (is the browser extension running?)."
|
|
3508
|
+
: "Plannotator is not available (no handler responded — is the browser extension installed?).",
|
|
3509
|
+
};
|
|
3140
3510
|
}
|
|
3141
3511
|
|
|
3142
3512
|
let result: { approved: boolean; feedback?: string; error?: string };
|
|
@@ -3154,175 +3524,139 @@ async function openCodeReviewInPlannotator(
|
|
|
3154
3524
|
return { status: result.approved ? "approved" : "needs_changes", feedback };
|
|
3155
3525
|
}
|
|
3156
3526
|
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3527
|
+
// Per-repo interleaved Plannotator loop (#3a). Runs from the persisted cursor: for
|
|
3528
|
+
// each repo, ask the diff scope, open Plannotator and WAIT (dialogue closed). On
|
|
3529
|
+
// NEEDS_CHANGES, persist the cursor advanced past this repo and return a work
|
|
3530
|
+
// instruction (mirrors the plan path: answer questions + apply changes) so the
|
|
3531
|
+
// agent fixes THIS repo before the next opens; the next /pp resumes the loop. On
|
|
3532
|
+
// approved, advance and continue in-loop. On error the repo is left UNREVIEWED:
|
|
3533
|
+
// the cursor stays put and the user chooses Retry / Skip / Done. When the cursor
|
|
3534
|
+
// is exhausted or the user stops, clear the cursor and return null (fall back to
|
|
3535
|
+
// the menu).
|
|
3536
|
+
async function runPlannotatorCursor(orchestrator: Orchestrator, ctx: any): Promise<string | null> {
|
|
3537
|
+
const task = orchestrator.active;
|
|
3538
|
+
if (!task) return null;
|
|
3539
|
+
const cursor = task.state.plannotatorCursor;
|
|
3540
|
+
if (!cursor) return null;
|
|
3541
|
+
|
|
3542
|
+
while (task.state.plannotatorCursor && task.state.plannotatorCursor.index < task.state.plannotatorCursor.repoPaths.length) {
|
|
3543
|
+
const cur = task.state.plannotatorCursor;
|
|
3544
|
+
const repoPath = cur.repoPaths[cur.index];
|
|
3545
|
+
const repo = getRegisteredRepos(orchestrator).find((r) => r.path === repoPath) ?? { path: repoPath, isRoot: false };
|
|
3546
|
+
|
|
3547
|
+
const diffChoice = await selectOption(ctx, `Review: ${formatRepoLabel(repo)}`, [
|
|
3548
|
+
opt("All branch changes", "Committed changes vs base branch"),
|
|
3549
|
+
opt("Last commit", "Changes in the most recent commit"),
|
|
3550
|
+
opt("Since commit", "Review all changes since a specific commit"),
|
|
3551
|
+
opt("Uncommitted changes", "Working directory changes"),
|
|
3552
|
+
opt("Skip this repo", "Move to the next repository"),
|
|
3553
|
+
opt("Done (stop reviewing)", "Stop iterating repositories"),
|
|
3554
|
+
]);
|
|
3555
|
+
|
|
3556
|
+
if (!diffChoice || diffChoice === "Done (stop reviewing)") {
|
|
3557
|
+
task.state.plannotatorCursor = undefined;
|
|
3558
|
+
saveTask(task.dir, task.state);
|
|
3559
|
+
return null;
|
|
3560
|
+
}
|
|
3561
|
+
if (diffChoice === "Skip this repo") {
|
|
3562
|
+
cur.index += 1;
|
|
3563
|
+
saveTask(task.dir, task.state);
|
|
3564
|
+
continue;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
let diffType: string;
|
|
3568
|
+
let defaultBranch: string | undefined;
|
|
3569
|
+
if (diffChoice === "All branch changes") {
|
|
3570
|
+
diffType = "branch";
|
|
3571
|
+
defaultBranch = await detectDefaultBranch(orchestrator, getRegisteredRepos(orchestrator), repo.path);
|
|
3572
|
+
} else if (diffChoice === "Last commit") {
|
|
3573
|
+
diffType = "last-commit";
|
|
3574
|
+
} else if (diffChoice === "Since commit") {
|
|
3575
|
+
const pickedHash = await pickCommitForRepo(orchestrator, ctx, repo);
|
|
3576
|
+
if (!pickedHash) continue;
|
|
3577
|
+
diffType = "branch";
|
|
3578
|
+
defaultBranch = pickedHash;
|
|
3579
|
+
} else {
|
|
3580
|
+
diffType = "uncommitted";
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
const result = await openCodeReviewInPlannotator(orchestrator, { cwd: repo.path, diffType, defaultBranch });
|
|
3584
|
+
|
|
3585
|
+
// On error the repo is UNREVIEWED: do not advance the cursor (that would
|
|
3586
|
+
// silently drop it from the pass). Keep it as the current repo until the
|
|
3587
|
+
// user retries, explicitly skips, or stops.
|
|
3588
|
+
if (result.status === "error") {
|
|
3589
|
+
ctx.ui.notify(`${formatRepoLabel(repo)}: ERROR${result.error ? ` — ${result.error}` : ""}`, "warning");
|
|
3590
|
+
const errorChoice = await selectOption(ctx, `Review failed: ${formatRepoLabel(repo)}`, [
|
|
3591
|
+
opt("Retry", "Try reviewing this repository again"),
|
|
3592
|
+
opt("Skip this repo", "Leave this repository unreviewed and move on"),
|
|
3593
|
+
opt("Done (stop reviewing)", "Stop iterating repositories"),
|
|
3594
|
+
]);
|
|
3595
|
+
if (!errorChoice || errorChoice === "Done (stop reviewing)") {
|
|
3596
|
+
task.state.plannotatorCursor = undefined;
|
|
3597
|
+
saveTask(task.dir, task.state);
|
|
3598
|
+
return null;
|
|
3599
|
+
}
|
|
3600
|
+
if (errorChoice === "Skip this repo") {
|
|
3601
|
+
cur.index += 1;
|
|
3602
|
+
saveTask(task.dir, task.state);
|
|
3603
|
+
}
|
|
3604
|
+
// Retry: leave cur.index unchanged so the loop re-reviews this repo.
|
|
3605
|
+
continue;
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// Advance past this repo on a resolved outcome; on needs_changes the agent
|
|
3609
|
+
// fixes it during the turn started by the returned instruction, then the next
|
|
3610
|
+
// /pp resumes at the following repo.
|
|
3611
|
+
cur.index += 1;
|
|
3612
|
+
const exhausted = cur.index >= cur.repoPaths.length;
|
|
3613
|
+
|
|
3614
|
+
if (result.status === "needs_changes") {
|
|
3615
|
+
if (exhausted) task.state.plannotatorCursor = undefined;
|
|
3616
|
+
setStep(orchestrator, "llm_work");
|
|
3617
|
+
saveTask(task.dir, task.state);
|
|
3618
|
+
const feedback = result.feedback ? `\n\nFeedback:\n${result.feedback}` : "";
|
|
3619
|
+
const more = exhausted
|
|
3620
|
+
? "This was the last repo to review."
|
|
3621
|
+
: "After you finish, run /pp to continue Plannotator review of the remaining repositories.";
|
|
3622
|
+
return advanceBanner(
|
|
3623
|
+
`[PI-PI] Plannotator requested changes for ${formatRepoLabel(repo)}.${feedback}\n\n` +
|
|
3624
|
+
"Address the user's feedback. If the feedback contains questions, answer them. If it requests changes, " +
|
|
3625
|
+
`make the changes. Then call pp_phase_complete when done.\n\n${more}`,
|
|
3626
|
+
);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
ctx.ui.notify(`${formatRepoLabel(repo)}: APPROVED`, "info");
|
|
3630
|
+
saveTask(task.dir, task.state);
|
|
3175
3631
|
}
|
|
3176
|
-
|
|
3632
|
+
|
|
3633
|
+
// Cursor exhausted with no outstanding changes: clear it and fall back to /pp.
|
|
3634
|
+
task.state.plannotatorCursor = undefined;
|
|
3635
|
+
saveTask(task.dir, task.state);
|
|
3636
|
+
return null;
|
|
3177
3637
|
}
|
|
3178
3638
|
|
|
3179
3639
|
async function showReviewMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
3180
3640
|
while (true) {
|
|
3181
|
-
const
|
|
3182
|
-
{ title: "
|
|
3183
|
-
{ title: "Last commit", description: "Review changes in the most recent commit" },
|
|
3184
|
-
{ title: "Since commit", description: "Review all changes since a specific commit" },
|
|
3185
|
-
{ title: "Uncommitted changes", description: "Review working directory changes" },
|
|
3186
|
-
{ title: "Describe", description: "Describe what to review and let the agent figure it out" },
|
|
3641
|
+
const choice = await selectOption(ctx, "Review", [
|
|
3642
|
+
{ title: "New", description: "Start a new review — type what to review (a branch, commit range, uncommitted changes, or a PR URL) as your first chat message" },
|
|
3187
3643
|
{ title: "Resume", description: "Resume a previously unfinished review" },
|
|
3188
3644
|
{ title: "Back", description: "Return to the previous menu" },
|
|
3189
|
-
];
|
|
3190
|
-
const choice = await selectOption(ctx, "Review", options);
|
|
3645
|
+
]);
|
|
3191
3646
|
if (!choice || choice === "Back") return BACK;
|
|
3192
3647
|
|
|
3193
|
-
if (choice === "
|
|
3194
|
-
const result = await showResumeMenu(orchestrator, ctx, "review", "No paused review tasks found.");
|
|
3195
|
-
if (result === "started") return result;
|
|
3196
|
-
continue;
|
|
3197
|
-
}
|
|
3198
|
-
|
|
3199
|
-
if (choice === "Current branch") {
|
|
3200
|
-
const repos = getRegisteredRepos(orchestrator);
|
|
3201
|
-
const repoRanges = await Promise.all(
|
|
3202
|
-
repos.map(async (repo) => `- ${formatRepoLabel(repo)}: ${await detectDefaultBranch(orchestrator, repos, repo.path)}..HEAD`),
|
|
3203
|
-
);
|
|
3204
|
-
const prContexts = await detectCurrentPrContext(orchestrator, repos);
|
|
3205
|
-
|
|
3206
|
-
const urLines = [
|
|
3207
|
-
"# User Request",
|
|
3208
|
-
"Review current branch changes across registered repositories.",
|
|
3209
|
-
"",
|
|
3210
|
-
"Diff ranges:",
|
|
3211
|
-
...repoRanges,
|
|
3212
|
-
];
|
|
3213
|
-
const prUrls = prContexts
|
|
3214
|
-
.filter((pr) => pr.prUrl)
|
|
3215
|
-
.map((pr) => `- ${pr.repoPath}: ${pr.prUrl}`);
|
|
3216
|
-
if (prUrls.length > 0) {
|
|
3217
|
-
urLines.push("", "Open PRs:", ...prUrls);
|
|
3218
|
-
}
|
|
3219
|
-
urLines.push("", "## Problem", "Review and identify issues in the code changes.", "", "## Constraints", "Focus on correctness, edge cases, style, missing tests, potential bugs.");
|
|
3220
|
-
const urContent = urLines.join("\n") + "\n";
|
|
3221
|
-
|
|
3222
|
-
let resContent = [
|
|
3223
|
-
"## Affected Code",
|
|
3224
|
-
"(to be filled during review)",
|
|
3225
|
-
"",
|
|
3226
|
-
"## Architecture Context",
|
|
3227
|
-
"(to be filled during review)",
|
|
3228
|
-
"",
|
|
3229
|
-
"## Constraints & Edge Cases",
|
|
3230
|
-
"- MUST: Review all changed code across the registered repositories",
|
|
3231
|
-
"- RISK: Issues can span repository boundaries",
|
|
3232
|
-
].join("\n") + "\n";
|
|
3233
|
-
if (prContexts.length > 0) {
|
|
3234
|
-
const prContextBlocks = prContexts
|
|
3235
|
-
.map((pr) => {
|
|
3236
|
-
const lines = [`${pr.repoPath}`];
|
|
3237
|
-
if (pr.prUrl) lines.push(`URL: ${pr.prUrl}`);
|
|
3238
|
-
if (pr.prContext) lines.push(pr.prContext);
|
|
3239
|
-
return lines.join("\n");
|
|
3240
|
-
})
|
|
3241
|
-
.join("\n\n");
|
|
3242
|
-
resContent = appendResearchOpenQuestions(resContent, `PR context:\n${prContextBlocks}`);
|
|
3243
|
-
}
|
|
3244
|
-
|
|
3245
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3246
|
-
if (!modeSelection) continue;
|
|
3247
|
-
const description = "review-current-branch";
|
|
3248
|
-
return startReviewTask(orchestrator, ctx, urContent, resContent, description, modeSelection.mode, modeSelection.autonomousConfig);
|
|
3249
|
-
}
|
|
3250
|
-
|
|
3251
|
-
if (choice === "Last commit") {
|
|
3252
|
-
const repos = getRegisteredRepos(orchestrator);
|
|
3253
|
-
const urContent = [
|
|
3254
|
-
"# User Request",
|
|
3255
|
-
"Review last commit changes across registered repositories",
|
|
3256
|
-
"",
|
|
3257
|
-
"## Problem",
|
|
3258
|
-
"Review and identify issues in the most recent commit.",
|
|
3259
|
-
"",
|
|
3260
|
-
"## Constraints",
|
|
3261
|
-
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3262
|
-
"",
|
|
3263
|
-
].join("\n");
|
|
3264
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3265
|
-
if (!modeSelection) continue;
|
|
3266
|
-
return startReviewTask(orchestrator, ctx, urContent, null, "review-last-commit", modeSelection.mode, modeSelection.autonomousConfig);
|
|
3267
|
-
}
|
|
3268
|
-
|
|
3269
|
-
if (choice === "Since commit") {
|
|
3270
|
-
const repos = getRegisteredRepos(orchestrator);
|
|
3271
|
-
const repoOptions: OptionInput[] = repos.map((repo) => ({
|
|
3272
|
-
title: formatRepoLabel(repo),
|
|
3273
|
-
description: "Choose repository for commit range",
|
|
3274
|
-
}));
|
|
3275
|
-
repoOptions.push({ title: "Back", description: "Return to the previous menu" });
|
|
3276
|
-
const repoChoice = await selectOption(ctx, "Select repository", repoOptions);
|
|
3277
|
-
if (!repoChoice || repoChoice === "Back") continue;
|
|
3278
|
-
const selectedRepo = repos.find((repo) => formatRepoLabel(repo) === repoChoice);
|
|
3279
|
-
if (!selectedRepo) continue;
|
|
3280
|
-
const pickedHash = await pickCommitForRepo(orchestrator, ctx, selectedRepo);
|
|
3281
|
-
if (!pickedHash) continue;
|
|
3282
|
-
|
|
3283
|
-
const urContent = [
|
|
3284
|
-
"# User Request",
|
|
3285
|
-
`Review changes in ${selectedRepo.path} since commit ${pickedHash}`,
|
|
3286
|
-
"",
|
|
3287
|
-
"## Problem",
|
|
3288
|
-
`Review and identify issues in all changes in ${selectedRepo.path} since ${pickedHash}.`,
|
|
3289
|
-
"",
|
|
3290
|
-
"## Constraints",
|
|
3291
|
-
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3292
|
-
"",
|
|
3293
|
-
].join("\n");
|
|
3294
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3295
|
-
if (!modeSelection) continue;
|
|
3296
|
-
return startReviewTask(orchestrator, ctx, urContent, null, "review-since-commit", modeSelection.mode, modeSelection.autonomousConfig);
|
|
3297
|
-
}
|
|
3298
|
-
|
|
3299
|
-
if (choice === "Uncommitted changes") {
|
|
3300
|
-
const repos = getRegisteredRepos(orchestrator);
|
|
3301
|
-
const urContent = [
|
|
3302
|
-
"# User Request",
|
|
3303
|
-
"Review uncommitted changes across registered repositories",
|
|
3304
|
-
"",
|
|
3305
|
-
"## Problem",
|
|
3306
|
-
"Review and identify issues in uncommitted working directory changes.",
|
|
3307
|
-
"",
|
|
3308
|
-
"## Constraints",
|
|
3309
|
-
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3310
|
-
"",
|
|
3311
|
-
].join("\n");
|
|
3648
|
+
if (choice === "New") {
|
|
3312
3649
|
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3313
3650
|
if (!modeSelection) continue;
|
|
3314
|
-
|
|
3651
|
+
await orchestrator.startTask(ctx, "review", "review", undefined, undefined, modeSelection.mode);
|
|
3652
|
+
if (!orchestrator.active || orchestrator.active.type !== "review") return BACK;
|
|
3653
|
+
orchestrator.active.state.autonomousConfig = modeSelection.autonomousConfig;
|
|
3654
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
3655
|
+
return "started";
|
|
3315
3656
|
}
|
|
3316
3657
|
|
|
3317
|
-
const
|
|
3318
|
-
if (
|
|
3319
|
-
const trimmed = String(input).trim();
|
|
3320
|
-
const description = trimmed || "review";
|
|
3321
|
-
|
|
3322
|
-
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`;
|
|
3323
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3324
|
-
if (!modeSelection) continue;
|
|
3325
|
-
return startReviewTask(orchestrator, ctx, urContent, null, description, modeSelection.mode, modeSelection.autonomousConfig);
|
|
3658
|
+
const result = await showResumeMenu(orchestrator, ctx, "review", "No paused review tasks found.");
|
|
3659
|
+
if (result === "started") return result;
|
|
3326
3660
|
}
|
|
3327
3661
|
}
|
|
3328
3662
|
|
|
@@ -3411,15 +3745,56 @@ async function showTaskMenu(orchestrator: Orchestrator, ctx: any): Promise<typeo
|
|
|
3411
3745
|
}
|
|
3412
3746
|
}
|
|
3413
3747
|
|
|
3748
|
+
// Minimal read-only path shown when loadConfig threw on session_start. It must NOT
|
|
3749
|
+
// register or trigger normal task execution against the invalid config — it only
|
|
3750
|
+
// helps the user locate and fix the broken config, then re-check it.
|
|
3751
|
+
async function showConfigErrorMenu(orchestrator: Orchestrator, ctx: any): Promise<string | undefined> {
|
|
3752
|
+
const projectConfigPath = join(orchestrator.cwd, ".pp", "config.json");
|
|
3753
|
+
while (true) {
|
|
3754
|
+
const choice = await selectOption(
|
|
3755
|
+
ctx,
|
|
3756
|
+
`/pp — config error\n\n${orchestrator.configError}\n\nProject config: ${projectConfigPath}\nGlobal config: ${GLOBAL_CONFIG_PATH}`,
|
|
3757
|
+
[
|
|
3758
|
+
{ title: "Re-check config", description: "Reload config after fixing it" },
|
|
3759
|
+
{ title: "Doctor", description: "Run diagnostic checks" },
|
|
3760
|
+
{ title: "Back", description: "Close this menu" },
|
|
3761
|
+
],
|
|
3762
|
+
);
|
|
3763
|
+
if (!choice || choice === "Back") return undefined;
|
|
3764
|
+
|
|
3765
|
+
if (choice === "Re-check config") {
|
|
3766
|
+
try {
|
|
3767
|
+
orchestrator.config = loadConfig(orchestrator.cwd);
|
|
3768
|
+
orchestrator.configError = null;
|
|
3769
|
+
// session_start skipped feature/tool/agent registration when config
|
|
3770
|
+
// loading failed; register them now that the config is valid so the
|
|
3771
|
+
// orchestration tools exist, not just the menu.
|
|
3772
|
+
registerFeatureToolsAndAgents(orchestrator);
|
|
3773
|
+
ctx.ui.notify("Config loaded successfully. Run /pp again to continue.", "info");
|
|
3774
|
+
return undefined;
|
|
3775
|
+
} catch (err: any) {
|
|
3776
|
+
orchestrator.configError = err.message;
|
|
3777
|
+
ctx.ui.notify(`Config error: ${err.message}`, "error");
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
if (choice === "Doctor") {
|
|
3783
|
+
await runDoctor(orchestrator, ctx);
|
|
3784
|
+
continue;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3414
3789
|
async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<string | undefined> {
|
|
3415
3790
|
while (true) {
|
|
3416
3791
|
const choice = await selectOption(ctx, "/pp", [
|
|
3417
3792
|
{ title: "Task", description: "Start a new task or resume a paused one" },
|
|
3418
|
-
{ title: "
|
|
3419
|
-
{ title: "Settings", description: "
|
|
3420
|
-
{ title: "Back", description: "Close this menu" },
|
|
3793
|
+
{ title: "Subagents", description: "View and manage running subagents" },
|
|
3794
|
+
{ title: "Settings", description: "Models, agents, commands, and other configuration" },
|
|
3795
|
+
{ title: "Back to prompt", description: "Close this menu" },
|
|
3421
3796
|
]);
|
|
3422
|
-
if (!choice || choice === "Back") return undefined;
|
|
3797
|
+
if (!choice || choice === "Back to prompt") return undefined;
|
|
3423
3798
|
|
|
3424
3799
|
if (choice === "Task") {
|
|
3425
3800
|
const result = await showTaskMenu(orchestrator, ctx);
|
|
@@ -3427,8 +3802,8 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
|
|
|
3427
3802
|
continue;
|
|
3428
3803
|
}
|
|
3429
3804
|
|
|
3430
|
-
if (choice === "
|
|
3431
|
-
await
|
|
3805
|
+
if (choice === "Subagents") {
|
|
3806
|
+
await showSubagentsMenu(ctx);
|
|
3432
3807
|
continue;
|
|
3433
3808
|
}
|
|
3434
3809
|
|
|
@@ -3476,16 +3851,16 @@ async function showQuickTaskMenu(
|
|
|
3476
3851
|
const { choice, cancelReason } = await selectOptionCancelable(ctx, menuTitle, [
|
|
3477
3852
|
opt("Complete", "Mark task as done and clean up"),
|
|
3478
3853
|
opt("Pause", "Suspend task to resume later"),
|
|
3479
|
-
opt("
|
|
3480
|
-
opt("Settings", "
|
|
3481
|
-
opt("Back", "Return to the prompt and keep working"),
|
|
3854
|
+
opt("Subagents", "View and manage running subagents"),
|
|
3855
|
+
opt("Settings", "Models, agents, commands, and other configuration"),
|
|
3856
|
+
opt("Back to prompt", "Return to the prompt and keep working"),
|
|
3482
3857
|
]);
|
|
3483
3858
|
// A deliberate ESC in tool mode must stop the turn cleanly (mirror the
|
|
3484
3859
|
// guided/autonomous branches); in command mode ESC just closes the menu.
|
|
3485
3860
|
if (cancelReason === "user" && mode === "tool") return USER_CANCELLED;
|
|
3486
|
-
if (!choice || choice === "Back") return "";
|
|
3487
|
-
if (choice === "
|
|
3488
|
-
await
|
|
3861
|
+
if (!choice || choice === "Back to prompt") return "";
|
|
3862
|
+
if (choice === "Subagents") {
|
|
3863
|
+
await showSubagentsMenu(ctx);
|
|
3489
3864
|
continue;
|
|
3490
3865
|
}
|
|
3491
3866
|
if (choice === "Settings") {
|
|
@@ -3506,6 +3881,11 @@ export async function showActiveTaskMenu(
|
|
|
3506
3881
|
ctx: any,
|
|
3507
3882
|
summary: string,
|
|
3508
3883
|
mode: MenuMode = "command",
|
|
3884
|
+
// Display-only override for the autonomous terminal implement handoff (#1):
|
|
3885
|
+
// render the guided Next/Review menu even though the task is autonomous, WITHOUT
|
|
3886
|
+
// mutating task.state.mode. Never persisted; does not affect the footer indicator
|
|
3887
|
+
// or getEffectivePhaseMode.
|
|
3888
|
+
forceGuided = false,
|
|
3509
3889
|
): Promise<string> {
|
|
3510
3890
|
const continueMessage = advanceBanner("[PI-PI] User wants to continue. Run /pp when ready to advance.");
|
|
3511
3891
|
|
|
@@ -3516,6 +3896,15 @@ export async function showActiveTaskMenu(
|
|
|
3516
3896
|
if (task.type === "quick") {
|
|
3517
3897
|
return showQuickTaskMenu(orchestrator, ctx, summary, mode);
|
|
3518
3898
|
}
|
|
3899
|
+
// Auto-resume an in-progress per-repo Plannotator review (#3a): if the agent
|
|
3900
|
+
// just finished fixing one repo's feedback and ran /pp, continue the loop at
|
|
3901
|
+
// the next repo instead of showing the top-level menu. On another
|
|
3902
|
+
// needs_changes this returns a fresh work instruction; when exhausted it
|
|
3903
|
+
// clears the cursor and falls through to the normal menu.
|
|
3904
|
+
if (task.state.plannotatorCursor) {
|
|
3905
|
+
const resumeText = await runPlannotatorCursor(orchestrator, ctx);
|
|
3906
|
+
if (resumeText) return resumeText;
|
|
3907
|
+
}
|
|
3519
3908
|
const phase = task.state.phase;
|
|
3520
3909
|
const step = task.state.step;
|
|
3521
3910
|
const effectiveMode = getEffectivePhaseMode(task.state);
|
|
@@ -3527,24 +3916,35 @@ export async function showActiveTaskMenu(
|
|
|
3527
3916
|
const { autoLabel } = getReviewLabels(orchestrator);
|
|
3528
3917
|
const isReviewPhase = phase === "review";
|
|
3529
3918
|
const hasPlannotator = phase === "plan" || phase === "implement" || isReviewPhase;
|
|
3919
|
+
// The artifact the automated reviewers scan, mirroring reviewPresetGroupForPhase:
|
|
3920
|
+
// brainstorm/debug → brainstormReviewers (research artifacts), plan → planReviewers
|
|
3921
|
+
// (synthesized plan), implement/review → codeReviewers (code changes). This is the primary
|
|
3922
|
+
// "Review" target named at the top level; the per-phase "Review on my own" description names
|
|
3923
|
+
// the manual editor pass's target separately (it can differ).
|
|
3924
|
+
const reviewGroup = reviewPresetGroupForPhase(phase);
|
|
3925
|
+
const reviewTarget = reviewGroup === "planReviewers"
|
|
3926
|
+
? "the synthesized plan"
|
|
3927
|
+
: reviewGroup === "brainstormReviewers"
|
|
3928
|
+
? "this phase's research artifacts"
|
|
3929
|
+
: "the code changes";
|
|
3530
3930
|
|
|
3531
3931
|
const opt = (title: string, description: string): OptionInput => ({ title, description });
|
|
3532
3932
|
|
|
3533
|
-
if (effectiveMode === "autonomous") {
|
|
3933
|
+
if (effectiveMode === "autonomous" && !forceGuided) {
|
|
3534
3934
|
const { choice: autoChoice, cancelReason } = await selectOptionCancelable(ctx, `/pp\n\nTask: ${task.type}\nPhase: ${phase}${summary !== "/pp" ? `\n\n${summary}` : ""}`, [
|
|
3535
3935
|
opt("Complete task", "Mark task as done and clean up"),
|
|
3536
3936
|
opt("Pause task", "Suspend task to resume later"),
|
|
3537
|
-
opt("
|
|
3538
|
-
opt("Settings", "
|
|
3539
|
-
opt("Back", "Return to the prompt and keep working"),
|
|
3937
|
+
opt("Subagents", "View and manage running subagents"),
|
|
3938
|
+
opt("Settings", "Models, agents, commands, and other configuration"),
|
|
3939
|
+
opt("Back to prompt", "Return to the prompt and keep working"),
|
|
3540
3940
|
]);
|
|
3541
3941
|
// A deliberate ESC only needs distinct handling in tool mode (so
|
|
3542
3942
|
// pp_phase_complete can stop the turn); in command mode ESC just closes
|
|
3543
3943
|
// the menu like "Back".
|
|
3544
3944
|
if (cancelReason === "user" && mode === "tool") return USER_CANCELLED;
|
|
3545
|
-
if (!autoChoice || autoChoice === "Back") return "";
|
|
3546
|
-
if (autoChoice === "
|
|
3547
|
-
await
|
|
3945
|
+
if (!autoChoice || autoChoice === "Back to prompt") return "";
|
|
3946
|
+
if (autoChoice === "Subagents") {
|
|
3947
|
+
await showSubagentsMenu(ctx);
|
|
3548
3948
|
continue;
|
|
3549
3949
|
}
|
|
3550
3950
|
if (autoChoice === "Settings") {
|
|
@@ -3562,23 +3962,23 @@ export async function showActiveTaskMenu(
|
|
|
3562
3962
|
const options: OptionInput[] = [];
|
|
3563
3963
|
options.push(opt("Next", "Complete, pause, or continue to next phase"));
|
|
3564
3964
|
if (!waiting) {
|
|
3565
|
-
options.push(opt("Review", "
|
|
3965
|
+
options.push(opt("Review", `Review ${reviewTarget}: automated reviewers${hasPlannotator ? ", Plannotator, or" : " or"} your own editor pass`));
|
|
3566
3966
|
}
|
|
3567
|
-
options.push(opt("
|
|
3568
|
-
options.push(opt("Settings", "
|
|
3569
|
-
options.push(opt("Back", "Return to the prompt and keep working"));
|
|
3967
|
+
options.push(opt("Subagents", "View and manage running subagents"));
|
|
3968
|
+
options.push(opt("Settings", "Models, agents, commands, and other configuration"));
|
|
3969
|
+
options.push(opt("Back to prompt", "Return to the prompt and keep working"));
|
|
3570
3970
|
|
|
3571
3971
|
const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
|
|
3572
3972
|
if (summary !== "/pp") headerLines.push(`\n\n${summary}`);
|
|
3573
3973
|
const menuTitle = headerLines.join("");
|
|
3574
3974
|
const { choice, cancelReason } = await selectOptionCancelable(ctx, menuTitle, options);
|
|
3575
3975
|
if (cancelReason === "user" && mode === "tool") return USER_CANCELLED;
|
|
3576
|
-
if (!choice || choice === "Back") {
|
|
3976
|
+
if (!choice || choice === "Back to prompt") {
|
|
3577
3977
|
return "";
|
|
3578
3978
|
}
|
|
3579
3979
|
|
|
3580
|
-
if (choice === "
|
|
3581
|
-
await
|
|
3980
|
+
if (choice === "Subagents") {
|
|
3981
|
+
await showSubagentsMenu(ctx);
|
|
3582
3982
|
continue;
|
|
3583
3983
|
}
|
|
3584
3984
|
if (choice === "Settings") {
|
|
@@ -3590,72 +3990,197 @@ export async function showActiveTaskMenu(
|
|
|
3590
3990
|
}
|
|
3591
3991
|
|
|
3592
3992
|
if (choice === "Next") {
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3993
|
+
// The Next submenu runs its own loop so that a deeper submenu's "Back"
|
|
3994
|
+
// (e.g. Publish) returns here rather than overshooting to the top-level menu.
|
|
3995
|
+
while (true) {
|
|
3996
|
+
const canContinue = phase !== "implement" && !waiting;
|
|
3997
|
+
const continueLabel = phase === "plan" ? "Continue to implement" : "Continue to plan & implement";
|
|
3998
|
+
const finishOptions: OptionInput[] = [];
|
|
3999
|
+
if (phase === "review") {
|
|
4000
|
+
finishOptions.push(opt("Publish", "Publish the synthesized review findings as file comments or GitHub PR comments"));
|
|
4001
|
+
}
|
|
4002
|
+
const nextPhaseName = phase === "plan" ? "implement" : "plan & implement";
|
|
4003
|
+
const autoThenContinueLabel = `Auto review, then continue to ${nextPhaseName}`;
|
|
4004
|
+
if (canContinue) {
|
|
4005
|
+
finishOptions.push(opt(continueLabel, "Approve and advance to the next phase"));
|
|
4006
|
+
finishOptions.push(opt(autoThenContinueLabel, "Loop reviewers over this phase until they approve (up to N passes), then advance"));
|
|
4007
|
+
}
|
|
4008
|
+
finishOptions.push(opt("Complete", "Mark task as done and clean up"));
|
|
4009
|
+
finishOptions.push(opt("Pause", "Suspend task to resume later"));
|
|
4010
|
+
finishOptions.push(opt("Back", "Return to the previous menu"));
|
|
4011
|
+
|
|
4012
|
+
const finishChoice = await selectOption(ctx, "Next", finishOptions);
|
|
4013
|
+
if (!finishChoice || finishChoice === "Back") break;
|
|
4014
|
+
if (finishChoice === autoThenContinueLabel) {
|
|
4015
|
+
const next = nextPhase(task.type, phase);
|
|
4016
|
+
// Collect the SAME advance inputs the plain-continue path would, NOW,
|
|
4017
|
+
// and stash them on the flag — the later headless pp_phase_complete
|
|
4018
|
+
// branch that advances cannot prompt the user.
|
|
4019
|
+
const deferred: { mode?: TaskMode; autonomousConfig?: AutonomousConfig; plannerPreset?: string } = {};
|
|
4020
|
+
if (next === "plan" && task.type === "brainstorm") {
|
|
4021
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "implement");
|
|
4022
|
+
if (!modeSelection) continue;
|
|
4023
|
+
deferred.mode = modeSelection.mode;
|
|
4024
|
+
deferred.autonomousConfig = modeSelection.autonomousConfig;
|
|
4025
|
+
}
|
|
4026
|
+
if (next === "plan" && getEffectiveMode(task.state) !== "autonomous") {
|
|
4027
|
+
const pickedPlannerPreset = await pickPreset(ctx, orchestrator, "planners", "Planner preset");
|
|
4028
|
+
if (!pickedPlannerPreset) continue;
|
|
4029
|
+
deferred.plannerPreset = pickedPlannerPreset;
|
|
4030
|
+
}
|
|
4031
|
+
// Already approved clean this phase: skip review, advance straight through.
|
|
4032
|
+
if (task.state.reviewApprovedClean) {
|
|
4033
|
+
if (deferred.mode) task.state.mode = deferred.mode;
|
|
4034
|
+
if (deferred.autonomousConfig) task.state.autonomousConfig = deferred.autonomousConfig;
|
|
4035
|
+
task.state.effectiveMode = undefined;
|
|
4036
|
+
finalizeReviewCycle(task);
|
|
4037
|
+
const advResult = await orchestrator.transitionToNextPhase(ctx, deferred.plannerPreset);
|
|
4038
|
+
if (!advResult.ok) return `Transition blocked: ${advResult.error}`;
|
|
4039
|
+
return "";
|
|
4040
|
+
}
|
|
4041
|
+
const loopPreset = await pickPreset(ctx, orchestrator, getReviewPresetGroup(phase), "Review preset");
|
|
4042
|
+
if (!loopPreset) continue;
|
|
4043
|
+
if (!hasEnabledReviewers(orchestrator, loopPreset)) {
|
|
4044
|
+
const rg = reviewPresetGroupForPhase(phase);
|
|
4045
|
+
const label = rg === "brainstormReviewers" ? "artifact" : rg === "planReviewers" ? "plan" : "code";
|
|
4046
|
+
ctx.ui.notify(`No ${label} reviewers enabled.`, "info");
|
|
4047
|
+
continue;
|
|
4048
|
+
}
|
|
4049
|
+
const passes = await pickMaxReviewPasses(ctx, 3);
|
|
4050
|
+
if (passes === null) continue;
|
|
4051
|
+
finalizeReviewCycle(task);
|
|
4052
|
+
if (task.state.reviewPassByKind?.[phase]) task.state.reviewPassByKind[phase].auto = 0;
|
|
4053
|
+
task.state.reviewApprovedClean = false;
|
|
4054
|
+
task.state.manualAutoReview = { phase, preset: loopPreset, maxPasses: passes, advanceOnComplete: true, deferredAdvance: deferred };
|
|
4055
|
+
saveTask(task.dir, task.state);
|
|
4056
|
+
return advanceBanner(
|
|
4057
|
+
`[PI-PI] Auto-review-then-continue started over ${reviewTarget} (up to ${passes >= 999 ? "unlimited" : passes} passes). ` +
|
|
4058
|
+
"Call pp_phase_complete now to run the first review pass; after each pass, apply the reviewers' feedback and call pp_phase_complete again. " +
|
|
4059
|
+
"Do NOT pause to ask for design or user approval while this loop runs — it supersedes the pre-finalization approval rule. " +
|
|
4060
|
+
`When reviewers unanimously approve or the pass cap is reached, the task advances to ${nextPhaseName} automatically.`,
|
|
4061
|
+
);
|
|
4062
|
+
}
|
|
4063
|
+
if (finishChoice === "Publish") {
|
|
4064
|
+
const target = await selectOption(ctx, "Publish", [
|
|
4065
|
+
opt("As file comments", "Insert AI_COMMENT: markers at each finding's location in the source"),
|
|
4066
|
+
opt("As GitHub PR comments", "Post line-anchored comments to the branch's PR from your GitHub account"),
|
|
4067
|
+
opt("Back", "Return to the previous menu"),
|
|
4068
|
+
]);
|
|
4069
|
+
if (!target || target === "Back") continue;
|
|
4070
|
+
const guardMessage = publishGuard(task.dir);
|
|
4071
|
+
if (guardMessage) {
|
|
4072
|
+
ctx.ui.notify(guardMessage, "info");
|
|
4073
|
+
continue;
|
|
4074
|
+
}
|
|
4075
|
+
return target === "As file comments"
|
|
4076
|
+
? publishFileCommentsBanner(task.dir)
|
|
4077
|
+
: publishPrCommentsBanner(task.dir);
|
|
4078
|
+
}
|
|
4079
|
+
if (finishChoice === "Pause") {
|
|
4080
|
+
const text = await pauseTask(orchestrator, ctx);
|
|
4081
|
+
return mode === "tool" ? text : "";
|
|
4082
|
+
}
|
|
4083
|
+
if (finishChoice === "Complete") {
|
|
4084
|
+
const text = await finishTask(orchestrator, ctx);
|
|
4085
|
+
return mode === "tool" ? text : "";
|
|
4086
|
+
}
|
|
4087
|
+
const next = nextPhase(task.type, phase);
|
|
4088
|
+
|
|
4089
|
+
if (
|
|
4090
|
+
next === "plan" &&
|
|
4091
|
+
task.type === "brainstorm"
|
|
4092
|
+
) {
|
|
4093
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "implement");
|
|
4094
|
+
if (!modeSelection) continue;
|
|
4095
|
+
task.state.mode = modeSelection.mode;
|
|
4096
|
+
task.state.effectiveMode = undefined;
|
|
4097
|
+
task.state.autonomousConfig = modeSelection.autonomousConfig;
|
|
4098
|
+
saveTask(task.dir, task.state);
|
|
4099
|
+
}
|
|
3626
4100
|
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
4101
|
+
let plannerPreset: string | undefined;
|
|
4102
|
+
if (next === "plan") {
|
|
4103
|
+
if (getEffectiveMode(task.state) !== "autonomous") {
|
|
4104
|
+
const pickedPlannerPreset = await pickPreset(ctx, orchestrator, "planners", "Planner preset");
|
|
4105
|
+
if (!pickedPlannerPreset) continue;
|
|
4106
|
+
plannerPreset = pickedPlannerPreset;
|
|
4107
|
+
}
|
|
3633
4108
|
}
|
|
4109
|
+
finalizeReviewCycle(task);
|
|
4110
|
+
const result = await orchestrator.transitionToNextPhase(ctx, plannerPreset);
|
|
4111
|
+
if (!result.ok) return `Transition blocked: ${result.error}`;
|
|
4112
|
+
// Transition is now owned by the controller (state ≠ running) or we're
|
|
4113
|
+
// awaiting subagents; either way the menu returns empty.
|
|
4114
|
+
return "";
|
|
3634
4115
|
}
|
|
3635
|
-
|
|
3636
|
-
const result = await orchestrator.transitionToNextPhase(ctx, plannerPreset);
|
|
3637
|
-
if (!result.ok) return `Transition blocked: ${result.error}`;
|
|
3638
|
-
// Transition is now owned by the controller (state ≠ running) or we're
|
|
3639
|
-
// awaiting subagents; either way the menu returns empty.
|
|
3640
|
-
return "";
|
|
4116
|
+
continue;
|
|
3641
4117
|
}
|
|
3642
4118
|
|
|
3643
4119
|
if (choice === "Review") {
|
|
4120
|
+
// The Review submenu runs its own loop so a recoverable choice (no reviewers
|
|
4121
|
+
// enabled, preset cancelled, "Review on my own" Back, a plannotator early
|
|
4122
|
+
// exit) returns HERE rather than overshooting to the top-level /pp menu.
|
|
4123
|
+
// Only an explicit "Back" breaks out; start/wait/instruction paths return.
|
|
4124
|
+
while (true) {
|
|
4125
|
+
// A review already running: block re-entry BEFORE any finalizeReviewCycle
|
|
4126
|
+
// (which would null the live cycle and hide it), so we never double-spawn.
|
|
4127
|
+
// Notify and break back to the top-level menu with the live cycle intact
|
|
4128
|
+
// rather than exiting /pp.
|
|
4129
|
+
if (isReviewCycleLive(task)) {
|
|
4130
|
+
ctx.ui.notify("A review is already running", "info");
|
|
4131
|
+
break;
|
|
4132
|
+
}
|
|
3644
4133
|
const reviewOptions: OptionInput[] = [
|
|
3645
|
-
opt(autoLabel,
|
|
4134
|
+
opt(autoLabel, `Run configured reviewers over ${reviewTarget}`),
|
|
4135
|
+
opt("Auto review until approved", `Loop reviewers over ${reviewTarget} until they unanimously approve, up to N passes`),
|
|
3646
4136
|
];
|
|
4137
|
+
const hasArtifactPlannotator = phase === "brainstorm" || phase === "debug";
|
|
3647
4138
|
if (hasPlannotator) {
|
|
3648
4139
|
reviewOptions.push(opt("Review in Plannotator", phase === "plan" ? "Open plan review in browser" : "Open code diff review in browser"));
|
|
4140
|
+
} else if (hasArtifactPlannotator) {
|
|
4141
|
+
reviewOptions.push(opt("Review in Plannotator", "Open USER_REQUEST.md, RESEARCH.md, and artifacts/*.md for review in browser"));
|
|
3649
4142
|
}
|
|
3650
4143
|
reviewOptions.push(opt("Review on my own", phase === "implement"
|
|
3651
|
-
? "
|
|
3652
|
-
:
|
|
4144
|
+
? "Mark spots in the changed files with AI_REVIEW: comments; the agent then addresses each and removes the marker"
|
|
4145
|
+
: phase === "plan"
|
|
4146
|
+
? "Mark spots in the synthesized plan with AI_REVIEW: comments; the agent then addresses each and removes the marker"
|
|
4147
|
+
: "Mark spots in USER_REQUEST.md, RESEARCH.md, and artifacts/*.md with AI_REVIEW: comments; the agent then addresses each and removes the marker"));
|
|
3653
4148
|
reviewOptions.push(opt("Back", "Return to the previous menu"));
|
|
3654
4149
|
|
|
3655
4150
|
const reviewChoice = await selectOption(ctx, "Review", reviewOptions);
|
|
3656
|
-
if (!reviewChoice || reviewChoice === "Back")
|
|
4151
|
+
if (!reviewChoice || reviewChoice === "Back") break;
|
|
3657
4152
|
|
|
3658
4153
|
if (reviewChoice === "Review in Plannotator") {
|
|
4154
|
+
if (hasArtifactPlannotator) {
|
|
4155
|
+
// brainstorm/debug have no diff or plan file: review the whole artifact
|
|
4156
|
+
// set in one annotate-folder pass over the task dir (walks USER_REQUEST.md +
|
|
4157
|
+
// RESEARCH.md + artifacts/*.md recursively). The result comes back on the
|
|
4158
|
+
// synchronous annotate respond callback, NOT via waitForPlannotatorResult.
|
|
4159
|
+
const { opened, result } = await openAnnotateReview(orchestrator.pi, {
|
|
4160
|
+
filePath: task.dir,
|
|
4161
|
+
folderPath: task.dir,
|
|
4162
|
+
mode: "annotate-folder",
|
|
4163
|
+
gate: true,
|
|
4164
|
+
});
|
|
4165
|
+
if (!opened || !result) {
|
|
4166
|
+
ctx.ui.notify("Could not open Plannotator for review.", "error");
|
|
4167
|
+
continue;
|
|
4168
|
+
}
|
|
4169
|
+
if (result.approved) {
|
|
4170
|
+
ctx.ui.notify("Plannotator review approved. Choose next action.", "info");
|
|
4171
|
+
continue;
|
|
4172
|
+
}
|
|
4173
|
+
const feedback = result.feedback?.trim();
|
|
4174
|
+
if (feedback) {
|
|
4175
|
+
setStep(orchestrator, "llm_work");
|
|
4176
|
+
return advanceBanner(
|
|
4177
|
+
"[PI-PI] The user reviewed this phase's artifacts in Plannotator and left the following feedback. " +
|
|
4178
|
+
"Address each point, updating USER_REQUEST.md, RESEARCH.md, and artifacts/*.md as needed:\n\n" +
|
|
4179
|
+
feedback,
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
continue;
|
|
4183
|
+
}
|
|
3659
4184
|
if (phase === "plan") {
|
|
3660
4185
|
finalizeReviewCycle(task);
|
|
3661
4186
|
const text = await enterReviewCycle(orchestrator, ctx, "plannotator");
|
|
@@ -3666,118 +4191,75 @@ export async function showActiveTaskMenu(
|
|
|
3666
4191
|
return handled.text ?? text;
|
|
3667
4192
|
}
|
|
3668
4193
|
const allRepos = getRegisteredRepos(orchestrator);
|
|
3669
|
-
const
|
|
3670
|
-
const repos: RepoInfo[] = [];
|
|
4194
|
+
const eligible: string[] = [];
|
|
3671
4195
|
for (const repo of allRepos) {
|
|
3672
4196
|
const base = await detectDefaultBranch(orchestrator, allRepos, repo.path);
|
|
3673
4197
|
const { changed, error } = await repoHasReviewableChanges(orchestrator, repo, base);
|
|
3674
|
-
if (error)
|
|
3675
|
-
summaries.push(`${formatRepoLabel(repo)}: ERROR — ${error}`);
|
|
3676
|
-
repos.push(repo);
|
|
3677
|
-
} else if (changed) {
|
|
3678
|
-
repos.push(repo);
|
|
3679
|
-
}
|
|
4198
|
+
if (error || changed) eligible.push(repo.path);
|
|
3680
4199
|
}
|
|
3681
|
-
if (
|
|
4200
|
+
if (eligible.length === 0) {
|
|
3682
4201
|
ctx.ui.notify("No registered repositories have changes to review.", "info");
|
|
3683
4202
|
continue;
|
|
3684
4203
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
opt("Last commit", "Changes in the most recent commit"),
|
|
3694
|
-
opt("Since commit", "Review all changes since a specific commit"),
|
|
3695
|
-
opt("Uncommitted changes", "Working directory changes"),
|
|
3696
|
-
opt("Skip this repo", "Move to the next repository"),
|
|
3697
|
-
opt("Done (stop reviewing)", "Stop iterating repositories"),
|
|
3698
|
-
]);
|
|
3699
|
-
|
|
3700
|
-
if (!diffChoice || diffChoice === "Done (stop reviewing)") {
|
|
3701
|
-
stopReviewing = true;
|
|
3702
|
-
break;
|
|
3703
|
-
}
|
|
3704
|
-
if (diffChoice === "Skip this repo") {
|
|
3705
|
-
summaries.push(`${formatRepoLabel(repo)}: SKIPPED`);
|
|
3706
|
-
break;
|
|
3707
|
-
}
|
|
3708
|
-
|
|
3709
|
-
let diffType: string | undefined;
|
|
3710
|
-
let defaultBranch: string | undefined;
|
|
3711
|
-
|
|
3712
|
-
if (diffChoice === "All branch changes") {
|
|
3713
|
-
diffType = "branch";
|
|
3714
|
-
defaultBranch = await detectDefaultBranch(orchestrator, repos, repo.path);
|
|
3715
|
-
} else if (diffChoice === "Last commit") {
|
|
3716
|
-
diffType = "last-commit";
|
|
3717
|
-
} else if (diffChoice === "Since commit") {
|
|
3718
|
-
const pickedHash = await pickCommitForRepo(orchestrator, ctx, repo);
|
|
3719
|
-
if (!pickedHash) continue;
|
|
3720
|
-
diffType = "branch";
|
|
3721
|
-
defaultBranch = pickedHash;
|
|
3722
|
-
} else {
|
|
3723
|
-
diffType = "uncommitted";
|
|
3724
|
-
}
|
|
3725
|
-
|
|
3726
|
-
const result = await openCodeReviewInPlannotator(orchestrator, {
|
|
3727
|
-
cwd: repo.path,
|
|
3728
|
-
diffType,
|
|
3729
|
-
defaultBranch,
|
|
3730
|
-
});
|
|
3731
|
-
if (result.status === "error") {
|
|
3732
|
-
summaries.push(`${formatRepoLabel(repo)}: ERROR${result.error ? ` — ${result.error}` : ""}`);
|
|
3733
|
-
} else if (result.status === "approved") {
|
|
3734
|
-
summaries.push(`${formatRepoLabel(repo)}: APPROVED`);
|
|
3735
|
-
} else {
|
|
3736
|
-
summaries.push(`${formatRepoLabel(repo)}: NEEDS_CHANGES${result.feedback ? `\nFeedback: ${result.feedback}` : ""}`);
|
|
3737
|
-
}
|
|
3738
|
-
break;
|
|
3739
|
-
}
|
|
3740
|
-
}
|
|
3741
|
-
|
|
3742
|
-
if (summaries.length > 0) {
|
|
3743
|
-
ctx.ui.notify(`Plannotator review summary:\n\n${summaries.join("\n\n")}`, "info");
|
|
3744
|
-
} else {
|
|
3745
|
-
ctx.ui.notify("No repositories were reviewed in Plannotator.", "info");
|
|
3746
|
-
}
|
|
4204
|
+
// Start (or restart) the interleaved per-repo cursor and run it. On
|
|
4205
|
+
// needs_changes runPlannotatorCursor persists the cursor and returns a
|
|
4206
|
+
// work instruction (exits the menu to start the agent turn); the next /pp
|
|
4207
|
+
// resumes at the following repo.
|
|
4208
|
+
task.state.plannotatorCursor = { repoPaths: eligible, index: 0 };
|
|
4209
|
+
saveTask(task.dir, task.state);
|
|
4210
|
+
const cursorText = await runPlannotatorCursor(orchestrator, ctx);
|
|
4211
|
+
if (cursorText) return cursorText;
|
|
3747
4212
|
continue;
|
|
3748
4213
|
}
|
|
3749
4214
|
|
|
3750
4215
|
if (reviewChoice === "Review on my own") {
|
|
3751
|
-
if (phase === "plan") {
|
|
3752
|
-
setStep(orchestrator, "synthesize");
|
|
3753
|
-
return continueMessage;
|
|
3754
|
-
}
|
|
3755
|
-
if (phase !== "implement") {
|
|
3756
|
-
setStep(orchestrator, "llm_work");
|
|
3757
|
-
return continueMessage;
|
|
3758
|
-
}
|
|
3759
|
-
|
|
3760
4216
|
const gate = await selectOption(ctx, "Editor review", [
|
|
3761
4217
|
opt("Done", "I've added AI_REVIEW: markers and saved my files"),
|
|
3762
4218
|
opt("Skip markers", "Continue without the marker workflow"),
|
|
3763
4219
|
opt("Back", "Return to the review menu"),
|
|
3764
4220
|
]);
|
|
3765
4221
|
if (!gate || gate === "Back") continue;
|
|
3766
|
-
setStep(orchestrator, "llm_work");
|
|
4222
|
+
setStep(orchestrator, phase === "plan" ? "synthesize" : "llm_work");
|
|
3767
4223
|
if (gate === "Skip markers") {
|
|
3768
4224
|
return continueMessage;
|
|
3769
4225
|
}
|
|
4226
|
+
if (phase === "implement") {
|
|
4227
|
+
return advanceBanner(
|
|
4228
|
+
"[PI-PI] The user reviewed the changes in their editor and left inline `AI_REVIEW:` markers " +
|
|
4229
|
+
`${AI_REVIEW_MARKER_SYNTAX}.\n\n` +
|
|
4230
|
+
"For each registered repo, enumerate the CHANGED files only — union of `git diff --name-only <base>...HEAD`, " +
|
|
4231
|
+
"`git diff --name-only`, `git diff --cached --name-only`, and untracked-non-ignored files from `git status --porcelain` " +
|
|
4232
|
+
"(base = each repo's configured base branch) — and search WITHIN those files for `AI_REVIEW:`. Do NOT grep the whole " +
|
|
4233
|
+
"worktree (avoid vendored/generated/node_modules and historical markers).\n\n" +
|
|
4234
|
+
AI_REVIEW_MARKER_LOOP,
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
return readOnlyReviewBanner(phase, task.dir);
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
if (reviewChoice === "Auto review until approved") {
|
|
4241
|
+
const loopPreset = await pickPreset(ctx, orchestrator, getReviewPresetGroup(phase), "Review preset");
|
|
4242
|
+
if (!loopPreset) continue;
|
|
4243
|
+
if (!hasEnabledReviewers(orchestrator, loopPreset)) {
|
|
4244
|
+
const rg = reviewPresetGroupForPhase(phase);
|
|
4245
|
+
const label = rg === "brainstormReviewers" ? "artifact" : rg === "planReviewers" ? "plan" : "code";
|
|
4246
|
+
ctx.ui.notify(`No ${label} reviewers enabled.`, "info");
|
|
4247
|
+
continue;
|
|
4248
|
+
}
|
|
4249
|
+
const passes = await pickMaxReviewPasses(ctx, 3);
|
|
4250
|
+
if (passes === null) continue;
|
|
4251
|
+
finalizeReviewCycle(task);
|
|
4252
|
+
// Item 5: stop-in-phase loop. Reset the per-phase auto-pass counter so the
|
|
4253
|
+
// cap counts THIS run's passes, and clear any stale clean flag.
|
|
4254
|
+
if (task.state.reviewPassByKind?.[phase]) task.state.reviewPassByKind[phase].auto = 0;
|
|
4255
|
+
task.state.reviewApprovedClean = false;
|
|
4256
|
+
task.state.manualAutoReview = { phase, preset: loopPreset, maxPasses: passes, advanceOnComplete: false };
|
|
4257
|
+
saveTask(task.dir, task.state);
|
|
3770
4258
|
return advanceBanner(
|
|
3771
|
-
|
|
3772
|
-
"
|
|
3773
|
-
"
|
|
3774
|
-
"
|
|
3775
|
-
"`git diff --name-only`, `git diff --cached --name-only`, and untracked-non-ignored files from `git status --porcelain` " +
|
|
3776
|
-
"(base = each repo's configured base branch) — and search WITHIN those files for `AI_REVIEW:`. Do NOT grep the whole " +
|
|
3777
|
-
"worktree (avoid vendored/generated/node_modules and historical markers).\n\n" +
|
|
3778
|
-
"For each marker: address the request, then remove that marker in the SAME edit. After a pass, re-scan the changed " +
|
|
3779
|
-
"files and repeat until no `AI_REVIEW:` markers remain. Then verify your work and report what you changed per marker. " +
|
|
3780
|
-
"When complete, call pp_phase_complete.",
|
|
4259
|
+
`[PI-PI] Auto-review-until-approved started over ${reviewTarget} (up to ${passes >= 999 ? "unlimited" : passes} passes). ` +
|
|
4260
|
+
"Call pp_phase_complete now to run the first review pass; after each pass, apply the reviewers' feedback and call pp_phase_complete again. " +
|
|
4261
|
+
"Do NOT pause to ask for design or user approval while this loop runs — it supersedes the pre-finalization approval rule. " +
|
|
4262
|
+
"The loop stops when reviewers unanimously approve or the pass cap is reached. Do NOT advance the phase yourself.",
|
|
3781
4263
|
);
|
|
3782
4264
|
}
|
|
3783
4265
|
|
|
@@ -3785,7 +4267,8 @@ export async function showActiveTaskMenu(
|
|
|
3785
4267
|
if (!reviewPreset) continue;
|
|
3786
4268
|
finalizeReviewCycle(task);
|
|
3787
4269
|
if (!hasEnabledReviewers(orchestrator, reviewPreset)) {
|
|
3788
|
-
const
|
|
4270
|
+
const reviewGroup = reviewPresetGroupForPhase(phase);
|
|
4271
|
+
const label = reviewGroup === "brainstormReviewers" ? "artifact" : reviewGroup === "planReviewers" ? "plan" : "code";
|
|
3789
4272
|
ctx.ui.notify(`No ${label} reviewers enabled.`, "info");
|
|
3790
4273
|
continue;
|
|
3791
4274
|
}
|
|
@@ -3795,11 +4278,15 @@ export async function showActiveTaskMenu(
|
|
|
3795
4278
|
const handled = handleReviewResult(ctx, text);
|
|
3796
4279
|
if (handled.continueLoop) continue;
|
|
3797
4280
|
return handled.text ?? text;
|
|
4281
|
+
}
|
|
3798
4282
|
}
|
|
3799
4283
|
}
|
|
3800
4284
|
}
|
|
3801
4285
|
|
|
3802
4286
|
export async function showPpMenu(orchestrator: Orchestrator, ctx: any, mode: MenuMode = "command"): Promise<string | undefined> {
|
|
4287
|
+
if (orchestrator.configError) {
|
|
4288
|
+
return showConfigErrorMenu(orchestrator, ctx);
|
|
4289
|
+
}
|
|
3803
4290
|
if (!orchestrator.active) {
|
|
3804
4291
|
return showNoActiveMenu(orchestrator, ctx);
|
|
3805
4292
|
}
|