@ilya-lesikov/pi-pi 0.7.0 → 0.9.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-plannotator/apps/pi-extension/README.md +6 -5
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
- package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
- package/3p/pi-subagents/src/agent-manager.ts +34 -1
- package/3p/pi-subagents/src/agent-runner.ts +66 -33
- package/3p/pi-subagents/src/index.ts +3 -38
- package/3p/pi-subagents/src/types.ts +4 -0
- package/extensions/orchestrator/agents/advisor.ts +35 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/code-reviewer.ts +27 -11
- package/extensions/orchestrator/agents/constraints.test.ts +82 -0
- package/extensions/orchestrator/agents/constraints.ts +66 -2
- package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/planner.ts +9 -8
- package/extensions/orchestrator/agents/prompts.test.ts +120 -0
- package/extensions/orchestrator/agents/reviewer.ts +48 -0
- package/extensions/orchestrator/agents/task.ts +6 -20
- package/extensions/orchestrator/agents/tool-routing.ts +39 -1
- package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
- package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
- package/extensions/orchestrator/command-handlers.test.ts +47 -0
- package/extensions/orchestrator/command-handlers.ts +44 -28
- package/extensions/orchestrator/config.test.ts +40 -1
- package/extensions/orchestrator/config.ts +18 -2
- package/extensions/orchestrator/context.test.ts +54 -0
- package/extensions/orchestrator/context.ts +81 -2
- package/extensions/orchestrator/custom-footer.test.ts +91 -0
- package/extensions/orchestrator/custom-footer.ts +20 -20
- package/extensions/orchestrator/event-handlers.test.ts +412 -2
- package/extensions/orchestrator/event-handlers.ts +556 -227
- package/extensions/orchestrator/flant-infra.test.ts +25 -0
- package/extensions/orchestrator/flant-infra.ts +91 -0
- package/extensions/orchestrator/index.ts +1 -1
- package/extensions/orchestrator/integration.test.ts +411 -20
- package/extensions/orchestrator/messages.test.ts +30 -0
- package/extensions/orchestrator/messages.ts +6 -0
- package/extensions/orchestrator/model-registry.test.ts +48 -1
- package/extensions/orchestrator/model-registry.ts +43 -1
- package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/orchestrator/orchestrator.test.ts +113 -0
- package/extensions/orchestrator/orchestrator.ts +197 -60
- package/extensions/orchestrator/phases/brainstorm.ts +20 -27
- package/extensions/orchestrator/phases/implementation.test.ts +11 -0
- package/extensions/orchestrator/phases/implementation.ts +4 -6
- package/extensions/orchestrator/phases/machine.test.ts +36 -0
- package/extensions/orchestrator/phases/machine.ts +11 -1
- package/extensions/orchestrator/phases/planning.test.ts +16 -0
- package/extensions/orchestrator/phases/planning.ts +11 -4
- package/extensions/orchestrator/phases/review-task.test.ts +20 -0
- package/extensions/orchestrator/phases/review-task.ts +47 -22
- package/extensions/orchestrator/phases/review.test.ts +34 -0
- package/extensions/orchestrator/phases/review.ts +44 -6
- package/extensions/orchestrator/plannotator.ts +9 -6
- package/extensions/orchestrator/pp-menu.test.ts +207 -1
- package/extensions/orchestrator/pp-menu.ts +514 -402
- package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
- package/extensions/orchestrator/pp-state-tools.ts +249 -0
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
- package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
- package/extensions/orchestrator/state.test.ts +9 -0
- package/extensions/orchestrator/state.ts +15 -0
- package/extensions/orchestrator/test-helpers.ts +4 -1
- package/extensions/orchestrator/transition-controller.test.ts +100 -0
- package/extensions/orchestrator/transition-controller.ts +61 -3
- package/extensions/orchestrator/usage-tracker.ts +5 -1
- package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
- package/extensions/orchestrator/validate-artifacts.ts +2 -2
- package/package.json +1 -1
- package/AGENTS.md +0 -28
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } 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";
|
|
@@ -24,10 +24,12 @@ import {
|
|
|
24
24
|
loadBrainstormReviewOutputs,
|
|
25
25
|
loadCodeReviewOutputs,
|
|
26
26
|
loadPlanReviewOutputs,
|
|
27
|
+
hasFinalPassAnchors,
|
|
27
28
|
} from "./context.js";
|
|
28
|
-
import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle } from "./event-handlers.js";
|
|
29
|
+
import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle, isReviewCycleLive } from "./event-handlers.js";
|
|
29
30
|
import { Orchestrator } from "./orchestrator.js";
|
|
30
|
-
import { cancelPendingPlannotatorWait } from "./plannotator.js";
|
|
31
|
+
import { cancelPendingPlannotatorWait, openPlannotator, waitForPlannotatorResult } from "./plannotator.js";
|
|
32
|
+
import { advanceBanner } from "./messages.js";
|
|
31
33
|
import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
|
|
32
34
|
import { spawnCodeReviewers } from "./phases/review.js";
|
|
33
35
|
import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
@@ -84,6 +86,7 @@ type TimeoutKey =
|
|
|
84
86
|
| "performance.commands.afterEdit"
|
|
85
87
|
| "performance.commands.afterImplement"
|
|
86
88
|
| "performance.internals.subagentStale"
|
|
89
|
+
| "performance.internals.mainTurnStale"
|
|
87
90
|
| "performance.internals.taskLockStale"
|
|
88
91
|
| "performance.internals.taskLockRefresh";
|
|
89
92
|
|
|
@@ -104,16 +107,22 @@ async function selectOptionCancelable(
|
|
|
104
107
|
question: string,
|
|
105
108
|
options: OptionInput[],
|
|
106
109
|
): Promise<{ choice?: string; cancelReason?: CancelReason }> {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
const orchestrator = Orchestrator.current;
|
|
111
|
+
if (orchestrator) orchestrator.interactivePromptOpen = true;
|
|
112
|
+
try {
|
|
113
|
+
const result = await askUser(ctx, {
|
|
114
|
+
question,
|
|
115
|
+
options,
|
|
116
|
+
allowFreeform: false,
|
|
117
|
+
allowComment: false,
|
|
118
|
+
allowMultiple: false,
|
|
119
|
+
});
|
|
120
|
+
if (result && isCancel(result)) return { cancelReason: result.reason };
|
|
121
|
+
if (!result || result.kind !== "selection") return {};
|
|
122
|
+
return { choice: result.selections[0] };
|
|
123
|
+
} finally {
|
|
124
|
+
if (orchestrator) orchestrator.interactivePromptOpen = false;
|
|
125
|
+
}
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
function opt(title: string, description: string): OptionInput {
|
|
@@ -150,51 +159,7 @@ function formatRepoList(repos: RepoInfo[]): string {
|
|
|
150
159
|
.join("\n");
|
|
151
160
|
}
|
|
152
161
|
|
|
153
|
-
function appendSection(content: string, heading: string, body: string): string {
|
|
154
|
-
const normalized = content.trimEnd();
|
|
155
|
-
if (normalized.includes(`${heading}\n`)) return normalized + "\n";
|
|
156
|
-
return `${normalized}\n\n${heading}\n${body}\n`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function appendToSection(content: string, heading: string, body: string): string {
|
|
160
|
-
const normalized = content.trimEnd();
|
|
161
|
-
if (normalized.includes(body)) return normalized + "\n";
|
|
162
|
-
|
|
163
|
-
const lines = normalized.split("\n");
|
|
164
|
-
const sectionIndex = lines.findIndex((line) => line.trim() === heading);
|
|
165
|
-
if (sectionIndex === -1) {
|
|
166
|
-
return appendSection(normalized, heading, body);
|
|
167
|
-
}
|
|
168
162
|
|
|
169
|
-
let insertIndex = lines.length;
|
|
170
|
-
for (let i = sectionIndex + 1; i < lines.length; i += 1) {
|
|
171
|
-
if (lines[i]?.startsWith("## ")) {
|
|
172
|
-
insertIndex = i;
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const insertLines = body.split("\n");
|
|
178
|
-
if (insertIndex > sectionIndex + 1 && lines[insertIndex - 1]?.trim() !== "") {
|
|
179
|
-
insertLines.unshift("");
|
|
180
|
-
}
|
|
181
|
-
lines.splice(insertIndex, 0, ...insertLines);
|
|
182
|
-
return lines.join("\n") + "\n";
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function appendRepoContext(content: string, repos: RepoInfo[]): string {
|
|
186
|
-
if (repos.length === 0) return content;
|
|
187
|
-
const lines = repos.map((repo) => `- ${formatRepoLabel(repo)}${repo.baseBranch ? ` (base: ${repo.baseBranch})` : ""}`);
|
|
188
|
-
return appendToSection(content, "## Constraints", `Registered repositories:\n${lines.join("\n")}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function appendResearchOpenQuestions(content: string, text: string): string {
|
|
192
|
-
const normalized = content.trimEnd();
|
|
193
|
-
if (normalized.includes("## Open Questions\n")) {
|
|
194
|
-
return `${normalized}\n${text}\n`;
|
|
195
|
-
}
|
|
196
|
-
return `${normalized}\n\n## Open Questions\n${text}\n`;
|
|
197
|
-
}
|
|
198
163
|
|
|
199
164
|
async function pickCommitForRepo(orchestrator: Orchestrator, ctx: any, repo: RepoInfo): Promise<string | null> {
|
|
200
165
|
let commits: Array<{ hash: string; message: string; age: string }> = [];
|
|
@@ -225,6 +190,44 @@ async function pickCommitForRepo(orchestrator: Orchestrator, ctx: any, repo: Rep
|
|
|
225
190
|
return pickedHash || null;
|
|
226
191
|
}
|
|
227
192
|
|
|
193
|
+
async function repoHasReviewableChanges(
|
|
194
|
+
orchestrator: Orchestrator,
|
|
195
|
+
repo: RepoInfo,
|
|
196
|
+
base: string,
|
|
197
|
+
): Promise<{ changed: boolean; error?: string }> {
|
|
198
|
+
const run = async (args: string[]): Promise<{ out: string; failed: boolean }> => {
|
|
199
|
+
try {
|
|
200
|
+
const res = await orchestrator.pi.exec("git", args, { cwd: repo.path, timeout: 5000 });
|
|
201
|
+
if (res.code !== 0) return { out: "", failed: true };
|
|
202
|
+
return { out: res.stdout, failed: false };
|
|
203
|
+
} catch {
|
|
204
|
+
return { out: "", failed: true };
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const committed = await run(["diff", "--name-only", `${base}...HEAD`]);
|
|
209
|
+
const unstaged = await run(["diff", "--name-only"]);
|
|
210
|
+
const staged = await run(["diff", "--cached", "--name-only"]);
|
|
211
|
+
const status = await run(["status", "--porcelain"]);
|
|
212
|
+
|
|
213
|
+
const hasCommitted = committed.out.trim().length > 0;
|
|
214
|
+
const hasUnstaged = unstaged.out.trim().length > 0;
|
|
215
|
+
const hasStaged = staged.out.trim().length > 0;
|
|
216
|
+
const hasUntracked = status.out.split("\n").some((line) => line.startsWith("??"));
|
|
217
|
+
const changed = hasCommitted || hasUnstaged || hasStaged || hasUntracked;
|
|
218
|
+
|
|
219
|
+
if (changed) return { changed: true };
|
|
220
|
+
|
|
221
|
+
// No change detected — but a failed probe (e.g. a missing/invalid base ref)
|
|
222
|
+
// could be masking real changes, so surface it rather than silently hiding
|
|
223
|
+
// the repo.
|
|
224
|
+
if (committed.failed || unstaged.failed || staged.failed || status.failed) {
|
|
225
|
+
return { changed: false, error: "git status/diff failed" };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { changed: false };
|
|
229
|
+
}
|
|
230
|
+
|
|
228
231
|
interface RepoPrContext {
|
|
229
232
|
repoPath: string;
|
|
230
233
|
prUrl: string | null;
|
|
@@ -237,6 +240,84 @@ function setStep(orchestrator: Orchestrator, step: string): void {
|
|
|
237
240
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
238
241
|
}
|
|
239
242
|
|
|
243
|
+
// Publish the latest synthesized review findings to a target. The agent (not the
|
|
244
|
+
// extension) performs the writes: file comments = insert `AI_COMMENT:` markers at
|
|
245
|
+
// each finding's line; PR comments = run `gh` to post line-anchored comments from
|
|
246
|
+
// the user's own account. Both are idempotent — the agent checks for markers/
|
|
247
|
+
// comments it already published and skips duplicates, so re-publishing is safe.
|
|
248
|
+
const PRIVACY_INSTRUCTION =
|
|
249
|
+
"PRIVACY: comment bodies MUST be self-contained observations about the code itself. Do NOT reference " +
|
|
250
|
+
"private or internal details, \"the ticket\", issue trackers, or internal design docs. Say what is wrong " +
|
|
251
|
+
"in the code, not that it \"violates the design goal\" of some private document.";
|
|
252
|
+
|
|
253
|
+
export function publishFileCommentsBanner(taskDir: string): string {
|
|
254
|
+
const reviewsDir = join(taskDir, "code-reviews");
|
|
255
|
+
return advanceBanner(
|
|
256
|
+
"[PI-PI] Publish the synthesized review findings as FILE COMMENTS now.\n\n" +
|
|
257
|
+
`Use the \`ANCHORS:\` block in the latest \`${reviewsDir}/*_final_pass-*.md\` as the file:line source — do NOT invent locations.\n\n` +
|
|
258
|
+
"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. " +
|
|
259
|
+
"This is the ONLY source edit permitted — no fixes, no other changes.\n\n" +
|
|
260
|
+
PRIVACY_INSTRUCTION + "\n\n" +
|
|
261
|
+
"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" +
|
|
262
|
+
"When done, report how many markers you inserted and how many you skipped as already-present, then end your turn.",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function publishPrCommentsBanner(taskDir: string): string {
|
|
267
|
+
const reviewsDir = join(taskDir, "code-reviews");
|
|
268
|
+
return advanceBanner(
|
|
269
|
+
"[PI-PI] Publish the synthesized review findings as GITHUB PR COMMENTS now, from the user's own `gh`-authenticated account.\n\n" +
|
|
270
|
+
`Use the \`ANCHORS:\` block in the latest \`${reviewsDir}/*_final_pass-*.md\` as the file:line source — do NOT invent locations.\n\n` +
|
|
271
|
+
"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" +
|
|
272
|
+
"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" +
|
|
273
|
+
"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" +
|
|
274
|
+
"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" +
|
|
275
|
+
"The LAST line of every finding's comment body MUST be exactly:\n(generated by pi-pi)\n\n" +
|
|
276
|
+
PRIVACY_INSTRUCTION + "\n\n" +
|
|
277
|
+
"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" +
|
|
278
|
+
"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" +
|
|
279
|
+
"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.",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Guard for the Publish menu: both banners consume the `ANCHORS:` block from the
|
|
284
|
+
// latest `code-reviews/*_final_pass-*.md`. If no such file exists, or the newest one
|
|
285
|
+
// carries no `ANCHORS:` block, publishing would spawn an agent that immediately fails.
|
|
286
|
+
// Returns a user-facing message to show instead, or undefined when publishing can proceed.
|
|
287
|
+
export const MISSING_FINAL_PASS_ANCHORS =
|
|
288
|
+
"No ANCHORS-bearing final review file exists yet. Run or finish a review pass first " +
|
|
289
|
+
"(/pp → Review) so the findings are synthesized into `code-reviews/*_final_pass-*.md`.";
|
|
290
|
+
|
|
291
|
+
export function publishGuard(taskDir: string): string | undefined {
|
|
292
|
+
return hasFinalPassAnchors(taskDir) ? undefined : MISSING_FINAL_PASS_ANCHORS;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const AI_REVIEW_MARKER_SYNTAX =
|
|
296
|
+
"(inside each file's native comment syntax, e.g. `// AI_REVIEW: ...`, `# AI_REVIEW: ...`, `<!-- AI_REVIEW: ... -->`)";
|
|
297
|
+
|
|
298
|
+
const AI_REVIEW_MARKER_LOOP =
|
|
299
|
+
"For each marker: address the request, then remove that marker in the SAME edit. After a pass, re-scan the target files and " +
|
|
300
|
+
"repeat until no `AI_REVIEW:` markers remain. Then verify your work and report what you changed per marker. When complete, " +
|
|
301
|
+
"call pp_phase_complete.";
|
|
302
|
+
|
|
303
|
+
// Read-only phases (brainstorm/debug/review) and plan produce markdown state files rather than
|
|
304
|
+
// source changes, so their AI_REVIEW scan targets those artifacts. Missing targets are skipped,
|
|
305
|
+
// not errors. Kept as literal-token guidance (no regexp/parser), mirroring the implement banner.
|
|
306
|
+
function readOnlyReviewBanner(phase: string, taskDir: string): string {
|
|
307
|
+
const targets =
|
|
308
|
+
phase === "plan"
|
|
309
|
+
? "the synthesized plan(s) at `" + taskDir + "/plans/*_synthesized.md` ONLY (ignore raw planner outputs, " +
|
|
310
|
+
"`review_*` files, and the `plan-reviews/`, `brainstorm-reviews/`, `code-reviews/` directories)"
|
|
311
|
+
: "`" + taskDir + "/USER_REQUEST.md`, `" + taskDir + "/RESEARCH.md`, and `" + taskDir + "/artifacts/*.md`";
|
|
312
|
+
return advanceBanner(
|
|
313
|
+
`[PI-PI] The user reviewed this phase's state files in their editor and left inline \`AI_REVIEW:\` markers ` +
|
|
314
|
+
`${AI_REVIEW_MARKER_SYNTAX}.\n\n` +
|
|
315
|
+
`Search WITHIN ${targets} for \`AI_REVIEW:\`. Only scan those files — skip any target that does not exist ` +
|
|
316
|
+
`(a missing \`artifacts/*.md\` or a not-yet-produced state file is fine, not an error).\n\n` +
|
|
317
|
+
AI_REVIEW_MARKER_LOOP,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
240
321
|
function showStatus(orchestrator: Orchestrator, ctx: any): void {
|
|
241
322
|
if (!orchestrator.active) {
|
|
242
323
|
ctx.ui.notify("No active task.", "info");
|
|
@@ -252,6 +333,11 @@ function showStatus(orchestrator: Orchestrator, ctx: any): void {
|
|
|
252
333
|
}
|
|
253
334
|
|
|
254
335
|
async function abortCurrentWork(orchestrator: Orchestrator, ctx: any): Promise<void> {
|
|
336
|
+
// Clear pi-pi's own delayed post-error retry (timer + ESC interrupt). Unlike
|
|
337
|
+
// pause/finish, abortCurrentWork does NOT go through cleanupActive/
|
|
338
|
+
// resetTaskScopedState, so without this a scheduled retry would survive the
|
|
339
|
+
// abort and re-nudge seconds later.
|
|
340
|
+
orchestrator.cancelPendingRetry();
|
|
255
341
|
orchestrator.abortAllSubagents();
|
|
256
342
|
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
257
343
|
await ctx.waitForIdle?.();
|
|
@@ -272,6 +358,9 @@ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
272
358
|
const type = orchestrator.active.type;
|
|
273
359
|
|
|
274
360
|
orchestrator.active.state.reviewCycle = null;
|
|
361
|
+
// Drop any in-progress per-repo Plannotator cursor so a resumed task does not
|
|
362
|
+
// auto-reopen Plannotator; the user re-enters Review explicitly.
|
|
363
|
+
orchestrator.active.state.plannotatorCursor = undefined;
|
|
275
364
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
276
365
|
unregisterAgentDefinitions(orchestrator.pi);
|
|
277
366
|
await orchestrator.cleanupActive();
|
|
@@ -296,6 +385,13 @@ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
296
385
|
async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string> {
|
|
297
386
|
if (!orchestrator.active) return "No active task.";
|
|
298
387
|
|
|
388
|
+
// The review phase must produce the ANCHORS-bearing final_pass file Publish
|
|
389
|
+
// consumes; block the direct Complete path too (it bypasses validateExitCriteria).
|
|
390
|
+
if (orchestrator.active.state.phase === "review" && !hasFinalPassAnchors(orchestrator.active.dir)) {
|
|
391
|
+
ctx.ui.notify(MISSING_FINAL_PASS_ANCHORS, "warning");
|
|
392
|
+
return MISSING_FINAL_PASS_ANCHORS;
|
|
393
|
+
}
|
|
394
|
+
|
|
299
395
|
cancelPendingPlannotatorWait(orchestrator);
|
|
300
396
|
orchestrator.abortAllSubagents();
|
|
301
397
|
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
@@ -322,7 +418,8 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
322
418
|
// is over, nothing awaits this compaction).
|
|
323
419
|
void orchestrator.transitionController.requestTransition({
|
|
324
420
|
kind: "done",
|
|
325
|
-
|
|
421
|
+
discard: true,
|
|
422
|
+
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.`,
|
|
326
423
|
});
|
|
327
424
|
|
|
328
425
|
const urExists = existsSync(join(dir, "USER_REQUEST.md"));
|
|
@@ -795,6 +892,9 @@ function collectRoleAssignments(config: Partial<PiPiConfig> | null): string[] {
|
|
|
795
892
|
add("agents.subagents.simple.explore", config.agents?.subagents?.simple?.explore?.model);
|
|
796
893
|
add("agents.subagents.simple.librarian", config.agents?.subagents?.simple?.librarian?.model);
|
|
797
894
|
add("agents.subagents.simple.task", config.agents?.subagents?.simple?.task?.model);
|
|
895
|
+
add("agents.subagents.simple.advisor", config.agents?.subagents?.simple?.advisor?.model);
|
|
896
|
+
add("agents.subagents.simple.deep-debugger", config.agents?.subagents?.simple?.["deep-debugger"]?.model);
|
|
897
|
+
add("agents.subagents.simple.reviewer", config.agents?.subagents?.simple?.reviewer?.model);
|
|
798
898
|
return out;
|
|
799
899
|
}
|
|
800
900
|
|
|
@@ -814,6 +914,7 @@ function flantStatusText(settings: FlantSettings): string {
|
|
|
814
914
|
lines.push(
|
|
815
915
|
`Personal subscription: on (${subActive ? `active — pp-flant-anthropic-sub, ${providers.anthropic} models` : "inactive"})`,
|
|
816
916
|
);
|
|
917
|
+
lines.push(`Rate-limit switch-back check: every ${settings.switchBackIntervalMinutes} min`);
|
|
817
918
|
if (!subActive) {
|
|
818
919
|
if (!hasOAuth) lines.push(" - missing Claude OAuth token (run pi /login for Anthropic)");
|
|
819
920
|
if (!hasGatewayKey) lines.push(" - missing gateway key (set LLM_API_KEY or FLANT_API_KEY)");
|
|
@@ -849,15 +950,22 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
849
950
|
while (true) {
|
|
850
951
|
const settings = loadFlantSettings();
|
|
851
952
|
const enableLabel = `Enable: ${settings.enabled ? "ON" : "OFF"}`;
|
|
852
|
-
const options: OptionInput[] = [
|
|
953
|
+
const options: OptionInput[] = [
|
|
954
|
+
{ title: enableLabel, description: "Turn the Flant AI model providers on or off" },
|
|
955
|
+
];
|
|
853
956
|
const subscriptionLabel = `Personal Claude subscription: ${settings.subscription ? "ON" : "OFF"}`;
|
|
854
957
|
if (settings.enabled) {
|
|
855
958
|
options.push(
|
|
856
|
-
|
|
857
|
-
`
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
959
|
+
{ title: subscriptionLabel, description: "Route Claude roles through your personal Claude subscription instead of the gateway" },
|
|
960
|
+
{ title: `Auto-update on startup: ${settings.autoUpdate ? "ON" : "OFF"}`, description: "Refresh the available model list automatically each time pi starts" },
|
|
961
|
+
{ title: `Cache period: ${settings.cacheTTLDays} ${settings.cacheTTLDays === 1 ? "day" : "days"}`, description: "How long the fetched model list is reused before it is refreshed" },
|
|
962
|
+
);
|
|
963
|
+
if (settings.subscription) {
|
|
964
|
+
options.push({ title: `Rate-limit switch-back check: every ${settings.switchBackIntervalMinutes} min`, description: "How often to retry your subscription after it was rate-limited and traffic fell back to the gateway" });
|
|
965
|
+
}
|
|
966
|
+
options.push(
|
|
967
|
+
{ title: "Update now", description: "Fetch the latest model list from Flant right away" },
|
|
968
|
+
{ title: "Current status", description: "Show the current Flant configuration, providers, and model counts" },
|
|
861
969
|
);
|
|
862
970
|
}
|
|
863
971
|
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
@@ -920,6 +1028,22 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
920
1028
|
continue;
|
|
921
1029
|
}
|
|
922
1030
|
|
|
1031
|
+
if (choice.startsWith("Rate-limit switch-back check:")) {
|
|
1032
|
+
const selected = await selectOption(ctx, "Switch-back check interval", [
|
|
1033
|
+
{ title: "15 min", description: "Probe the subscription limit every 15 minutes" },
|
|
1034
|
+
{ title: "30 min", description: "Default — probe every 30 minutes" },
|
|
1035
|
+
{ title: "60 min", description: "Probe hourly" },
|
|
1036
|
+
{ title: "120 min", description: "Probe every two hours" },
|
|
1037
|
+
{ title: "Back", description: "Return to the previous menu" },
|
|
1038
|
+
]);
|
|
1039
|
+
if (!selected || selected === "Back") continue;
|
|
1040
|
+
const mins = Number(selected.split(" ")[0]);
|
|
1041
|
+
if (!Number.isFinite(mins) || mins <= 0) continue;
|
|
1042
|
+
saveFlantSettings({ ...settings, switchBackIntervalMinutes: mins });
|
|
1043
|
+
ctx.ui.notify(`Switch-back check interval set to ${mins} min.`, "info");
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
923
1047
|
if (choice.startsWith("Cache period:")) {
|
|
924
1048
|
const selected = await selectOption(ctx, "Cache period", [
|
|
925
1049
|
{ title: "1 day", description: "Refresh model metadata daily" },
|
|
@@ -1199,6 +1323,9 @@ const SUBAGENT_ROLES: Array<{ role: AgentRole; label: string; description: strin
|
|
|
1199
1323
|
{ role: "explore", label: "Explore", description: "agents.subagents.simple.explore" },
|
|
1200
1324
|
{ role: "librarian", label: "Librarian", description: "agents.subagents.simple.librarian" },
|
|
1201
1325
|
{ role: "task", label: "Task", description: "agents.subagents.simple.task" },
|
|
1326
|
+
{ role: "advisor", label: "Advisor", description: "agents.subagents.simple.advisor" },
|
|
1327
|
+
{ role: "deep-debugger", label: "Deep debugger", description: "agents.subagents.simple.deep-debugger" },
|
|
1328
|
+
{ role: "reviewer", label: "Reviewer", description: "agents.subagents.simple.reviewer" },
|
|
1202
1329
|
];
|
|
1203
1330
|
|
|
1204
1331
|
const PRESET_GROUP_ITEMS: Array<{ group: PresetGroup; label: string }> = [
|
|
@@ -1212,6 +1339,7 @@ const TIMEOUT_LABELS: Record<TimeoutKey, string> = {
|
|
|
1212
1339
|
"performance.commands.afterEdit": "Command after file edit",
|
|
1213
1340
|
"performance.commands.afterImplement": "Command after implementation",
|
|
1214
1341
|
"performance.internals.subagentStale": "Subagent stale",
|
|
1342
|
+
"performance.internals.mainTurnStale": "Main turn stale",
|
|
1215
1343
|
"performance.internals.taskLockStale": "Lock stale",
|
|
1216
1344
|
"performance.internals.taskLockRefresh": "Lock update",
|
|
1217
1345
|
};
|
|
@@ -1753,8 +1881,8 @@ async function showOrchestratorEditor(
|
|
|
1753
1881
|
const current = orchestrator.config.agents.orchestrators[role];
|
|
1754
1882
|
const basePath = ["agents", "orchestrators", role];
|
|
1755
1883
|
const choice = await selectOption(ctx, label, [
|
|
1756
|
-
opt(`Model: ${current.model}`, "
|
|
1757
|
-
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "
|
|
1884
|
+
opt(`Model: ${current.model}`, "Choose the model for this agent"),
|
|
1885
|
+
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "Choose how much this agent thinks before acting"),
|
|
1758
1886
|
...buildResetOptions(orchestrator, basePath),
|
|
1759
1887
|
opt("Back", "Return to the previous menu"),
|
|
1760
1888
|
]);
|
|
@@ -1800,8 +1928,8 @@ async function showSimpleSubagentEditor(
|
|
|
1800
1928
|
const current = orchestrator.config.agents.subagents.simple[role];
|
|
1801
1929
|
const basePath = ["agents", "subagents", "simple", role];
|
|
1802
1930
|
const choice = await selectOption(ctx, label, [
|
|
1803
|
-
opt(`Model: ${current.model}`, "
|
|
1804
|
-
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "
|
|
1931
|
+
opt(`Model: ${current.model}`, "Choose the model for this agent"),
|
|
1932
|
+
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "Choose how much this agent thinks before acting"),
|
|
1805
1933
|
...buildResetOptions(orchestrator, basePath),
|
|
1806
1934
|
opt("Back", "Return to the previous menu"),
|
|
1807
1935
|
]);
|
|
@@ -1902,8 +2030,8 @@ async function showPresetVariantEditor(
|
|
|
1902
2030
|
const variantPath = ["agents", "subagents", "presetGroups", group, "presets", presetName, "agents", variantName];
|
|
1903
2031
|
const options: OptionInput[] = [
|
|
1904
2032
|
opt(`Enabled: ${isEnabled(variant) ? "Yes" : "No"}`, "Toggle enabled state"),
|
|
1905
|
-
opt(`Model: ${variant.model}`, "
|
|
1906
|
-
opt(`Thinking: ${thinkingLabel(variant.thinking)}`, "
|
|
2033
|
+
opt(`Model: ${variant.model}`, "Choose the model for this agent"),
|
|
2034
|
+
opt(`Thinking: ${thinkingLabel(variant.thinking)}`, "Choose how much this agent thinks before acting"),
|
|
1907
2035
|
];
|
|
1908
2036
|
if (getOwnedScopes(orchestrator, variantPath).length > 0) {
|
|
1909
2037
|
options.push(opt("Delete", "Delete this agent override"));
|
|
@@ -1924,7 +2052,7 @@ async function showPresetVariantEditor(
|
|
|
1924
2052
|
continue;
|
|
1925
2053
|
}
|
|
1926
2054
|
if (choice.startsWith("Enabled:")) {
|
|
1927
|
-
await showBooleanSetting(orchestrator, ctx, "Enabled", [...variantPath, "enabled"]);
|
|
2055
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...variantPath, "enabled"], "Make this agent available for use", "Disable this agent so it is not used");
|
|
1928
2056
|
continue;
|
|
1929
2057
|
}
|
|
1930
2058
|
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
@@ -1955,7 +2083,7 @@ async function showPresetAgentsMenu(
|
|
|
1955
2083
|
options.push(opt(title, `${variant.model} / ${thinkingLabel(variant.thinking)}`));
|
|
1956
2084
|
byTitle.set(title, variantName);
|
|
1957
2085
|
}
|
|
1958
|
-
options.push(opt("New agent", "Add
|
|
2086
|
+
options.push(opt("New agent", "Add an agent variant to this preset"));
|
|
1959
2087
|
options.push(opt("Back", "Return to the previous menu"));
|
|
1960
2088
|
const choice = await selectOption(ctx, "Agents", options);
|
|
1961
2089
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2065,8 +2193,8 @@ async function showPresetEnabledSetting(
|
|
|
2065
2193
|
const yesTitle = withTags("Yes", formatSourceTags(true, info));
|
|
2066
2194
|
const noTitle = withTags("No", formatSourceTags(false, info));
|
|
2067
2195
|
const choice = await selectOption(ctx, "Enabled", [
|
|
2068
|
-
yesTitle,
|
|
2069
|
-
noTitle,
|
|
2196
|
+
{ title: yesTitle, description: "Make this preset available for selection" },
|
|
2197
|
+
{ title: noTitle, description: "Hide this preset so it can no longer be selected" },
|
|
2070
2198
|
opt("Back", "Return to the previous menu"),
|
|
2071
2199
|
]);
|
|
2072
2200
|
if (!choice || choice === "Back") return;
|
|
@@ -2124,7 +2252,7 @@ async function showPresetSettings(
|
|
|
2124
2252
|
options.push(opt(optionTitle, enabledPresetSummary(preset.agents)));
|
|
2125
2253
|
byTitle.set(optionTitle, presetName);
|
|
2126
2254
|
}
|
|
2127
|
-
options.push(opt("New preset", "Create preset"));
|
|
2255
|
+
options.push(opt("New preset", "Create a preset in this group"));
|
|
2128
2256
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2129
2257
|
const choice = await selectOption(ctx, title, options);
|
|
2130
2258
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2198,7 +2326,7 @@ async function showAfterEditCommands(orchestrator: Orchestrator, ctx: any): Prom
|
|
|
2198
2326
|
options.push(opt(`${title}${enabledTag}`, `${globsCount} patterns — ${cmd.run}`));
|
|
2199
2327
|
byTitle.set(`${title}${enabledTag}`, id);
|
|
2200
2328
|
}
|
|
2201
|
-
options.push(opt("New command", "Add command"));
|
|
2329
|
+
options.push(opt("New command", "Add a command to run after a file edit"));
|
|
2202
2330
|
options.push(...buildResetOptions(orchestrator, ["commands", "afterEdit"]));
|
|
2203
2331
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2204
2332
|
const choice = await selectOption(ctx, `After file edit: ${entries.length} commands`, options);
|
|
@@ -2247,7 +2375,7 @@ async function showAfterEditCommands(orchestrator: Orchestrator, ctx: any): Prom
|
|
|
2247
2375
|
continue;
|
|
2248
2376
|
}
|
|
2249
2377
|
if (commandChoice.startsWith("Enabled:")) {
|
|
2250
|
-
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"]);
|
|
2378
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"], "Run this command when its triggers fire", "Keep this command configured but stop running it");
|
|
2251
2379
|
continue;
|
|
2252
2380
|
}
|
|
2253
2381
|
if (commandChoice === "Triggers") {
|
|
@@ -2327,7 +2455,7 @@ async function showAfterImplementCommands(orchestrator: Orchestrator, ctx: any):
|
|
|
2327
2455
|
options.push(opt(title, cmd.run));
|
|
2328
2456
|
byTitle.set(title, id);
|
|
2329
2457
|
}
|
|
2330
|
-
options.push(opt("New command", "Add command"));
|
|
2458
|
+
options.push(opt("New command", "Add a command to run after implementation"));
|
|
2331
2459
|
options.push(...buildResetOptions(orchestrator, ["commands", "afterImplement"]));
|
|
2332
2460
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2333
2461
|
const choice = await selectOption(ctx, `After implementation: ${entries.length} commands`, options);
|
|
@@ -2364,7 +2492,7 @@ async function showAfterImplementCommands(orchestrator: Orchestrator, ctx: any):
|
|
|
2364
2492
|
continue;
|
|
2365
2493
|
}
|
|
2366
2494
|
if (commandChoice.startsWith("Enabled:")) {
|
|
2367
|
-
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"]);
|
|
2495
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"], "Run this command when its triggers fire", "Keep this command configured but stop running it");
|
|
2368
2496
|
continue;
|
|
2369
2497
|
}
|
|
2370
2498
|
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
@@ -2387,11 +2515,11 @@ async function showCommandsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2387
2515
|
const choice = await selectOption(ctx, "Commands", [
|
|
2388
2516
|
opt(
|
|
2389
2517
|
`After file edit: ${afterEditCount} commands`,
|
|
2390
|
-
|
|
2518
|
+
"Commands run when a matching file is edited (e.g. format, lint)",
|
|
2391
2519
|
),
|
|
2392
2520
|
opt(
|
|
2393
2521
|
`After implementation: ${afterImplementCount} commands`,
|
|
2394
|
-
|
|
2522
|
+
"Commands run once the implement phase completes (e.g. build, test)",
|
|
2395
2523
|
),
|
|
2396
2524
|
opt("Back", "Return to the previous menu"),
|
|
2397
2525
|
]);
|
|
@@ -2422,6 +2550,11 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2422
2550
|
path: ["performance", "internals", "subagentStale"],
|
|
2423
2551
|
value: orchestrator.config.performance.internals.subagentStale,
|
|
2424
2552
|
},
|
|
2553
|
+
{
|
|
2554
|
+
key: "performance.internals.mainTurnStale",
|
|
2555
|
+
path: ["performance", "internals", "mainTurnStale"],
|
|
2556
|
+
value: orchestrator.config.performance.internals.mainTurnStale,
|
|
2557
|
+
},
|
|
2425
2558
|
{
|
|
2426
2559
|
key: "performance.internals.taskLockStale",
|
|
2427
2560
|
path: ["performance", "internals", "taskLockStale"],
|
|
@@ -2436,7 +2569,7 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2436
2569
|
const options: OptionInput[] = timeoutEntries.map((entry) => {
|
|
2437
2570
|
const value = entry.value;
|
|
2438
2571
|
const key = entry.key;
|
|
2439
|
-
return opt(`${TIMEOUT_LABELS[key]}: ${formatDuration(value)}`, "
|
|
2572
|
+
return opt(`${TIMEOUT_LABELS[key]}: ${formatDuration(value)}`, "Change this time limit");
|
|
2440
2573
|
});
|
|
2441
2574
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2442
2575
|
const choice = await selectOption(ctx, "Timeouts", options);
|
|
@@ -2474,7 +2607,7 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2474
2607
|
async function showPerformanceSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2475
2608
|
while (true) {
|
|
2476
2609
|
const choice = await selectOption(ctx, "Performance", [
|
|
2477
|
-
opt("Timeouts", "
|
|
2610
|
+
opt("Timeouts", "Adjust per-operation time limits"),
|
|
2478
2611
|
opt("Back", "Return to the previous menu"),
|
|
2479
2612
|
]);
|
|
2480
2613
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2487,14 +2620,16 @@ async function showBooleanSetting(
|
|
|
2487
2620
|
ctx: any,
|
|
2488
2621
|
title: string,
|
|
2489
2622
|
keyPath: string[],
|
|
2623
|
+
yesDescription = "Turn this setting on",
|
|
2624
|
+
noDescription = "Turn this setting off",
|
|
2490
2625
|
): Promise<void> {
|
|
2491
2626
|
while (true) {
|
|
2492
2627
|
const info = getConfigSourceInfo(orchestrator, keyPath);
|
|
2493
2628
|
const yesTitle = withTags("Yes", formatSourceTags(true, info));
|
|
2494
2629
|
const noTitle = withTags("No", formatSourceTags(false, info));
|
|
2495
2630
|
const choice = await selectOption(ctx, title, [
|
|
2496
|
-
yesTitle,
|
|
2497
|
-
noTitle,
|
|
2631
|
+
{ title: yesTitle, description: yesDescription },
|
|
2632
|
+
{ title: noTitle, description: noDescription },
|
|
2498
2633
|
...buildResetOptions(orchestrator, keyPath),
|
|
2499
2634
|
opt("Back", "Return to the previous menu"),
|
|
2500
2635
|
]);
|
|
@@ -2516,14 +2651,16 @@ async function showInvertedBooleanSetting(
|
|
|
2516
2651
|
ctx: any,
|
|
2517
2652
|
title: string,
|
|
2518
2653
|
keyPath: string[],
|
|
2654
|
+
yesDescription = "Turn this setting on",
|
|
2655
|
+
noDescription = "Turn this setting off",
|
|
2519
2656
|
): Promise<void> {
|
|
2520
2657
|
while (true) {
|
|
2521
2658
|
const info = getConfigSourceInfo(orchestrator, keyPath);
|
|
2522
2659
|
const yesTitle = withTags("Yes", formatSourceTags(false, info));
|
|
2523
2660
|
const noTitle = withTags("No", formatSourceTags(true, info));
|
|
2524
2661
|
const choice = await selectOption(ctx, title, [
|
|
2525
|
-
yesTitle,
|
|
2526
|
-
noTitle,
|
|
2662
|
+
{ title: yesTitle, description: yesDescription },
|
|
2663
|
+
{ title: noTitle, description: noDescription },
|
|
2527
2664
|
...buildResetOptions(orchestrator, keyPath),
|
|
2528
2665
|
opt("Back", "Return to the previous menu"),
|
|
2529
2666
|
]);
|
|
@@ -2541,15 +2678,15 @@ async function showInvertedBooleanSetting(
|
|
|
2541
2678
|
}
|
|
2542
2679
|
|
|
2543
2680
|
async function showLogLevelSetting(orchestrator: Orchestrator, ctx: any): Promise<void> {
|
|
2544
|
-
const levels: Array<{ value: PiPiConfig["general"]["logLevel"]; label: string }> = [
|
|
2545
|
-
{ value: "debug", label: "Debug" },
|
|
2546
|
-
{ value: "info", label: "Info" },
|
|
2547
|
-
{ value: "warn", label: "Warning" },
|
|
2548
|
-
{ value: "error", label: "Error" },
|
|
2681
|
+
const levels: Array<{ value: PiPiConfig["general"]["logLevel"]; label: string; description: string }> = [
|
|
2682
|
+
{ value: "debug", label: "Debug", description: "Log everything, including detailed diagnostics for troubleshooting" },
|
|
2683
|
+
{ value: "info", label: "Info", description: "Log normal activity plus warnings and errors" },
|
|
2684
|
+
{ value: "warn", label: "Warning", description: "Log only warnings and errors" },
|
|
2685
|
+
{ value: "error", label: "Error", description: "Log only errors" },
|
|
2549
2686
|
];
|
|
2550
2687
|
while (true) {
|
|
2551
2688
|
const info = getConfigSourceInfo(orchestrator, ["general", "logLevel"]);
|
|
2552
|
-
const options: OptionInput[] = levels.map((entry) => withTags(entry.label, formatSourceTags(entry.value, info)));
|
|
2689
|
+
const options: OptionInput[] = levels.map((entry) => ({ title: withTags(entry.label, formatSourceTags(entry.value, info)), description: entry.description }));
|
|
2553
2690
|
options.push(...buildResetOptions(orchestrator, ["general", "logLevel"]));
|
|
2554
2691
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2555
2692
|
const choice = await selectOption(ctx, "Log level", options);
|
|
@@ -2571,6 +2708,7 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2571
2708
|
while (true) {
|
|
2572
2709
|
const choice = await selectOption(ctx, "General", [
|
|
2573
2710
|
opt(`Commit automatically: ${orchestrator.config.general.autoCommit ? "Yes" : "No"}`, "Enable or disable auto commits"),
|
|
2711
|
+
opt(`Inject root AGENTS.md: ${orchestrator.config.general.injectAgentsMd ? "Yes" : "No"}`, "Inject the working repo's root AGENTS.md into the agent system prompt"),
|
|
2574
2712
|
opt(`Ignore configs from other repos: ${orchestrator.config.general.loadExtraRepoConfigs ? "No" : "Yes"}`, "Load only root repo config"),
|
|
2575
2713
|
opt(`Log level: ${logLevelLabel(orchestrator.config.general.logLevel)}`, "Logging verbosity"),
|
|
2576
2714
|
opt(`Tracing: ${orchestrator.config.general.tracing ? "Yes" : "No"}`, "Capture full session traces to .pp/logs/traces/"),
|
|
@@ -2579,11 +2717,15 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2579
2717
|
]);
|
|
2580
2718
|
if (!choice || choice === "Back") return BACK;
|
|
2581
2719
|
if (choice.startsWith("Commit automatically:")) {
|
|
2582
|
-
await showBooleanSetting(orchestrator, ctx, "Commit automatically", ["general", "autoCommit"]);
|
|
2720
|
+
await showBooleanSetting(orchestrator, ctx, "Commit automatically", ["general", "autoCommit"], "Commit changes automatically as work progresses", "Leave committing to you");
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
if (choice.startsWith("Inject root AGENTS.md:")) {
|
|
2724
|
+
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");
|
|
2583
2725
|
continue;
|
|
2584
2726
|
}
|
|
2585
2727
|
if (choice.startsWith("Ignore configs from other repos:")) {
|
|
2586
|
-
await showInvertedBooleanSetting(orchestrator, ctx, "Ignore configs from other repos", ["general", "loadExtraRepoConfigs"]);
|
|
2728
|
+
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");
|
|
2587
2729
|
continue;
|
|
2588
2730
|
}
|
|
2589
2731
|
if (choice.startsWith("Log level:")) {
|
|
@@ -2591,7 +2733,7 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2591
2733
|
continue;
|
|
2592
2734
|
}
|
|
2593
2735
|
if (choice.startsWith("Tracing:")) {
|
|
2594
|
-
await showBooleanSetting(orchestrator, ctx, "Tracing", ["general", "tracing"]);
|
|
2736
|
+
await showBooleanSetting(orchestrator, ctx, "Tracing", ["general", "tracing"], "Capture full session traces to .pp/logs/traces/", "Do not record session traces");
|
|
2595
2737
|
continue;
|
|
2596
2738
|
}
|
|
2597
2739
|
await showFlantInfraMenu(orchestrator, ctx);
|
|
@@ -2685,7 +2827,7 @@ async function showSettingsMenu(orchestrator: Orchestrator, ctx: any): Promise<t
|
|
|
2685
2827
|
opt("General", "Commit, log level, Flant AI, repos"),
|
|
2686
2828
|
opt("Agents", "Orchestrator and subagent configuration"),
|
|
2687
2829
|
opt("Commands", "After file edit and after implementation"),
|
|
2688
|
-
opt("Performance", "
|
|
2830
|
+
opt("Performance", "Per-operation timeout limits"),
|
|
2689
2831
|
opt("LSP", "Language server controls"),
|
|
2690
2832
|
opt("Back", "Return to the previous menu"),
|
|
2691
2833
|
];
|
|
@@ -2777,10 +2919,10 @@ async function showAutonomousPhaseSettings(
|
|
|
2777
2919
|
const reviewPreset = phaseConfig.reviewPreset ?? defaultAutonomousReviewPreset(type, phase);
|
|
2778
2920
|
const maxReview = phaseConfig.maxReviewPasses >= 999 ? "No limit" : String(phaseConfig.maxReviewPasses);
|
|
2779
2921
|
const options: OptionInput[] = [
|
|
2780
|
-
opt(`Review preset: ${reviewPreset}`, "
|
|
2922
|
+
opt(`Review preset: ${reviewPreset}`, "Pick which reviewer group runs in this phase"),
|
|
2781
2923
|
];
|
|
2782
2924
|
if (phase === "plan") {
|
|
2783
|
-
options.push(opt(`Planner preset: ${phaseConfig.plannerPreset ?? "regular"}`, "
|
|
2925
|
+
options.push(opt(`Planner preset: ${phaseConfig.plannerPreset ?? "regular"}`, "Pick which planner group runs in this phase"));
|
|
2784
2926
|
}
|
|
2785
2927
|
options.push(opt(`Max review passes: ${maxReview}`, "Safety cap for autonomous review loops"));
|
|
2786
2928
|
options.push(opt("Back", "Return to autonomous settings"));
|
|
@@ -3056,201 +3198,138 @@ async function openCodeReviewInPlannotator(
|
|
|
3056
3198
|
if (payload.diffType) requestPayload.diffType = payload.diffType;
|
|
3057
3199
|
if (payload.defaultBranch) requestPayload.defaultBranch = payload.defaultBranch;
|
|
3058
3200
|
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
}, 30000);
|
|
3064
|
-
orchestrator.pi.events.emit("plannotator:request", {
|
|
3065
|
-
requestId: crypto.randomUUID(),
|
|
3066
|
-
action: "code-review",
|
|
3067
|
-
payload: requestPayload,
|
|
3068
|
-
respond: (response: any) => {
|
|
3069
|
-
handled = true;
|
|
3070
|
-
clearTimeout(timer);
|
|
3071
|
-
if (response?.status !== "handled") {
|
|
3072
|
-
resolve({ status: "error", error: response?.error || "Plannotator is not available." });
|
|
3073
|
-
return;
|
|
3074
|
-
}
|
|
3075
|
-
const approved = !!response?.result?.approved;
|
|
3076
|
-
const feedback = typeof response?.result?.feedback === "string" && response.result.feedback.trim().length > 0
|
|
3077
|
-
? response.result.feedback
|
|
3078
|
-
: undefined;
|
|
3079
|
-
resolve({ status: approved ? "approved" : "needs_changes", feedback });
|
|
3080
|
-
},
|
|
3081
|
-
});
|
|
3082
|
-
});
|
|
3083
|
-
}
|
|
3201
|
+
const { opened, reviewId } = await openPlannotator(orchestrator.pi, "code-review", requestPayload);
|
|
3202
|
+
if (!opened) {
|
|
3203
|
+
return { status: "error", error: "Plannotator is not available." };
|
|
3204
|
+
}
|
|
3084
3205
|
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3206
|
+
let result: { approved: boolean; feedback?: string; error?: string };
|
|
3207
|
+
try {
|
|
3208
|
+
result = await waitForPlannotatorResult(orchestrator, reviewId, null);
|
|
3209
|
+
} catch (err) {
|
|
3210
|
+
return { status: "error", error: err instanceof Error ? err.message : "Plannotator review failed." };
|
|
3211
|
+
}
|
|
3212
|
+
if (result.error) {
|
|
3213
|
+
return { status: "error", error: result.error };
|
|
3214
|
+
}
|
|
3215
|
+
const feedback = typeof result.feedback === "string" && result.feedback.trim().length > 0
|
|
3216
|
+
? result.feedback
|
|
3217
|
+
: undefined;
|
|
3218
|
+
return { status: result.approved ? "approved" : "needs_changes", feedback };
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
// Per-repo interleaved Plannotator loop (#3a). Runs from the persisted cursor: for
|
|
3222
|
+
// each repo, ask the diff scope, open Plannotator and WAIT (dialogue closed). On
|
|
3223
|
+
// NEEDS_CHANGES, persist the cursor advanced past this repo and return a work
|
|
3224
|
+
// instruction (mirrors the plan path: answer questions + apply changes) so the
|
|
3225
|
+
// agent fixes THIS repo before the next opens; the next /pp resumes the loop. On
|
|
3226
|
+
// approved/error/skip, advance and continue in-loop. When the cursor is exhausted
|
|
3227
|
+
// or the user stops, clear the cursor and return null (fall back to the menu).
|
|
3228
|
+
async function runPlannotatorCursor(orchestrator: Orchestrator, ctx: any): Promise<string | null> {
|
|
3229
|
+
const task = orchestrator.active;
|
|
3230
|
+
if (!task) return null;
|
|
3231
|
+
const cursor = task.state.plannotatorCursor;
|
|
3232
|
+
if (!cursor) return null;
|
|
3233
|
+
|
|
3234
|
+
while (task.state.plannotatorCursor && task.state.plannotatorCursor.index < task.state.plannotatorCursor.repoPaths.length) {
|
|
3235
|
+
const cur = task.state.plannotatorCursor;
|
|
3236
|
+
const repoPath = cur.repoPaths[cur.index];
|
|
3237
|
+
const repo = getRegisteredRepos(orchestrator).find((r) => r.path === repoPath) ?? { path: repoPath, isRoot: false };
|
|
3238
|
+
|
|
3239
|
+
const diffChoice = await selectOption(ctx, `Review: ${formatRepoLabel(repo)}`, [
|
|
3240
|
+
opt("All branch changes", "Committed changes vs base branch"),
|
|
3241
|
+
opt("Last commit", "Changes in the most recent commit"),
|
|
3242
|
+
opt("Since commit", "Review all changes since a specific commit"),
|
|
3243
|
+
opt("Uncommitted changes", "Working directory changes"),
|
|
3244
|
+
opt("Skip this repo", "Move to the next repository"),
|
|
3245
|
+
opt("Done (stop reviewing)", "Stop iterating repositories"),
|
|
3246
|
+
]);
|
|
3247
|
+
|
|
3248
|
+
if (!diffChoice || diffChoice === "Done (stop reviewing)") {
|
|
3249
|
+
task.state.plannotatorCursor = undefined;
|
|
3250
|
+
saveTask(task.dir, task.state);
|
|
3251
|
+
return null;
|
|
3252
|
+
}
|
|
3253
|
+
if (diffChoice === "Skip this repo") {
|
|
3254
|
+
cur.index += 1;
|
|
3255
|
+
saveTask(task.dir, task.state);
|
|
3256
|
+
continue;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
let diffType: string;
|
|
3260
|
+
let defaultBranch: string | undefined;
|
|
3261
|
+
if (diffChoice === "All branch changes") {
|
|
3262
|
+
diffType = "branch";
|
|
3263
|
+
defaultBranch = await detectDefaultBranch(orchestrator, getRegisteredRepos(orchestrator), repo.path);
|
|
3264
|
+
} else if (diffChoice === "Last commit") {
|
|
3265
|
+
diffType = "last-commit";
|
|
3266
|
+
} else if (diffChoice === "Since commit") {
|
|
3267
|
+
const pickedHash = await pickCommitForRepo(orchestrator, ctx, repo);
|
|
3268
|
+
if (!pickedHash) continue;
|
|
3269
|
+
diffType = "branch";
|
|
3270
|
+
defaultBranch = pickedHash;
|
|
3271
|
+
} else {
|
|
3272
|
+
diffType = "uncommitted";
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
const result = await openCodeReviewInPlannotator(orchestrator, { cwd: repo.path, diffType, defaultBranch });
|
|
3276
|
+
|
|
3277
|
+
// Advance past this repo regardless of outcome; on needs_changes the agent
|
|
3278
|
+
// fixes it during the turn started by the returned instruction, then the next
|
|
3279
|
+
// /pp resumes at the following repo.
|
|
3280
|
+
cur.index += 1;
|
|
3281
|
+
const exhausted = cur.index >= cur.repoPaths.length;
|
|
3282
|
+
|
|
3283
|
+
if (result.status === "needs_changes") {
|
|
3284
|
+
if (exhausted) task.state.plannotatorCursor = undefined;
|
|
3285
|
+
setStep(orchestrator, "llm_work");
|
|
3286
|
+
saveTask(task.dir, task.state);
|
|
3287
|
+
const feedback = result.feedback ? `\n\nFeedback:\n${result.feedback}` : "";
|
|
3288
|
+
const more = exhausted
|
|
3289
|
+
? "This was the last repo to review."
|
|
3290
|
+
: "After you finish, run /pp to continue Plannotator review of the remaining repositories.";
|
|
3291
|
+
return advanceBanner(
|
|
3292
|
+
`[PI-PI] Plannotator requested changes for ${formatRepoLabel(repo)}.${feedback}\n\n` +
|
|
3293
|
+
"Address the user's feedback. If the feedback contains questions, answer them. If it requests changes, " +
|
|
3294
|
+
`make the changes. Then call pp_phase_complete when done.\n\n${more}`,
|
|
3295
|
+
);
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
if (result.status === "error") {
|
|
3299
|
+
ctx.ui.notify(`${formatRepoLabel(repo)}: ERROR${result.error ? ` — ${result.error}` : ""}`, "warning");
|
|
3300
|
+
} else {
|
|
3301
|
+
ctx.ui.notify(`${formatRepoLabel(repo)}: APPROVED`, "info");
|
|
3302
|
+
}
|
|
3303
|
+
saveTask(task.dir, task.state);
|
|
3103
3304
|
}
|
|
3104
|
-
|
|
3305
|
+
|
|
3306
|
+
// Cursor exhausted with no outstanding changes: clear it and fall back to /pp.
|
|
3307
|
+
task.state.plannotatorCursor = undefined;
|
|
3308
|
+
saveTask(task.dir, task.state);
|
|
3309
|
+
return null;
|
|
3105
3310
|
}
|
|
3106
3311
|
|
|
3107
3312
|
async function showReviewMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
3108
3313
|
while (true) {
|
|
3109
|
-
const
|
|
3110
|
-
{ title: "
|
|
3111
|
-
{ title: "Last commit", description: "Review changes in the most recent commit" },
|
|
3112
|
-
{ title: "Since commit", description: "Review all changes since a specific commit" },
|
|
3113
|
-
{ title: "Uncommitted changes", description: "Review working directory changes" },
|
|
3114
|
-
{ title: "Describe", description: "Describe what to review and let the agent figure it out" },
|
|
3314
|
+
const choice = await selectOption(ctx, "Review", [
|
|
3315
|
+
{ 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" },
|
|
3115
3316
|
{ title: "Resume", description: "Resume a previously unfinished review" },
|
|
3116
3317
|
{ title: "Back", description: "Return to the previous menu" },
|
|
3117
|
-
];
|
|
3118
|
-
const choice = await selectOption(ctx, "Review", options);
|
|
3318
|
+
]);
|
|
3119
3319
|
if (!choice || choice === "Back") return BACK;
|
|
3120
3320
|
|
|
3121
|
-
if (choice === "
|
|
3122
|
-
const result = await showResumeMenu(orchestrator, ctx, "review", "No paused review tasks found.");
|
|
3123
|
-
if (result === "started") return result;
|
|
3124
|
-
continue;
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
if (choice === "Current branch") {
|
|
3128
|
-
const repos = getRegisteredRepos(orchestrator);
|
|
3129
|
-
const repoRanges = await Promise.all(
|
|
3130
|
-
repos.map(async (repo) => `- ${formatRepoLabel(repo)}: ${await detectDefaultBranch(orchestrator, repos, repo.path)}..HEAD`),
|
|
3131
|
-
);
|
|
3132
|
-
const prContexts = await detectCurrentPrContext(orchestrator, repos);
|
|
3133
|
-
|
|
3134
|
-
const urLines = [
|
|
3135
|
-
"# User Request",
|
|
3136
|
-
"Review current branch changes across registered repositories.",
|
|
3137
|
-
"",
|
|
3138
|
-
"Diff ranges:",
|
|
3139
|
-
...repoRanges,
|
|
3140
|
-
];
|
|
3141
|
-
const prUrls = prContexts
|
|
3142
|
-
.filter((pr) => pr.prUrl)
|
|
3143
|
-
.map((pr) => `- ${pr.repoPath}: ${pr.prUrl}`);
|
|
3144
|
-
if (prUrls.length > 0) {
|
|
3145
|
-
urLines.push("", "Open PRs:", ...prUrls);
|
|
3146
|
-
}
|
|
3147
|
-
urLines.push("", "## Problem", "Review and identify issues in the code changes.", "", "## Constraints", "Focus on correctness, edge cases, style, missing tests, potential bugs.");
|
|
3148
|
-
const urContent = urLines.join("\n") + "\n";
|
|
3149
|
-
|
|
3150
|
-
let resContent = [
|
|
3151
|
-
"## Affected Code",
|
|
3152
|
-
"(to be filled during review)",
|
|
3153
|
-
"",
|
|
3154
|
-
"## Architecture Context",
|
|
3155
|
-
"(to be filled during review)",
|
|
3156
|
-
"",
|
|
3157
|
-
"## Constraints & Edge Cases",
|
|
3158
|
-
"- MUST: Review all changed code across the registered repositories",
|
|
3159
|
-
"- RISK: Issues can span repository boundaries",
|
|
3160
|
-
].join("\n") + "\n";
|
|
3161
|
-
if (prContexts.length > 0) {
|
|
3162
|
-
const prContextBlocks = prContexts
|
|
3163
|
-
.map((pr) => {
|
|
3164
|
-
const lines = [`${pr.repoPath}`];
|
|
3165
|
-
if (pr.prUrl) lines.push(`URL: ${pr.prUrl}`);
|
|
3166
|
-
if (pr.prContext) lines.push(pr.prContext);
|
|
3167
|
-
return lines.join("\n");
|
|
3168
|
-
})
|
|
3169
|
-
.join("\n\n");
|
|
3170
|
-
resContent = appendResearchOpenQuestions(resContent, `PR context:\n${prContextBlocks}`);
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3174
|
-
if (!modeSelection) continue;
|
|
3175
|
-
const description = "review-current-branch";
|
|
3176
|
-
return startReviewTask(orchestrator, ctx, urContent, resContent, description, modeSelection.mode, modeSelection.autonomousConfig);
|
|
3177
|
-
}
|
|
3178
|
-
|
|
3179
|
-
if (choice === "Last commit") {
|
|
3180
|
-
const repos = getRegisteredRepos(orchestrator);
|
|
3181
|
-
const urContent = [
|
|
3182
|
-
"# User Request",
|
|
3183
|
-
"Review last commit changes across registered repositories",
|
|
3184
|
-
"",
|
|
3185
|
-
"## Problem",
|
|
3186
|
-
"Review and identify issues in the most recent commit.",
|
|
3187
|
-
"",
|
|
3188
|
-
"## Constraints",
|
|
3189
|
-
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3190
|
-
"",
|
|
3191
|
-
].join("\n");
|
|
3192
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3193
|
-
if (!modeSelection) continue;
|
|
3194
|
-
return startReviewTask(orchestrator, ctx, urContent, null, "review-last-commit", modeSelection.mode, modeSelection.autonomousConfig);
|
|
3195
|
-
}
|
|
3196
|
-
|
|
3197
|
-
if (choice === "Since commit") {
|
|
3198
|
-
const repos = getRegisteredRepos(orchestrator);
|
|
3199
|
-
const repoOptions: OptionInput[] = repos.map((repo) => ({
|
|
3200
|
-
title: formatRepoLabel(repo),
|
|
3201
|
-
description: "Choose repository for commit range",
|
|
3202
|
-
}));
|
|
3203
|
-
repoOptions.push({ title: "Back", description: "Return to the previous menu" });
|
|
3204
|
-
const repoChoice = await selectOption(ctx, "Select repository", repoOptions);
|
|
3205
|
-
if (!repoChoice || repoChoice === "Back") continue;
|
|
3206
|
-
const selectedRepo = repos.find((repo) => formatRepoLabel(repo) === repoChoice);
|
|
3207
|
-
if (!selectedRepo) continue;
|
|
3208
|
-
const pickedHash = await pickCommitForRepo(orchestrator, ctx, selectedRepo);
|
|
3209
|
-
if (!pickedHash) continue;
|
|
3210
|
-
|
|
3211
|
-
const urContent = [
|
|
3212
|
-
"# User Request",
|
|
3213
|
-
`Review changes in ${selectedRepo.path} since commit ${pickedHash}`,
|
|
3214
|
-
"",
|
|
3215
|
-
"## Problem",
|
|
3216
|
-
`Review and identify issues in all changes in ${selectedRepo.path} since ${pickedHash}.`,
|
|
3217
|
-
"",
|
|
3218
|
-
"## Constraints",
|
|
3219
|
-
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3220
|
-
"",
|
|
3221
|
-
].join("\n");
|
|
3222
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3223
|
-
if (!modeSelection) continue;
|
|
3224
|
-
return startReviewTask(orchestrator, ctx, urContent, null, "review-since-commit", modeSelection.mode, modeSelection.autonomousConfig);
|
|
3225
|
-
}
|
|
3226
|
-
|
|
3227
|
-
if (choice === "Uncommitted changes") {
|
|
3228
|
-
const repos = getRegisteredRepos(orchestrator);
|
|
3229
|
-
const urContent = [
|
|
3230
|
-
"# User Request",
|
|
3231
|
-
"Review uncommitted changes across registered repositories",
|
|
3232
|
-
"",
|
|
3233
|
-
"## Problem",
|
|
3234
|
-
"Review and identify issues in uncommitted working directory changes.",
|
|
3235
|
-
"",
|
|
3236
|
-
"## Constraints",
|
|
3237
|
-
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3238
|
-
"",
|
|
3239
|
-
].join("\n");
|
|
3321
|
+
if (choice === "New") {
|
|
3240
3322
|
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3241
3323
|
if (!modeSelection) continue;
|
|
3242
|
-
|
|
3324
|
+
await orchestrator.startTask(ctx, "review", "review", undefined, undefined, modeSelection.mode);
|
|
3325
|
+
if (!orchestrator.active || orchestrator.active.type !== "review") return BACK;
|
|
3326
|
+
orchestrator.active.state.autonomousConfig = modeSelection.autonomousConfig;
|
|
3327
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
3328
|
+
return "started";
|
|
3243
3329
|
}
|
|
3244
3330
|
|
|
3245
|
-
const
|
|
3246
|
-
if (
|
|
3247
|
-
const trimmed = String(input).trim();
|
|
3248
|
-
const description = trimmed || "review";
|
|
3249
|
-
|
|
3250
|
-
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`;
|
|
3251
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3252
|
-
if (!modeSelection) continue;
|
|
3253
|
-
return startReviewTask(orchestrator, ctx, urContent, null, description, modeSelection.mode, modeSelection.autonomousConfig);
|
|
3331
|
+
const result = await showResumeMenu(orchestrator, ctx, "review", "No paused review tasks found.");
|
|
3332
|
+
if (result === "started") return result;
|
|
3254
3333
|
}
|
|
3255
3334
|
}
|
|
3256
3335
|
|
|
@@ -3344,7 +3423,7 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
|
|
|
3344
3423
|
const choice = await selectOption(ctx, "/pp", [
|
|
3345
3424
|
{ title: "Task", description: "Start a new task or resume a paused one" },
|
|
3346
3425
|
{ title: "Info", description: "Subagents, usage, and task status" },
|
|
3347
|
-
{ title: "Settings", description: "
|
|
3426
|
+
{ title: "Settings", description: "Models, agents, commands, and other configuration" },
|
|
3348
3427
|
{ title: "Back", description: "Close this menu" },
|
|
3349
3428
|
]);
|
|
3350
3429
|
if (!choice || choice === "Back") return undefined;
|
|
@@ -3405,7 +3484,7 @@ async function showQuickTaskMenu(
|
|
|
3405
3484
|
opt("Complete", "Mark task as done and clean up"),
|
|
3406
3485
|
opt("Pause", "Suspend task to resume later"),
|
|
3407
3486
|
opt("Info", "Subagents, usage, and task status"),
|
|
3408
|
-
opt("Settings", "
|
|
3487
|
+
opt("Settings", "Models, agents, commands, and other configuration"),
|
|
3409
3488
|
opt("Back", "Return to the prompt and keep working"),
|
|
3410
3489
|
]);
|
|
3411
3490
|
// A deliberate ESC in tool mode must stop the turn cleanly (mirror the
|
|
@@ -3434,8 +3513,13 @@ export async function showActiveTaskMenu(
|
|
|
3434
3513
|
ctx: any,
|
|
3435
3514
|
summary: string,
|
|
3436
3515
|
mode: MenuMode = "command",
|
|
3516
|
+
// Display-only override for the autonomous terminal implement handoff (#1):
|
|
3517
|
+
// render the guided Next/Review menu even though the task is autonomous, WITHOUT
|
|
3518
|
+
// mutating task.state.mode. Never persisted; does not affect the footer indicator
|
|
3519
|
+
// or getEffectivePhaseMode.
|
|
3520
|
+
forceGuided = false,
|
|
3437
3521
|
): Promise<string> {
|
|
3438
|
-
const continueMessage = "User wants to continue. Run /pp when ready to advance.";
|
|
3522
|
+
const continueMessage = advanceBanner("[PI-PI] User wants to continue. Run /pp when ready to advance.");
|
|
3439
3523
|
|
|
3440
3524
|
while (true) {
|
|
3441
3525
|
if (!orchestrator.active) return "No active task.";
|
|
@@ -3444,6 +3528,15 @@ export async function showActiveTaskMenu(
|
|
|
3444
3528
|
if (task.type === "quick") {
|
|
3445
3529
|
return showQuickTaskMenu(orchestrator, ctx, summary, mode);
|
|
3446
3530
|
}
|
|
3531
|
+
// Auto-resume an in-progress per-repo Plannotator review (#3a): if the agent
|
|
3532
|
+
// just finished fixing one repo's feedback and ran /pp, continue the loop at
|
|
3533
|
+
// the next repo instead of showing the top-level menu. On another
|
|
3534
|
+
// needs_changes this returns a fresh work instruction; when exhausted it
|
|
3535
|
+
// clears the cursor and falls through to the normal menu.
|
|
3536
|
+
if (task.state.plannotatorCursor) {
|
|
3537
|
+
const resumeText = await runPlannotatorCursor(orchestrator, ctx);
|
|
3538
|
+
if (resumeText) return resumeText;
|
|
3539
|
+
}
|
|
3447
3540
|
const phase = task.state.phase;
|
|
3448
3541
|
const step = task.state.step;
|
|
3449
3542
|
const effectiveMode = getEffectivePhaseMode(task.state);
|
|
@@ -3455,15 +3548,21 @@ export async function showActiveTaskMenu(
|
|
|
3455
3548
|
const { autoLabel } = getReviewLabels(orchestrator);
|
|
3456
3549
|
const isReviewPhase = phase === "review";
|
|
3457
3550
|
const hasPlannotator = phase === "plan" || phase === "implement" || isReviewPhase;
|
|
3551
|
+
// The artifact the automated reviewers scan, mirroring getReviewPresetGroup: brainstorm →
|
|
3552
|
+
// brainstormReviewers (state files), plan → planReviewers (synthesized plan), every other
|
|
3553
|
+
// phase → codeReviewers (code changes). This is the primary "Review" target named at the
|
|
3554
|
+
// top level; the per-phase "Review on my own" description names the manual editor pass's
|
|
3555
|
+
// target separately (it can differ, e.g. state files in a debug/review phase).
|
|
3556
|
+
const reviewTarget = phase === "plan" ? "the synthesized plan" : phase === "brainstorm" ? "this phase's state files" : "the code changes";
|
|
3458
3557
|
|
|
3459
3558
|
const opt = (title: string, description: string): OptionInput => ({ title, description });
|
|
3460
3559
|
|
|
3461
|
-
if (effectiveMode === "autonomous") {
|
|
3560
|
+
if (effectiveMode === "autonomous" && !forceGuided) {
|
|
3462
3561
|
const { choice: autoChoice, cancelReason } = await selectOptionCancelable(ctx, `/pp\n\nTask: ${task.type}\nPhase: ${phase}${summary !== "/pp" ? `\n\n${summary}` : ""}`, [
|
|
3463
3562
|
opt("Complete task", "Mark task as done and clean up"),
|
|
3464
3563
|
opt("Pause task", "Suspend task to resume later"),
|
|
3465
3564
|
opt("Info", "Subagents, usage, and task status"),
|
|
3466
|
-
opt("Settings", "
|
|
3565
|
+
opt("Settings", "Models, agents, commands, and other configuration"),
|
|
3467
3566
|
opt("Back", "Return to the prompt and keep working"),
|
|
3468
3567
|
]);
|
|
3469
3568
|
// A deliberate ESC only needs distinct handling in tool mode (so
|
|
@@ -3490,10 +3589,10 @@ export async function showActiveTaskMenu(
|
|
|
3490
3589
|
const options: OptionInput[] = [];
|
|
3491
3590
|
options.push(opt("Next", "Complete, pause, or continue to next phase"));
|
|
3492
3591
|
if (!waiting) {
|
|
3493
|
-
options.push(opt("Review", "
|
|
3592
|
+
options.push(opt("Review", `Review ${reviewTarget}: automated reviewers${hasPlannotator ? ", Plannotator, or" : " or"} your own editor pass`));
|
|
3494
3593
|
}
|
|
3495
3594
|
options.push(opt("Info", "Subagents, usage, and task status"));
|
|
3496
|
-
options.push(opt("Settings", "
|
|
3595
|
+
options.push(opt("Settings", "Models, agents, commands, and other configuration"));
|
|
3497
3596
|
options.push(opt("Back", "Return to the prompt and keep working"));
|
|
3498
3597
|
|
|
3499
3598
|
const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
|
|
@@ -3518,68 +3617,109 @@ export async function showActiveTaskMenu(
|
|
|
3518
3617
|
}
|
|
3519
3618
|
|
|
3520
3619
|
if (choice === "Next") {
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3620
|
+
// The Next submenu runs its own loop so that a deeper submenu's "Back"
|
|
3621
|
+
// (e.g. Publish) returns here rather than overshooting to the top-level menu.
|
|
3622
|
+
while (true) {
|
|
3623
|
+
const canContinue = phase !== "implement" && !waiting;
|
|
3624
|
+
const continueLabel = phase === "plan" ? "Continue to implement" : "Continue to plan & implement";
|
|
3625
|
+
const finishOptions: OptionInput[] = [];
|
|
3626
|
+
if (phase === "review") {
|
|
3627
|
+
finishOptions.push(opt("Publish", "Publish the synthesized review findings as file comments or GitHub PR comments"));
|
|
3628
|
+
}
|
|
3629
|
+
if (canContinue) {
|
|
3630
|
+
finishOptions.push(opt(continueLabel, "Approve and advance to the next phase"));
|
|
3631
|
+
}
|
|
3632
|
+
finishOptions.push(opt("Complete", "Mark task as done and clean up"));
|
|
3633
|
+
finishOptions.push(opt("Pause", "Suspend task to resume later"));
|
|
3634
|
+
finishOptions.push(opt("Back", "Return to the previous menu"));
|
|
3635
|
+
|
|
3636
|
+
const finishChoice = await selectOption(ctx, "Next", finishOptions);
|
|
3637
|
+
if (!finishChoice || finishChoice === "Back") break;
|
|
3638
|
+
if (finishChoice === "Publish") {
|
|
3639
|
+
const target = await selectOption(ctx, "Publish", [
|
|
3640
|
+
opt("As file comments", "Insert AI_COMMENT: markers at each finding's location in the source"),
|
|
3641
|
+
opt("As GitHub PR comments", "Post line-anchored comments to the branch's PR from your GitHub account"),
|
|
3642
|
+
opt("Back", "Return to the previous menu"),
|
|
3643
|
+
]);
|
|
3644
|
+
if (!target || target === "Back") continue;
|
|
3645
|
+
const guardMessage = publishGuard(task.dir);
|
|
3646
|
+
if (guardMessage) {
|
|
3647
|
+
ctx.ui.notify(guardMessage, "info");
|
|
3648
|
+
continue;
|
|
3649
|
+
}
|
|
3650
|
+
return target === "As file comments"
|
|
3651
|
+
? publishFileCommentsBanner(task.dir)
|
|
3652
|
+
: publishPrCommentsBanner(task.dir);
|
|
3653
|
+
}
|
|
3654
|
+
if (finishChoice === "Pause") {
|
|
3655
|
+
const text = await pauseTask(orchestrator, ctx);
|
|
3656
|
+
return mode === "tool" ? text : "";
|
|
3657
|
+
}
|
|
3658
|
+
if (finishChoice === "Complete") {
|
|
3659
|
+
const text = await finishTask(orchestrator, ctx);
|
|
3660
|
+
return mode === "tool" ? text : "";
|
|
3661
|
+
}
|
|
3662
|
+
const next = nextPhase(task.type, phase);
|
|
3663
|
+
|
|
3664
|
+
if (
|
|
3665
|
+
next === "plan" &&
|
|
3666
|
+
task.type === "brainstorm"
|
|
3667
|
+
) {
|
|
3668
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "implement");
|
|
3669
|
+
if (!modeSelection) continue;
|
|
3670
|
+
task.state.mode = modeSelection.mode;
|
|
3671
|
+
task.state.effectiveMode = undefined;
|
|
3672
|
+
task.state.autonomousConfig = modeSelection.autonomousConfig;
|
|
3673
|
+
saveTask(task.dir, task.state);
|
|
3674
|
+
}
|
|
3554
3675
|
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3676
|
+
let plannerPreset: string | undefined;
|
|
3677
|
+
if (next === "plan") {
|
|
3678
|
+
if (getEffectiveMode(task.state) !== "autonomous") {
|
|
3679
|
+
const pickedPlannerPreset = await pickPreset(ctx, orchestrator, "planners", "Planner preset");
|
|
3680
|
+
if (!pickedPlannerPreset) continue;
|
|
3681
|
+
plannerPreset = pickedPlannerPreset;
|
|
3682
|
+
}
|
|
3561
3683
|
}
|
|
3684
|
+
finalizeReviewCycle(task);
|
|
3685
|
+
const result = await orchestrator.transitionToNextPhase(ctx, plannerPreset);
|
|
3686
|
+
if (!result.ok) return `Transition blocked: ${result.error}`;
|
|
3687
|
+
// Transition is now owned by the controller (state ≠ running) or we're
|
|
3688
|
+
// awaiting subagents; either way the menu returns empty.
|
|
3689
|
+
return "";
|
|
3562
3690
|
}
|
|
3563
|
-
|
|
3564
|
-
const result = await orchestrator.transitionToNextPhase(ctx, plannerPreset);
|
|
3565
|
-
if (!result.ok) return `Transition blocked: ${result.error}`;
|
|
3566
|
-
// Transition is now owned by the controller (state ≠ running) or we're
|
|
3567
|
-
// awaiting subagents; either way the menu returns empty.
|
|
3568
|
-
return "";
|
|
3691
|
+
continue;
|
|
3569
3692
|
}
|
|
3570
3693
|
|
|
3571
3694
|
if (choice === "Review") {
|
|
3695
|
+
// The Review submenu runs its own loop so a recoverable choice (no reviewers
|
|
3696
|
+
// enabled, preset cancelled, "Review on my own" Back, a plannotator early
|
|
3697
|
+
// exit) returns HERE rather than overshooting to the top-level /pp menu.
|
|
3698
|
+
// Only an explicit "Back" breaks out; start/wait/instruction paths return.
|
|
3699
|
+
while (true) {
|
|
3700
|
+
// A review already running: block re-entry BEFORE any finalizeReviewCycle
|
|
3701
|
+
// (which would null the live cycle and hide it), so we never double-spawn.
|
|
3702
|
+
// Notify and break back to the top-level menu with the live cycle intact
|
|
3703
|
+
// rather than exiting /pp.
|
|
3704
|
+
if (isReviewCycleLive(task)) {
|
|
3705
|
+
ctx.ui.notify("A review is already running", "info");
|
|
3706
|
+
break;
|
|
3707
|
+
}
|
|
3572
3708
|
const reviewOptions: OptionInput[] = [
|
|
3573
|
-
opt(autoLabel,
|
|
3709
|
+
opt(autoLabel, `Run configured reviewers over ${reviewTarget}`),
|
|
3574
3710
|
];
|
|
3575
3711
|
if (hasPlannotator) {
|
|
3576
3712
|
reviewOptions.push(opt("Review in Plannotator", phase === "plan" ? "Open plan review in browser" : "Open code diff review in browser"));
|
|
3577
3713
|
}
|
|
3578
|
-
reviewOptions.push(opt("Review on my own",
|
|
3714
|
+
reviewOptions.push(opt("Review on my own", phase === "implement"
|
|
3715
|
+
? "Mark spots in the changed files with AI_REVIEW: comments; the agent then addresses each and removes the marker"
|
|
3716
|
+
: phase === "plan"
|
|
3717
|
+
? "Mark spots in the synthesized plan with AI_REVIEW: comments; the agent then addresses each and removes the marker"
|
|
3718
|
+
: "Mark spots in USER_REQUEST.md, RESEARCH.md, and artifacts/*.md with AI_REVIEW: comments; the agent then addresses each and removes the marker"));
|
|
3579
3719
|
reviewOptions.push(opt("Back", "Return to the previous menu"));
|
|
3580
3720
|
|
|
3581
3721
|
const reviewChoice = await selectOption(ctx, "Review", reviewOptions);
|
|
3582
|
-
if (!reviewChoice || reviewChoice === "Back")
|
|
3722
|
+
if (!reviewChoice || reviewChoice === "Back") break;
|
|
3583
3723
|
|
|
3584
3724
|
if (reviewChoice === "Review in Plannotator") {
|
|
3585
3725
|
if (phase === "plan") {
|
|
@@ -3591,80 +3731,51 @@ export async function showActiveTaskMenu(
|
|
|
3591
3731
|
if (handled.continueLoop) continue;
|
|
3592
3732
|
return handled.text ?? text;
|
|
3593
3733
|
}
|
|
3594
|
-
const
|
|
3595
|
-
const
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
if (
|
|
3600
|
-
|
|
3601
|
-
while (true) {
|
|
3602
|
-
const diffChoice = await selectOption(ctx, `Review: ${formatRepoLabel(repo)}`, [
|
|
3603
|
-
opt("All branch changes", "Committed changes vs base branch"),
|
|
3604
|
-
opt("Last commit", "Changes in the most recent commit"),
|
|
3605
|
-
opt("Since commit", "Review all changes since a specific commit"),
|
|
3606
|
-
opt("Uncommitted changes", "Working directory changes"),
|
|
3607
|
-
opt("Skip this repo", "Move to the next repository"),
|
|
3608
|
-
opt("Done (stop reviewing)", "Stop iterating repositories"),
|
|
3609
|
-
]);
|
|
3610
|
-
|
|
3611
|
-
if (!diffChoice || diffChoice === "Done (stop reviewing)") {
|
|
3612
|
-
stopReviewing = true;
|
|
3613
|
-
break;
|
|
3614
|
-
}
|
|
3615
|
-
if (diffChoice === "Skip this repo") {
|
|
3616
|
-
summaries.push(`${formatRepoLabel(repo)}: SKIPPED`);
|
|
3617
|
-
break;
|
|
3618
|
-
}
|
|
3619
|
-
|
|
3620
|
-
let diffType: string | undefined;
|
|
3621
|
-
let defaultBranch: string | undefined;
|
|
3622
|
-
|
|
3623
|
-
if (diffChoice === "All branch changes") {
|
|
3624
|
-
diffType = "branch";
|
|
3625
|
-
defaultBranch = await detectDefaultBranch(orchestrator, repos, repo.path);
|
|
3626
|
-
} else if (diffChoice === "Last commit") {
|
|
3627
|
-
diffType = "last-commit";
|
|
3628
|
-
} else if (diffChoice === "Since commit") {
|
|
3629
|
-
const pickedHash = await pickCommitForRepo(orchestrator, ctx, repo);
|
|
3630
|
-
if (!pickedHash) continue;
|
|
3631
|
-
diffType = "branch";
|
|
3632
|
-
defaultBranch = pickedHash;
|
|
3633
|
-
} else {
|
|
3634
|
-
diffType = "uncommitted";
|
|
3635
|
-
}
|
|
3636
|
-
|
|
3637
|
-
const result = await openCodeReviewInPlannotator(orchestrator, {
|
|
3638
|
-
cwd: repo.path,
|
|
3639
|
-
diffType,
|
|
3640
|
-
defaultBranch,
|
|
3641
|
-
});
|
|
3642
|
-
if (result.status === "error") {
|
|
3643
|
-
summaries.push(`${formatRepoLabel(repo)}: ERROR${result.error ? ` — ${result.error}` : ""}`);
|
|
3644
|
-
} else if (result.status === "approved") {
|
|
3645
|
-
summaries.push(`${formatRepoLabel(repo)}: APPROVED`);
|
|
3646
|
-
} else {
|
|
3647
|
-
summaries.push(`${formatRepoLabel(repo)}: NEEDS_CHANGES${result.feedback ? `\nFeedback: ${result.feedback}` : ""}`);
|
|
3648
|
-
}
|
|
3649
|
-
break;
|
|
3650
|
-
}
|
|
3734
|
+
const allRepos = getRegisteredRepos(orchestrator);
|
|
3735
|
+
const eligible: string[] = [];
|
|
3736
|
+
for (const repo of allRepos) {
|
|
3737
|
+
const base = await detectDefaultBranch(orchestrator, allRepos, repo.path);
|
|
3738
|
+
const { changed, error } = await repoHasReviewableChanges(orchestrator, repo, base);
|
|
3739
|
+
if (error || changed) eligible.push(repo.path);
|
|
3651
3740
|
}
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
} else {
|
|
3656
|
-
ctx.ui.notify("No repositories were reviewed in Plannotator.", "info");
|
|
3741
|
+
if (eligible.length === 0) {
|
|
3742
|
+
ctx.ui.notify("No registered repositories have changes to review.", "info");
|
|
3743
|
+
continue;
|
|
3657
3744
|
}
|
|
3745
|
+
// Start (or restart) the interleaved per-repo cursor and run it. On
|
|
3746
|
+
// needs_changes runPlannotatorCursor persists the cursor and returns a
|
|
3747
|
+
// work instruction (exits the menu to start the agent turn); the next /pp
|
|
3748
|
+
// resumes at the following repo.
|
|
3749
|
+
task.state.plannotatorCursor = { repoPaths: eligible, index: 0 };
|
|
3750
|
+
saveTask(task.dir, task.state);
|
|
3751
|
+
const cursorText = await runPlannotatorCursor(orchestrator, ctx);
|
|
3752
|
+
if (cursorText) return cursorText;
|
|
3658
3753
|
continue;
|
|
3659
3754
|
}
|
|
3660
3755
|
|
|
3661
3756
|
if (reviewChoice === "Review on my own") {
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3757
|
+
const gate = await selectOption(ctx, "Editor review", [
|
|
3758
|
+
opt("Done", "I've added AI_REVIEW: markers and saved my files"),
|
|
3759
|
+
opt("Skip markers", "Continue without the marker workflow"),
|
|
3760
|
+
opt("Back", "Return to the review menu"),
|
|
3761
|
+
]);
|
|
3762
|
+
if (!gate || gate === "Back") continue;
|
|
3763
|
+
setStep(orchestrator, phase === "plan" ? "synthesize" : "llm_work");
|
|
3764
|
+
if (gate === "Skip markers") {
|
|
3765
|
+
return continueMessage;
|
|
3766
|
+
}
|
|
3767
|
+
if (phase === "implement") {
|
|
3768
|
+
return advanceBanner(
|
|
3769
|
+
"[PI-PI] The user reviewed the changes in their editor and left inline `AI_REVIEW:` markers " +
|
|
3770
|
+
`${AI_REVIEW_MARKER_SYNTAX}.\n\n` +
|
|
3771
|
+
"For each registered repo, enumerate the CHANGED files only — union of `git diff --name-only <base>...HEAD`, " +
|
|
3772
|
+
"`git diff --name-only`, `git diff --cached --name-only`, and untracked-non-ignored files from `git status --porcelain` " +
|
|
3773
|
+
"(base = each repo's configured base branch) — and search WITHIN those files for `AI_REVIEW:`. Do NOT grep the whole " +
|
|
3774
|
+
"worktree (avoid vendored/generated/node_modules and historical markers).\n\n" +
|
|
3775
|
+
AI_REVIEW_MARKER_LOOP,
|
|
3776
|
+
);
|
|
3666
3777
|
}
|
|
3667
|
-
return
|
|
3778
|
+
return readOnlyReviewBanner(phase, task.dir);
|
|
3668
3779
|
}
|
|
3669
3780
|
|
|
3670
3781
|
const reviewPreset = await pickPreset(ctx, orchestrator, getReviewPresetGroup(phase), "Review preset");
|
|
@@ -3681,6 +3792,7 @@ export async function showActiveTaskMenu(
|
|
|
3681
3792
|
const handled = handleReviewResult(ctx, text);
|
|
3682
3793
|
if (handled.continueLoop) continue;
|
|
3683
3794
|
return handled.text ?? text;
|
|
3795
|
+
}
|
|
3684
3796
|
}
|
|
3685
3797
|
}
|
|
3686
3798
|
}
|