@ilya-lesikov/pi-pi 0.8.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/extensions/orchestrator/agents/code-reviewer.ts +21 -5
- package/extensions/orchestrator/agents/constraints.test.ts +39 -1
- package/extensions/orchestrator/agents/constraints.ts +63 -2
- package/extensions/orchestrator/agents/tool-routing.ts +30 -14
- 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 +43 -27
- package/extensions/orchestrator/config.test.ts +40 -1
- package/extensions/orchestrator/config.ts +13 -0
- package/extensions/orchestrator/context.ts +16 -0
- package/extensions/orchestrator/custom-footer.test.ts +91 -0
- package/extensions/orchestrator/custom-footer.ts +20 -20
- package/extensions/orchestrator/event-handlers.test.ts +315 -1
- package/extensions/orchestrator/event-handlers.ts +397 -201
- package/extensions/orchestrator/integration.test.ts +305 -9
- package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/orchestrator/orchestrator.ts +46 -58
- package/extensions/orchestrator/phases/brainstorm.ts +11 -7
- package/extensions/orchestrator/phases/machine.test.ts +36 -0
- package/extensions/orchestrator/phases/machine.ts +11 -1
- package/extensions/orchestrator/phases/review-task.test.ts +20 -0
- package/extensions/orchestrator/phases/review-task.ts +44 -16
- package/extensions/orchestrator/phases/review.test.ts +26 -0
- package/extensions/orchestrator/phases/review.ts +40 -3
- package/extensions/orchestrator/pp-menu.test.ts +207 -1
- package/extensions/orchestrator/pp-menu.ts +376 -378
- package/extensions/orchestrator/state.test.ts +9 -0
- package/extensions/orchestrator/state.ts +15 -0
- package/extensions/orchestrator/transition-controller.test.ts +100 -0
- package/extensions/orchestrator/transition-controller.ts +61 -3
- 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,8 +24,9 @@ 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
31
|
import { cancelPendingPlannotatorWait, openPlannotator, waitForPlannotatorResult } from "./plannotator.js";
|
|
31
32
|
import { advanceBanner } from "./messages.js";
|
|
@@ -85,6 +86,7 @@ type TimeoutKey =
|
|
|
85
86
|
| "performance.commands.afterEdit"
|
|
86
87
|
| "performance.commands.afterImplement"
|
|
87
88
|
| "performance.internals.subagentStale"
|
|
89
|
+
| "performance.internals.mainTurnStale"
|
|
88
90
|
| "performance.internals.taskLockStale"
|
|
89
91
|
| "performance.internals.taskLockRefresh";
|
|
90
92
|
|
|
@@ -105,16 +107,22 @@ async function selectOptionCancelable(
|
|
|
105
107
|
question: string,
|
|
106
108
|
options: OptionInput[],
|
|
107
109
|
): Promise<{ choice?: string; cancelReason?: CancelReason }> {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
function opt(title: string, description: string): OptionInput {
|
|
@@ -151,51 +159,7 @@ function formatRepoList(repos: RepoInfo[]): string {
|
|
|
151
159
|
.join("\n");
|
|
152
160
|
}
|
|
153
161
|
|
|
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
162
|
|
|
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
163
|
|
|
200
164
|
async function pickCommitForRepo(orchestrator: Orchestrator, ctx: any, repo: RepoInfo): Promise<string | null> {
|
|
201
165
|
let commits: Array<{ hash: string; message: string; age: string }> = [];
|
|
@@ -276,6 +240,84 @@ function setStep(orchestrator: Orchestrator, step: string): void {
|
|
|
276
240
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
277
241
|
}
|
|
278
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
|
+
|
|
279
321
|
function showStatus(orchestrator: Orchestrator, ctx: any): void {
|
|
280
322
|
if (!orchestrator.active) {
|
|
281
323
|
ctx.ui.notify("No active task.", "info");
|
|
@@ -316,6 +358,9 @@ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
316
358
|
const type = orchestrator.active.type;
|
|
317
359
|
|
|
318
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;
|
|
319
364
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
320
365
|
unregisterAgentDefinitions(orchestrator.pi);
|
|
321
366
|
await orchestrator.cleanupActive();
|
|
@@ -340,6 +385,13 @@ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
340
385
|
async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string> {
|
|
341
386
|
if (!orchestrator.active) return "No active task.";
|
|
342
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
|
+
|
|
343
395
|
cancelPendingPlannotatorWait(orchestrator);
|
|
344
396
|
orchestrator.abortAllSubagents();
|
|
345
397
|
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
@@ -366,7 +418,8 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
366
418
|
// is over, nothing awaits this compaction).
|
|
367
419
|
void orchestrator.transitionController.requestTransition({
|
|
368
420
|
kind: "done",
|
|
369
|
-
|
|
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.`,
|
|
370
423
|
});
|
|
371
424
|
|
|
372
425
|
const urExists = existsSync(join(dir, "USER_REQUEST.md"));
|
|
@@ -1286,6 +1339,7 @@ const TIMEOUT_LABELS: Record<TimeoutKey, string> = {
|
|
|
1286
1339
|
"performance.commands.afterEdit": "Command after file edit",
|
|
1287
1340
|
"performance.commands.afterImplement": "Command after implementation",
|
|
1288
1341
|
"performance.internals.subagentStale": "Subagent stale",
|
|
1342
|
+
"performance.internals.mainTurnStale": "Main turn stale",
|
|
1289
1343
|
"performance.internals.taskLockStale": "Lock stale",
|
|
1290
1344
|
"performance.internals.taskLockRefresh": "Lock update",
|
|
1291
1345
|
};
|
|
@@ -1827,8 +1881,8 @@ async function showOrchestratorEditor(
|
|
|
1827
1881
|
const current = orchestrator.config.agents.orchestrators[role];
|
|
1828
1882
|
const basePath = ["agents", "orchestrators", role];
|
|
1829
1883
|
const choice = await selectOption(ctx, label, [
|
|
1830
|
-
opt(`Model: ${current.model}`, "
|
|
1831
|
-
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"),
|
|
1832
1886
|
...buildResetOptions(orchestrator, basePath),
|
|
1833
1887
|
opt("Back", "Return to the previous menu"),
|
|
1834
1888
|
]);
|
|
@@ -1874,8 +1928,8 @@ async function showSimpleSubagentEditor(
|
|
|
1874
1928
|
const current = orchestrator.config.agents.subagents.simple[role];
|
|
1875
1929
|
const basePath = ["agents", "subagents", "simple", role];
|
|
1876
1930
|
const choice = await selectOption(ctx, label, [
|
|
1877
|
-
opt(`Model: ${current.model}`, "
|
|
1878
|
-
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"),
|
|
1879
1933
|
...buildResetOptions(orchestrator, basePath),
|
|
1880
1934
|
opt("Back", "Return to the previous menu"),
|
|
1881
1935
|
]);
|
|
@@ -1976,8 +2030,8 @@ async function showPresetVariantEditor(
|
|
|
1976
2030
|
const variantPath = ["agents", "subagents", "presetGroups", group, "presets", presetName, "agents", variantName];
|
|
1977
2031
|
const options: OptionInput[] = [
|
|
1978
2032
|
opt(`Enabled: ${isEnabled(variant) ? "Yes" : "No"}`, "Toggle enabled state"),
|
|
1979
|
-
opt(`Model: ${variant.model}`, "
|
|
1980
|
-
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"),
|
|
1981
2035
|
];
|
|
1982
2036
|
if (getOwnedScopes(orchestrator, variantPath).length > 0) {
|
|
1983
2037
|
options.push(opt("Delete", "Delete this agent override"));
|
|
@@ -2029,7 +2083,7 @@ async function showPresetAgentsMenu(
|
|
|
2029
2083
|
options.push(opt(title, `${variant.model} / ${thinkingLabel(variant.thinking)}`));
|
|
2030
2084
|
byTitle.set(title, variantName);
|
|
2031
2085
|
}
|
|
2032
|
-
options.push(opt("New agent", "Add
|
|
2086
|
+
options.push(opt("New agent", "Add an agent variant to this preset"));
|
|
2033
2087
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2034
2088
|
const choice = await selectOption(ctx, "Agents", options);
|
|
2035
2089
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2198,7 +2252,7 @@ async function showPresetSettings(
|
|
|
2198
2252
|
options.push(opt(optionTitle, enabledPresetSummary(preset.agents)));
|
|
2199
2253
|
byTitle.set(optionTitle, presetName);
|
|
2200
2254
|
}
|
|
2201
|
-
options.push(opt("New preset", "Create preset"));
|
|
2255
|
+
options.push(opt("New preset", "Create a preset in this group"));
|
|
2202
2256
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2203
2257
|
const choice = await selectOption(ctx, title, options);
|
|
2204
2258
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2272,7 +2326,7 @@ async function showAfterEditCommands(orchestrator: Orchestrator, ctx: any): Prom
|
|
|
2272
2326
|
options.push(opt(`${title}${enabledTag}`, `${globsCount} patterns — ${cmd.run}`));
|
|
2273
2327
|
byTitle.set(`${title}${enabledTag}`, id);
|
|
2274
2328
|
}
|
|
2275
|
-
options.push(opt("New command", "Add command"));
|
|
2329
|
+
options.push(opt("New command", "Add a command to run after a file edit"));
|
|
2276
2330
|
options.push(...buildResetOptions(orchestrator, ["commands", "afterEdit"]));
|
|
2277
2331
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2278
2332
|
const choice = await selectOption(ctx, `After file edit: ${entries.length} commands`, options);
|
|
@@ -2401,7 +2455,7 @@ async function showAfterImplementCommands(orchestrator: Orchestrator, ctx: any):
|
|
|
2401
2455
|
options.push(opt(title, cmd.run));
|
|
2402
2456
|
byTitle.set(title, id);
|
|
2403
2457
|
}
|
|
2404
|
-
options.push(opt("New command", "Add command"));
|
|
2458
|
+
options.push(opt("New command", "Add a command to run after implementation"));
|
|
2405
2459
|
options.push(...buildResetOptions(orchestrator, ["commands", "afterImplement"]));
|
|
2406
2460
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2407
2461
|
const choice = await selectOption(ctx, `After implementation: ${entries.length} commands`, options);
|
|
@@ -2461,11 +2515,11 @@ async function showCommandsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2461
2515
|
const choice = await selectOption(ctx, "Commands", [
|
|
2462
2516
|
opt(
|
|
2463
2517
|
`After file edit: ${afterEditCount} commands`,
|
|
2464
|
-
|
|
2518
|
+
"Commands run when a matching file is edited (e.g. format, lint)",
|
|
2465
2519
|
),
|
|
2466
2520
|
opt(
|
|
2467
2521
|
`After implementation: ${afterImplementCount} commands`,
|
|
2468
|
-
|
|
2522
|
+
"Commands run once the implement phase completes (e.g. build, test)",
|
|
2469
2523
|
),
|
|
2470
2524
|
opt("Back", "Return to the previous menu"),
|
|
2471
2525
|
]);
|
|
@@ -2496,6 +2550,11 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2496
2550
|
path: ["performance", "internals", "subagentStale"],
|
|
2497
2551
|
value: orchestrator.config.performance.internals.subagentStale,
|
|
2498
2552
|
},
|
|
2553
|
+
{
|
|
2554
|
+
key: "performance.internals.mainTurnStale",
|
|
2555
|
+
path: ["performance", "internals", "mainTurnStale"],
|
|
2556
|
+
value: orchestrator.config.performance.internals.mainTurnStale,
|
|
2557
|
+
},
|
|
2499
2558
|
{
|
|
2500
2559
|
key: "performance.internals.taskLockStale",
|
|
2501
2560
|
path: ["performance", "internals", "taskLockStale"],
|
|
@@ -2510,7 +2569,7 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2510
2569
|
const options: OptionInput[] = timeoutEntries.map((entry) => {
|
|
2511
2570
|
const value = entry.value;
|
|
2512
2571
|
const key = entry.key;
|
|
2513
|
-
return opt(`${TIMEOUT_LABELS[key]}: ${formatDuration(value)}`, "
|
|
2572
|
+
return opt(`${TIMEOUT_LABELS[key]}: ${formatDuration(value)}`, "Change this time limit");
|
|
2514
2573
|
});
|
|
2515
2574
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2516
2575
|
const choice = await selectOption(ctx, "Timeouts", options);
|
|
@@ -2548,7 +2607,7 @@ async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promi
|
|
|
2548
2607
|
async function showPerformanceSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2549
2608
|
while (true) {
|
|
2550
2609
|
const choice = await selectOption(ctx, "Performance", [
|
|
2551
|
-
opt("Timeouts", "
|
|
2610
|
+
opt("Timeouts", "Adjust per-operation time limits"),
|
|
2552
2611
|
opt("Back", "Return to the previous menu"),
|
|
2553
2612
|
]);
|
|
2554
2613
|
if (!choice || choice === "Back") return BACK;
|
|
@@ -2649,6 +2708,7 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2649
2708
|
while (true) {
|
|
2650
2709
|
const choice = await selectOption(ctx, "General", [
|
|
2651
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"),
|
|
2652
2712
|
opt(`Ignore configs from other repos: ${orchestrator.config.general.loadExtraRepoConfigs ? "No" : "Yes"}`, "Load only root repo config"),
|
|
2653
2713
|
opt(`Log level: ${logLevelLabel(orchestrator.config.general.logLevel)}`, "Logging verbosity"),
|
|
2654
2714
|
opt(`Tracing: ${orchestrator.config.general.tracing ? "Yes" : "No"}`, "Capture full session traces to .pp/logs/traces/"),
|
|
@@ -2660,6 +2720,10 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2660
2720
|
await showBooleanSetting(orchestrator, ctx, "Commit automatically", ["general", "autoCommit"], "Commit changes automatically as work progresses", "Leave committing to you");
|
|
2661
2721
|
continue;
|
|
2662
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");
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2663
2727
|
if (choice.startsWith("Ignore configs from other repos:")) {
|
|
2664
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");
|
|
2665
2729
|
continue;
|
|
@@ -2763,7 +2827,7 @@ async function showSettingsMenu(orchestrator: Orchestrator, ctx: any): Promise<t
|
|
|
2763
2827
|
opt("General", "Commit, log level, Flant AI, repos"),
|
|
2764
2828
|
opt("Agents", "Orchestrator and subagent configuration"),
|
|
2765
2829
|
opt("Commands", "After file edit and after implementation"),
|
|
2766
|
-
opt("Performance", "
|
|
2830
|
+
opt("Performance", "Per-operation timeout limits"),
|
|
2767
2831
|
opt("LSP", "Language server controls"),
|
|
2768
2832
|
opt("Back", "Return to the previous menu"),
|
|
2769
2833
|
];
|
|
@@ -2855,10 +2919,10 @@ async function showAutonomousPhaseSettings(
|
|
|
2855
2919
|
const reviewPreset = phaseConfig.reviewPreset ?? defaultAutonomousReviewPreset(type, phase);
|
|
2856
2920
|
const maxReview = phaseConfig.maxReviewPasses >= 999 ? "No limit" : String(phaseConfig.maxReviewPasses);
|
|
2857
2921
|
const options: OptionInput[] = [
|
|
2858
|
-
opt(`Review preset: ${reviewPreset}`, "
|
|
2922
|
+
opt(`Review preset: ${reviewPreset}`, "Pick which reviewer group runs in this phase"),
|
|
2859
2923
|
];
|
|
2860
2924
|
if (phase === "plan") {
|
|
2861
|
-
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"));
|
|
2862
2926
|
}
|
|
2863
2927
|
options.push(opt(`Max review passes: ${maxReview}`, "Safety cap for autonomous review loops"));
|
|
2864
2928
|
options.push(opt("Back", "Return to autonomous settings"));
|
|
@@ -3154,175 +3218,118 @@ async function openCodeReviewInPlannotator(
|
|
|
3154
3218
|
return { status: result.approved ? "approved" : "needs_changes", feedback };
|
|
3155
3219
|
}
|
|
3156
3220
|
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
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);
|
|
3175
3304
|
}
|
|
3176
|
-
|
|
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;
|
|
3177
3310
|
}
|
|
3178
3311
|
|
|
3179
3312
|
async function showReviewMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
3180
3313
|
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" },
|
|
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" },
|
|
3187
3316
|
{ title: "Resume", description: "Resume a previously unfinished review" },
|
|
3188
3317
|
{ title: "Back", description: "Return to the previous menu" },
|
|
3189
|
-
];
|
|
3190
|
-
const choice = await selectOption(ctx, "Review", options);
|
|
3318
|
+
]);
|
|
3191
3319
|
if (!choice || choice === "Back") return BACK;
|
|
3192
3320
|
|
|
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");
|
|
3321
|
+
if (choice === "New") {
|
|
3312
3322
|
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3313
3323
|
if (!modeSelection) continue;
|
|
3314
|
-
|
|
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";
|
|
3315
3329
|
}
|
|
3316
3330
|
|
|
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);
|
|
3331
|
+
const result = await showResumeMenu(orchestrator, ctx, "review", "No paused review tasks found.");
|
|
3332
|
+
if (result === "started") return result;
|
|
3326
3333
|
}
|
|
3327
3334
|
}
|
|
3328
3335
|
|
|
@@ -3416,7 +3423,7 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
|
|
|
3416
3423
|
const choice = await selectOption(ctx, "/pp", [
|
|
3417
3424
|
{ title: "Task", description: "Start a new task or resume a paused one" },
|
|
3418
3425
|
{ title: "Info", description: "Subagents, usage, and task status" },
|
|
3419
|
-
{ title: "Settings", description: "
|
|
3426
|
+
{ title: "Settings", description: "Models, agents, commands, and other configuration" },
|
|
3420
3427
|
{ title: "Back", description: "Close this menu" },
|
|
3421
3428
|
]);
|
|
3422
3429
|
if (!choice || choice === "Back") return undefined;
|
|
@@ -3477,7 +3484,7 @@ async function showQuickTaskMenu(
|
|
|
3477
3484
|
opt("Complete", "Mark task as done and clean up"),
|
|
3478
3485
|
opt("Pause", "Suspend task to resume later"),
|
|
3479
3486
|
opt("Info", "Subagents, usage, and task status"),
|
|
3480
|
-
opt("Settings", "
|
|
3487
|
+
opt("Settings", "Models, agents, commands, and other configuration"),
|
|
3481
3488
|
opt("Back", "Return to the prompt and keep working"),
|
|
3482
3489
|
]);
|
|
3483
3490
|
// A deliberate ESC in tool mode must stop the turn cleanly (mirror the
|
|
@@ -3506,6 +3513,11 @@ export async function showActiveTaskMenu(
|
|
|
3506
3513
|
ctx: any,
|
|
3507
3514
|
summary: string,
|
|
3508
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,
|
|
3509
3521
|
): Promise<string> {
|
|
3510
3522
|
const continueMessage = advanceBanner("[PI-PI] User wants to continue. Run /pp when ready to advance.");
|
|
3511
3523
|
|
|
@@ -3516,6 +3528,15 @@ export async function showActiveTaskMenu(
|
|
|
3516
3528
|
if (task.type === "quick") {
|
|
3517
3529
|
return showQuickTaskMenu(orchestrator, ctx, summary, mode);
|
|
3518
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
|
+
}
|
|
3519
3540
|
const phase = task.state.phase;
|
|
3520
3541
|
const step = task.state.step;
|
|
3521
3542
|
const effectiveMode = getEffectivePhaseMode(task.state);
|
|
@@ -3527,15 +3548,21 @@ export async function showActiveTaskMenu(
|
|
|
3527
3548
|
const { autoLabel } = getReviewLabels(orchestrator);
|
|
3528
3549
|
const isReviewPhase = phase === "review";
|
|
3529
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";
|
|
3530
3557
|
|
|
3531
3558
|
const opt = (title: string, description: string): OptionInput => ({ title, description });
|
|
3532
3559
|
|
|
3533
|
-
if (effectiveMode === "autonomous") {
|
|
3560
|
+
if (effectiveMode === "autonomous" && !forceGuided) {
|
|
3534
3561
|
const { choice: autoChoice, cancelReason } = await selectOptionCancelable(ctx, `/pp\n\nTask: ${task.type}\nPhase: ${phase}${summary !== "/pp" ? `\n\n${summary}` : ""}`, [
|
|
3535
3562
|
opt("Complete task", "Mark task as done and clean up"),
|
|
3536
3563
|
opt("Pause task", "Suspend task to resume later"),
|
|
3537
3564
|
opt("Info", "Subagents, usage, and task status"),
|
|
3538
|
-
opt("Settings", "
|
|
3565
|
+
opt("Settings", "Models, agents, commands, and other configuration"),
|
|
3539
3566
|
opt("Back", "Return to the prompt and keep working"),
|
|
3540
3567
|
]);
|
|
3541
3568
|
// A deliberate ESC only needs distinct handling in tool mode (so
|
|
@@ -3562,10 +3589,10 @@ export async function showActiveTaskMenu(
|
|
|
3562
3589
|
const options: OptionInput[] = [];
|
|
3563
3590
|
options.push(opt("Next", "Complete, pause, or continue to next phase"));
|
|
3564
3591
|
if (!waiting) {
|
|
3565
|
-
options.push(opt("Review", "
|
|
3592
|
+
options.push(opt("Review", `Review ${reviewTarget}: automated reviewers${hasPlannotator ? ", Plannotator, or" : " or"} your own editor pass`));
|
|
3566
3593
|
}
|
|
3567
3594
|
options.push(opt("Info", "Subagents, usage, and task status"));
|
|
3568
|
-
options.push(opt("Settings", "
|
|
3595
|
+
options.push(opt("Settings", "Models, agents, commands, and other configuration"));
|
|
3569
3596
|
options.push(opt("Back", "Return to the prompt and keep working"));
|
|
3570
3597
|
|
|
3571
3598
|
const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
|
|
@@ -3590,70 +3617,109 @@ export async function showActiveTaskMenu(
|
|
|
3590
3617
|
}
|
|
3591
3618
|
|
|
3592
3619
|
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
|
-
|
|
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
|
+
}
|
|
3626
3675
|
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
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
|
+
}
|
|
3633
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 "";
|
|
3634
3690
|
}
|
|
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 "";
|
|
3691
|
+
continue;
|
|
3641
3692
|
}
|
|
3642
3693
|
|
|
3643
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
|
+
}
|
|
3644
3708
|
const reviewOptions: OptionInput[] = [
|
|
3645
|
-
opt(autoLabel,
|
|
3709
|
+
opt(autoLabel, `Run configured reviewers over ${reviewTarget}`),
|
|
3646
3710
|
];
|
|
3647
3711
|
if (hasPlannotator) {
|
|
3648
3712
|
reviewOptions.push(opt("Review in Plannotator", phase === "plan" ? "Open plan review in browser" : "Open code diff review in browser"));
|
|
3649
3713
|
}
|
|
3650
3714
|
reviewOptions.push(opt("Review on my own", phase === "implement"
|
|
3651
|
-
? "
|
|
3652
|
-
:
|
|
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"));
|
|
3653
3719
|
reviewOptions.push(opt("Back", "Return to the previous menu"));
|
|
3654
3720
|
|
|
3655
3721
|
const reviewChoice = await selectOption(ctx, "Review", reviewOptions);
|
|
3656
|
-
if (!reviewChoice || reviewChoice === "Back")
|
|
3722
|
+
if (!reviewChoice || reviewChoice === "Back") break;
|
|
3657
3723
|
|
|
3658
3724
|
if (reviewChoice === "Review in Plannotator") {
|
|
3659
3725
|
if (phase === "plan") {
|
|
@@ -3666,119 +3732,50 @@ export async function showActiveTaskMenu(
|
|
|
3666
3732
|
return handled.text ?? text;
|
|
3667
3733
|
}
|
|
3668
3734
|
const allRepos = getRegisteredRepos(orchestrator);
|
|
3669
|
-
const
|
|
3670
|
-
const repos: RepoInfo[] = [];
|
|
3735
|
+
const eligible: string[] = [];
|
|
3671
3736
|
for (const repo of allRepos) {
|
|
3672
3737
|
const base = await detectDefaultBranch(orchestrator, allRepos, repo.path);
|
|
3673
3738
|
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
|
-
}
|
|
3739
|
+
if (error || changed) eligible.push(repo.path);
|
|
3680
3740
|
}
|
|
3681
|
-
if (
|
|
3741
|
+
if (eligible.length === 0) {
|
|
3682
3742
|
ctx.ui.notify("No registered repositories have changes to review.", "info");
|
|
3683
3743
|
continue;
|
|
3684
3744
|
}
|
|
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
|
-
}
|
|
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;
|
|
3747
3753
|
continue;
|
|
3748
3754
|
}
|
|
3749
3755
|
|
|
3750
3756
|
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
3757
|
const gate = await selectOption(ctx, "Editor review", [
|
|
3761
3758
|
opt("Done", "I've added AI_REVIEW: markers and saved my files"),
|
|
3762
3759
|
opt("Skip markers", "Continue without the marker workflow"),
|
|
3763
3760
|
opt("Back", "Return to the review menu"),
|
|
3764
3761
|
]);
|
|
3765
3762
|
if (!gate || gate === "Back") continue;
|
|
3766
|
-
setStep(orchestrator, "llm_work");
|
|
3763
|
+
setStep(orchestrator, phase === "plan" ? "synthesize" : "llm_work");
|
|
3767
3764
|
if (gate === "Skip markers") {
|
|
3768
3765
|
return continueMessage;
|
|
3769
3766
|
}
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
);
|
|
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
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
return readOnlyReviewBanner(phase, task.dir);
|
|
3782
3779
|
}
|
|
3783
3780
|
|
|
3784
3781
|
const reviewPreset = await pickPreset(ctx, orchestrator, getReviewPresetGroup(phase), "Review preset");
|
|
@@ -3795,6 +3792,7 @@ export async function showActiveTaskMenu(
|
|
|
3795
3792
|
const handled = handleReviewResult(ctx, text);
|
|
3796
3793
|
if (handled.continueLoop) continue;
|
|
3797
3794
|
return handled.text ?? text;
|
|
3795
|
+
}
|
|
3798
3796
|
}
|
|
3799
3797
|
}
|
|
3800
3798
|
}
|