@ilya-lesikov/pi-pi 0.10.0 → 0.12.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-subagents/package.json +0 -1
- package/3p/pi-subagents/src/agent-manager.ts +1 -0
- package/3p/pi-subagents/src/index.ts +1 -0
- package/3p/pi-subagents/src/types.ts +8 -0
- package/extensions/orchestrator/agents/advisor.ts +2 -1
- package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/constraints.test.ts +43 -10
- package/extensions/orchestrator/agents/constraints.ts +26 -5
- package/extensions/orchestrator/agents/deep-debugger.ts +7 -6
- package/extensions/orchestrator/agents/explore.ts +1 -1
- package/extensions/orchestrator/agents/librarian.ts +1 -1
- package/extensions/orchestrator/agents/planner.ts +1 -1
- package/extensions/orchestrator/agents/prompts.test.ts +135 -1
- package/extensions/orchestrator/agents/reviewer.ts +2 -2
- package/extensions/orchestrator/agents/task.ts +6 -2
- package/extensions/orchestrator/agents/tool-routing.ts +22 -6
- package/extensions/orchestrator/assumptions.test.ts +77 -0
- package/extensions/orchestrator/assumptions.ts +85 -0
- package/extensions/orchestrator/cbm.ts +4 -1
- package/extensions/orchestrator/cbm.which.test.ts +92 -0
- package/extensions/orchestrator/command-handlers.ts +9 -1
- package/extensions/orchestrator/config.test.ts +1 -3
- package/extensions/orchestrator/config.ts +3 -4
- package/extensions/orchestrator/context.test.ts +2 -7
- package/extensions/orchestrator/context.ts +3 -3
- package/extensions/orchestrator/doctor.test.ts +35 -0
- package/extensions/orchestrator/doctor.ts +4 -2
- package/extensions/orchestrator/event-handlers.more.test.ts +185 -0
- package/extensions/orchestrator/event-handlers.test.ts +22 -2
- package/extensions/orchestrator/event-handlers.ts +156 -18
- package/extensions/orchestrator/flant-infra.more.test.ts +55 -0
- package/extensions/orchestrator/flant-infra.test.ts +0 -3
- package/extensions/orchestrator/flant-infra.ts +17 -7
- package/extensions/orchestrator/integration.test.ts +355 -173
- package/extensions/orchestrator/orchestrator.ts +3 -9
- package/extensions/orchestrator/phases/brainstorm.test.ts +25 -7
- package/extensions/orchestrator/phases/brainstorm.ts +35 -71
- package/extensions/orchestrator/phases/implementation.test.ts +22 -0
- package/extensions/orchestrator/phases/implementation.ts +6 -0
- package/extensions/orchestrator/phases/machine.test.ts +0 -28
- package/extensions/orchestrator/phases/machine.ts +0 -38
- package/extensions/orchestrator/phases/planning.test.ts +27 -1
- package/extensions/orchestrator/phases/planning.ts +1 -1
- package/extensions/orchestrator/phases/review-task.test.ts +7 -0
- package/extensions/orchestrator/phases/review-task.ts +1 -1
- package/extensions/orchestrator/phases/review.test.ts +47 -10
- package/extensions/orchestrator/phases/review.ts +36 -25
- package/extensions/orchestrator/pp-menu.from.test.ts +65 -0
- package/extensions/orchestrator/pp-menu.leaves.test.ts +17 -0
- package/extensions/orchestrator/pp-menu.test.ts +106 -49
- package/extensions/orchestrator/pp-menu.ts +280 -134
- package/extensions/orchestrator/pp-state-tools.ts +1 -1
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +2 -2
- package/extensions/orchestrator/rate-limit-fallback.ts +1 -1
- package/extensions/orchestrator/state.test.ts +17 -17
- package/extensions/orchestrator/state.ts +35 -12
- package/package.json +6 -3
- package/scripts/lib/smoke-resolve.mjs +99 -0
- package/scripts/postinstall.mjs +93 -0
- package/scripts/test-package.sh +62 -0
- package/scripts/postinstall.sh +0 -18
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "fs";
|
|
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";
|
|
@@ -32,6 +32,7 @@ import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle, isReviewCyc
|
|
|
32
32
|
import { Orchestrator } from "./orchestrator.js";
|
|
33
33
|
import { cancelPendingPlannotatorWait, openPlannotator, openAnnotateReview, waitForPlannotatorResult } from "./plannotator.js";
|
|
34
34
|
import { advanceBanner } from "./messages.js";
|
|
35
|
+
import { openAssumptionsBanner, terminalAssumptionsSummary } from "./assumptions.js";
|
|
35
36
|
import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
|
|
36
37
|
import { spawnCodeReviewers } from "./phases/review.js";
|
|
37
38
|
import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
@@ -246,6 +247,53 @@ function setStep(orchestrator: Orchestrator, step: string): void {
|
|
|
246
247
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
// Pause/finish abort the agent, so they cannot reconcile in-line; flag pending so
|
|
251
|
+
// the next resume re-prompts. Set unconditionally (not gated on reconciledPhase):
|
|
252
|
+
// work done AFTER a phase was reconciled would otherwise resume from a stale stamp.
|
|
253
|
+
function flagReconcilePending(state: { phase: string; reconcilePending?: boolean }): void {
|
|
254
|
+
if (state.phase !== "quick") {
|
|
255
|
+
state.reconcilePending = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Manual /pp → Review launches reviewer subagents while the main agent is idle, so
|
|
260
|
+
// the pp_phase_complete reconcile gate never ran and cannot run (no live turn to
|
|
261
|
+
// prompt). Reviewers read the on-disk state files at spawn, which may lag the
|
|
262
|
+
// agent's latest work. Guard just those synchronous automated-review launches with
|
|
263
|
+
// a confirm; the guided flow reaches Review right after pp_phase_complete
|
|
264
|
+
// reconciled (reconciledPhase === phase, not pending), so it never prompts.
|
|
265
|
+
async function confirmReviewDespiteStaleState(orchestrator: Orchestrator, ctx: any): Promise<boolean> {
|
|
266
|
+
const state = orchestrator.active?.state;
|
|
267
|
+
if (!state) return true;
|
|
268
|
+
const stale = state.reconcilePending || state.reconciledPhase !== state.phase;
|
|
269
|
+
if (!stale) return true;
|
|
270
|
+
const choice = await selectOption(ctx, "Reviewers will use the current on-disk state files, which may not include the agent's latest findings. Continue?", [
|
|
271
|
+
opt("Review anyway", "Spawn reviewers against the on-disk state files"),
|
|
272
|
+
opt("Cancel", "Return to the menu without reviewing"),
|
|
273
|
+
]);
|
|
274
|
+
if (choice !== "Review anyway") return false;
|
|
275
|
+
// The manual bypass must not count as a fresh reconciliation: keep it pending so
|
|
276
|
+
// the next live-agent pp_phase_complete re-prompts.
|
|
277
|
+
state.reconcilePending = true;
|
|
278
|
+
saveTask(orchestrator.active!.dir, state);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Leaving a Plannotator/review flow (stop, exhaust, or cancel) must return the
|
|
283
|
+
// task's step to an interactive value. Otherwise a step left at "await_reviewers"
|
|
284
|
+
// keeps transitionController.isRunning() false, which hides the Review/Continue
|
|
285
|
+
// actions in /pp and starves the idle watchdog until it stalls (the reported
|
|
286
|
+
// post-"Stop reviewing" deadlock). Also drop any pending Plannotator wait so a
|
|
287
|
+
// stale reject/timeout can't fire after the user has moved on. The caller owns
|
|
288
|
+
// the subsequent saveTask.
|
|
289
|
+
export function restoreMenuStepAfterReview(orchestrator: Orchestrator, task: { state: { phase: string; step: string | null } }): void {
|
|
290
|
+
cancelPendingPlannotatorWait(orchestrator);
|
|
291
|
+
const step = task.state.step;
|
|
292
|
+
if (step === "await_reviewers" || step === "await_planners" || step === null) {
|
|
293
|
+
task.state.step = task.state.phase === "plan" ? "synthesize" : "llm_work";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
249
297
|
// Publish the latest synthesized review findings to a target. The agent (not the
|
|
250
298
|
// extension) performs the writes: file comments = insert `AI_COMMENT:` markers at
|
|
251
299
|
// each finding's line; PR comments = run `gh` to post line-anchored comments from
|
|
@@ -277,11 +325,11 @@ export function publishPrCommentsBanner(taskDir: string): string {
|
|
|
277
325
|
"For each registered repo, resolve the branch's PR with `gh pr view --json number,headRefName,headRefOid,url` (skip the repo if there is no PR or `gh auth status` fails). Derive owner/repo from the PR `url` (the base repo).\n\n" +
|
|
278
326
|
"PRE-VALIDATE each anchor against the PR diff BEFORE building the review: fetch the diff (e.g. `gh pr diff <number>`) and keep, in a `comments` array, ONLY findings whose path+line are part of that diff. A single GitHub review is all-or-nothing — one invalid line comment rejects the WHOLE review — so an unvalidated anchor must NEVER go into `comments`.\n\n" +
|
|
279
327
|
"Post ONE bundled review per repo via `gh api --method POST repos/<owner>/<repo>/pulls/<number>/reviews` with `commit_id=<headRefOid>`, `event=COMMENT` (neutral — never APPROVE or REQUEST_CHANGES), a summary `body`, and a `comments` array of `{path, line, side: RIGHT, body}` for the validated findings.\n\n" +
|
|
280
|
-
"NON-DIFF findings (anchors not in the diff) are NEVER dropped: list them in the review `body` under a `Findings not anchorable to the diff:` heading,
|
|
281
|
-
"The
|
|
328
|
+
"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, each finding as its own block that ends with a blank line then the exact `(generated by pi-pi)` footer on its own line, using `file:—` (never an invented line). Do NOT rely on GitHub rejecting them.\n\n" +
|
|
329
|
+
"The comment body of every finding MUST end with a blank line followed by exactly:\n(generated by pi-pi)\n(i.e. there is always an empty line immediately before that footer, separating it from the finding text).\n\n" +
|
|
282
330
|
PRIVACY_INSTRUCTION + "\n\n" +
|
|
283
331
|
"IDEMPOTENT (line comments): first list existing PR comments (`gh api repos/<owner>/<repo>/pulls/<number>/comments`, which also returns comments from earlier per-comment publishes) and do NOT re-post a finding whose path, line, `(generated by pi-pi)` footer, AND body text all match one already present. Two distinct findings on the same line are NOT duplicates. Skip only true duplicates.\n\n" +
|
|
284
|
-
"IDEMPOTENT (body findings): also list existing reviews (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`) and read their `body`. A GitHub review body cannot be deduped per-line, so before creating a new review, drop any non-diff finding whose `<file:—> — <text
|
|
332
|
+
"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>` block (matched by its file, text, and `(generated by pi-pi)` footer, ignoring blank-line spacing) already appears in a prior pi-pi review body. If, after that, there are NO new line comments AND NO new body findings for a repo, do NOT create an empty duplicate review — report it as fully-published instead.\n\n" +
|
|
285
333
|
"When done, report per repo how many comments you posted, how many you skipped as duplicates, and how many were unanchorable (in the body), then end your turn.",
|
|
286
334
|
);
|
|
287
335
|
}
|
|
@@ -306,7 +354,7 @@ const AI_REVIEW_MARKER_LOOP =
|
|
|
306
354
|
"repeat until no `AI_REVIEW:` markers remain. Then verify your work and report what you changed per marker. When complete, " +
|
|
307
355
|
"call pp_phase_complete.";
|
|
308
356
|
|
|
309
|
-
// Read-only phases (brainstorm/
|
|
357
|
+
// Read-only phases (brainstorm/review) and plan produce markdown state files rather than
|
|
310
358
|
// source changes, so their AI_REVIEW scan targets those artifacts. Missing targets are skipped,
|
|
311
359
|
// not errors. Kept as literal-token guidance (no regexp/parser), mirroring the implement banner.
|
|
312
360
|
function readOnlyReviewBanner(phase: string, taskDir: string): string {
|
|
@@ -355,6 +403,8 @@ async function abortCurrentWork(orchestrator: Orchestrator, ctx: any): Promise<v
|
|
|
355
403
|
async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string> {
|
|
356
404
|
if (!orchestrator.active) return "No active task.";
|
|
357
405
|
|
|
406
|
+
flagReconcilePending(orchestrator.active.state);
|
|
407
|
+
|
|
358
408
|
cancelPendingPlannotatorWait(orchestrator);
|
|
359
409
|
orchestrator.abortAllSubagents();
|
|
360
410
|
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
@@ -398,6 +448,8 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
398
448
|
return MISSING_FINAL_PASS_ANCHORS;
|
|
399
449
|
}
|
|
400
450
|
|
|
451
|
+
flagReconcilePending(orchestrator.active.state);
|
|
452
|
+
|
|
401
453
|
cancelPendingPlannotatorWait(orchestrator);
|
|
402
454
|
orchestrator.abortAllSubagents();
|
|
403
455
|
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
@@ -406,6 +458,7 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
406
458
|
const name = orchestrator.active.description;
|
|
407
459
|
const type = orchestrator.active.type;
|
|
408
460
|
const dir = orchestrator.active.dir;
|
|
461
|
+
const wasAutonomous = getEffectiveMode(orchestrator.active.state) === "autonomous";
|
|
409
462
|
|
|
410
463
|
orchestrator.lastCtx = ctx;
|
|
411
464
|
|
|
@@ -432,10 +485,16 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
432
485
|
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.`,
|
|
433
486
|
});
|
|
434
487
|
|
|
488
|
+
// Terminal assumptions summary (#B/e): mirror the automatic done transition so a
|
|
489
|
+
// /pp → Complete finish of an autonomous run also lists recorded assumptions.
|
|
490
|
+
if (wasAutonomous) {
|
|
491
|
+
ctx.ui.notify(terminalAssumptionsSummary(dir), "info");
|
|
492
|
+
}
|
|
493
|
+
|
|
435
494
|
const urExists = existsSync(join(dir, "USER_REQUEST.md"));
|
|
436
495
|
const resExists = existsSync(join(dir, "RESEARCH.md"));
|
|
437
496
|
|
|
438
|
-
if (
|
|
497
|
+
if (type === "brainstorm" && urExists && resExists) {
|
|
439
498
|
const taskRelPath = relative(join(orchestrator.cwd, ".pp", "state"), dir);
|
|
440
499
|
ctx.ui.notify(
|
|
441
500
|
`Task "${name}" completed. Artifacts saved.\nUse /pp → Implement → From and choose ${taskRelPath}`,
|
|
@@ -499,7 +558,7 @@ export async function pickPreset(
|
|
|
499
558
|
// phase, which is wrong only for tasks completed early).
|
|
500
559
|
function reopenPhaseForDoneTask(type: TaskType, state: TaskState): Phase {
|
|
501
560
|
if (state.completedFrom) return state.completedFrom;
|
|
502
|
-
for (const phase of ["implement", "plan", "review", "
|
|
561
|
+
for (const phase of ["implement", "plan", "review", "brainstorm", "quick"] as Phase[]) {
|
|
503
562
|
if (nextPhase(type, phase) === "done") return phase;
|
|
504
563
|
}
|
|
505
564
|
return "implement" as Phase;
|
|
@@ -597,8 +656,7 @@ export async function resumeTask(
|
|
|
597
656
|
getLogger().info({ s: "task", dir: task.dir, type: task.type, phase: task.state.phase }, "task resumed");
|
|
598
657
|
|
|
599
658
|
const modelConfig = orchestrator.config.agents.orchestrators[
|
|
600
|
-
task.type === "
|
|
601
|
-
: task.type === "brainstorm" ? "brainstorm"
|
|
659
|
+
task.type === "brainstorm" ? "brainstorm"
|
|
602
660
|
: task.type === "review" ? "review"
|
|
603
661
|
: "implement"
|
|
604
662
|
];
|
|
@@ -826,15 +884,14 @@ export async function resumeTask(
|
|
|
826
884
|
return { ok: true };
|
|
827
885
|
}
|
|
828
886
|
|
|
829
|
-
function listCompletedFromTasks(cwd: string): TaskInfo[] {
|
|
887
|
+
export function listCompletedFromTasks(cwd: string): TaskInfo[] {
|
|
830
888
|
const paused = new Set<string>([
|
|
831
889
|
...listTasks(cwd, "brainstorm").map((t) => t.dir),
|
|
832
|
-
...listTasks(cwd, "debug").map((t) => t.dir),
|
|
833
890
|
...listTasks(cwd, "review").map((t) => t.dir),
|
|
834
891
|
]);
|
|
835
892
|
const results: TaskInfo[] = [];
|
|
836
893
|
|
|
837
|
-
for (const type of ["brainstorm", "
|
|
894
|
+
for (const type of ["brainstorm", "review"] as TaskType[]) {
|
|
838
895
|
const typeDir = join(cwd, ".pp", "state", type);
|
|
839
896
|
if (!existsSync(typeDir)) continue;
|
|
840
897
|
for (const entry of readdirSync(typeDir, { withFileTypes: true })) {
|
|
@@ -852,6 +909,27 @@ function listCompletedFromTasks(cwd: string): TaskInfo[] {
|
|
|
852
909
|
}
|
|
853
910
|
}
|
|
854
911
|
|
|
912
|
+
// Legacy .pp/state/debug/ tasks predate the removal of the debug task type, so
|
|
913
|
+
// their state.json cannot pass loadTask (its phase is a now-unsupported value).
|
|
914
|
+
// Read state.json directly so done debug tasks with artifacts stay reachable.
|
|
915
|
+
const debugDir = join(cwd, ".pp", "state", "debug");
|
|
916
|
+
if (existsSync(debugDir)) {
|
|
917
|
+
for (const entry of readdirSync(debugDir, { withFileTypes: true })) {
|
|
918
|
+
if (!entry.isDirectory()) continue;
|
|
919
|
+
const dir = join(debugDir, entry.name);
|
|
920
|
+
const sp = join(dir, "state.json");
|
|
921
|
+
if (!existsSync(sp)) continue;
|
|
922
|
+
try {
|
|
923
|
+
const state = JSON.parse(readFileSync(sp, "utf-8")) as TaskState;
|
|
924
|
+
if (state.phase !== "done") continue;
|
|
925
|
+
if (!existsSync(join(dir, "USER_REQUEST.md")) || !existsSync(join(dir, "RESEARCH.md"))) continue;
|
|
926
|
+
results.push({ dir, state, type: "implement" });
|
|
927
|
+
} catch {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
855
933
|
results.sort((a, b) => {
|
|
856
934
|
const aTime = a.state.startedAt ? new Date(a.state.startedAt).getTime() : 0;
|
|
857
935
|
const bTime = b.state.startedAt ? new Date(b.state.startedAt).getTime() : 0;
|
|
@@ -892,7 +970,6 @@ function collectRoleAssignments(config: Partial<PiPiConfig> | null): string[] {
|
|
|
892
970
|
|
|
893
971
|
add("agents.orchestrators.implement", config.agents?.orchestrators?.implement?.model);
|
|
894
972
|
add("agents.orchestrators.plan", config.agents?.orchestrators?.plan?.model);
|
|
895
|
-
add("agents.orchestrators.debug", config.agents?.orchestrators?.debug?.model);
|
|
896
973
|
add("agents.orchestrators.brainstorm", config.agents?.orchestrators?.brainstorm?.model);
|
|
897
974
|
add("agents.orchestrators.review", config.agents?.orchestrators?.review?.model);
|
|
898
975
|
|
|
@@ -1094,7 +1171,7 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
1094
1171
|
}
|
|
1095
1172
|
|
|
1096
1173
|
if (choice === "Update now") {
|
|
1097
|
-
const result = await updateFlantInfra(orchestrator.pi);
|
|
1174
|
+
const result = await updateFlantInfra(orchestrator.pi, { force: true });
|
|
1098
1175
|
const message = describeUpdateResult(result);
|
|
1099
1176
|
ctx.ui.notify(message.text, message.kind);
|
|
1100
1177
|
continue;
|
|
@@ -1341,7 +1418,6 @@ const ORCHESTRATOR_ROLES: Array<{ role: MainModelRole; label: string; descriptio
|
|
|
1341
1418
|
{ role: "brainstorm", label: "Brainstormer", description: "agents.orchestrators.brainstorm" },
|
|
1342
1419
|
{ role: "implement", label: "Implementer", description: "agents.orchestrators.implement" },
|
|
1343
1420
|
{ role: "plan", label: "Planner", description: "agents.orchestrators.plan" },
|
|
1344
|
-
{ role: "debug", label: "Debugger", description: "agents.orchestrators.debug" },
|
|
1345
1421
|
{ role: "review", label: "Reviewer", description: "agents.orchestrators.review" },
|
|
1346
1422
|
{ role: "quick", label: "Quick", description: "agents.orchestrators.quick" },
|
|
1347
1423
|
];
|
|
@@ -2924,7 +3000,7 @@ async function showMaxConcurrentSubagentsSetting(orchestrator: Orchestrator, ctx
|
|
|
2924
3000
|
if (!action || action === "Back") break;
|
|
2925
3001
|
if (action === "Edit") {
|
|
2926
3002
|
const value = await pickMaxConcurrentSubagents(ctx, current);
|
|
2927
|
-
if (value === null) continue;
|
|
3003
|
+
if (value === null || value === current) continue;
|
|
2928
3004
|
applyScopeChoice(orchestrator, MAX_CONCURRENT_SUBAGENTS_PATH, value, await pickScope(ctx, orchestrator));
|
|
2929
3005
|
continue;
|
|
2930
3006
|
}
|
|
@@ -2940,7 +3016,7 @@ async function pickMaxConcurrentSubagents(ctx: any, current: number): Promise<nu
|
|
|
2940
3016
|
);
|
|
2941
3017
|
if (input === undefined || input === null) return null;
|
|
2942
3018
|
const trimmed = String(input).trim();
|
|
2943
|
-
if (trimmed === "") return
|
|
3019
|
+
if (trimmed === "") return current;
|
|
2944
3020
|
if (!/^\d+$/.test(trimmed)) {
|
|
2945
3021
|
ctx.ui.notify(`Please enter a positive integer between 1 and ${MAX_CONCURRENT_SUBAGENTS_CEILING}.`, "warning");
|
|
2946
3022
|
continue;
|
|
@@ -3045,10 +3121,10 @@ async function showSettingsMenu(orchestrator: Orchestrator, ctx: any): Promise<t
|
|
|
3045
3121
|
}
|
|
3046
3122
|
}
|
|
3047
3123
|
|
|
3048
|
-
// First phases (brainstorm/
|
|
3124
|
+
// First phases (brainstorm/review) are always interactive and can never run
|
|
3049
3125
|
// autonomously — autonomous configs only ever cover plan/implement.
|
|
3050
3126
|
export function autonomousPhasesForTask(type: TaskType): string[] {
|
|
3051
|
-
if (type === "implement" || type === "
|
|
3127
|
+
if (type === "implement" || type === "review") return ["plan", "implement"];
|
|
3052
3128
|
return [];
|
|
3053
3129
|
}
|
|
3054
3130
|
|
|
@@ -3081,25 +3157,20 @@ async function showTaskModePicker(ctx: any): Promise<TaskMode | "back"> {
|
|
|
3081
3157
|
}
|
|
3082
3158
|
|
|
3083
3159
|
export async function pickMaxReviewPasses(ctx: any, current: number): Promise<number | null> {
|
|
3084
|
-
const currentLabel = current >= 999 ? "-" : String(current);
|
|
3160
|
+
const currentLabel = current >= 999 ? "-" : current === 0 ? "0 (disabled)" : String(current);
|
|
3085
3161
|
while (true) {
|
|
3086
3162
|
const input = await ctx.ui.input(
|
|
3087
|
-
`Max review passes (
|
|
3163
|
+
`Max review passes (0 to disable, a positive integer, or "-" for unlimited) [${currentLabel}]`,
|
|
3088
3164
|
);
|
|
3089
3165
|
if (input === undefined || input === null) return null;
|
|
3090
3166
|
const trimmed = String(input).trim();
|
|
3091
|
-
if (trimmed === "") return
|
|
3167
|
+
if (trimmed === "") return current;
|
|
3092
3168
|
if (trimmed === "-") return 999;
|
|
3093
3169
|
if (!/^\d+$/.test(trimmed)) {
|
|
3094
|
-
ctx.ui.notify('Please enter a positive integer, or "-" for unlimited.', "warning");
|
|
3170
|
+
ctx.ui.notify('Please enter 0 (disable), a positive integer, or "-" for unlimited.', "warning");
|
|
3095
3171
|
continue;
|
|
3096
3172
|
}
|
|
3097
|
-
|
|
3098
|
-
if (parsed <= 0) {
|
|
3099
|
-
ctx.ui.notify('Please enter a positive integer, or "-" for unlimited.', "warning");
|
|
3100
|
-
continue;
|
|
3101
|
-
}
|
|
3102
|
-
return parsed;
|
|
3173
|
+
return Number.parseInt(trimmed, 10);
|
|
3103
3174
|
}
|
|
3104
3175
|
}
|
|
3105
3176
|
|
|
@@ -3119,7 +3190,7 @@ async function showAutonomousPhaseSettings(
|
|
|
3119
3190
|
|
|
3120
3191
|
while (true) {
|
|
3121
3192
|
const reviewPreset = phaseConfig.reviewPreset ?? defaultAutonomousReviewPreset(type, phase);
|
|
3122
|
-
const maxReview = phaseConfig.maxReviewPasses >= 999 ? "No limit" : String(phaseConfig.maxReviewPasses);
|
|
3193
|
+
const maxReview = phaseConfig.maxReviewPasses >= 999 ? "No limit" : phaseConfig.maxReviewPasses === 0 ? "0 (disabled)" : String(phaseConfig.maxReviewPasses);
|
|
3123
3194
|
const options: OptionInput[] = [
|
|
3124
3195
|
opt(`Review preset: ${reviewPreset}`, "Pick which reviewer group runs in this phase"),
|
|
3125
3196
|
];
|
|
@@ -3259,7 +3330,7 @@ interface ResumeFilters {
|
|
|
3259
3330
|
}
|
|
3260
3331
|
|
|
3261
3332
|
const STATUS_CYCLE: StatusFilter[] = ["Active", "Active + Done", "Done only", "All"];
|
|
3262
|
-
const TYPE_CYCLE: Array<"All" | TaskType> = ["All", "implement", "
|
|
3333
|
+
const TYPE_CYCLE: Array<"All" | TaskType> = ["All", "implement", "brainstorm", "review", "quick"];
|
|
3263
3334
|
const MODE_CYCLE: Array<"All" | TaskMode> = ["All", "guided", "autonomous"];
|
|
3264
3335
|
|
|
3265
3336
|
function defaultResumeFilters(): ResumeFilters {
|
|
@@ -3380,7 +3451,7 @@ async function showFromMenu(orchestrator: Orchestrator, ctx: any): Promise<typeo
|
|
|
3380
3451
|
while (true) {
|
|
3381
3452
|
const tasks = listCompletedFromTasks(orchestrator.cwd);
|
|
3382
3453
|
if (tasks.length === 0) {
|
|
3383
|
-
ctx.ui.notify("No completed brainstorm/
|
|
3454
|
+
ctx.ui.notify("No completed brainstorm/review tasks with artifacts found.", "info");
|
|
3384
3455
|
return BACK;
|
|
3385
3456
|
}
|
|
3386
3457
|
|
|
@@ -3414,7 +3485,7 @@ async function showImplementMenu(orchestrator: Orchestrator, ctx: any): Promise<
|
|
|
3414
3485
|
while (true) {
|
|
3415
3486
|
const choice = await selectOption(ctx, "Implement", [
|
|
3416
3487
|
{ title: "New", description: "Start a new implementation from scratch" },
|
|
3417
|
-
{ title: "From", description: "Continue from a completed brainstorm
|
|
3488
|
+
{ title: "From", description: "Continue from a completed brainstorm task" },
|
|
3418
3489
|
{ title: "Resume", description: "Resume a paused implementation" },
|
|
3419
3490
|
{ title: "Back", description: "Return to the previous menu" },
|
|
3420
3491
|
]);
|
|
@@ -3533,109 +3604,196 @@ async function openCodeReviewInPlannotator(
|
|
|
3533
3604
|
// the cursor stays put and the user chooses Retry / Skip / Done. When the cursor
|
|
3534
3605
|
// is exhausted or the user stops, clear the cursor and return null (fall back to
|
|
3535
3606
|
// the menu).
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3607
|
+
// Prompt for the diff scope of one repo and open Plannotator for it. Returns the
|
|
3608
|
+
// review outcome, or null when the user backed out of the scope prompt (caller
|
|
3609
|
+
// decides what that means in its flow).
|
|
3610
|
+
async function reviewOneRepoInPlannotator(
|
|
3611
|
+
orchestrator: Orchestrator,
|
|
3612
|
+
ctx: any,
|
|
3613
|
+
repo: { path: string; isRoot: boolean },
|
|
3614
|
+
): Promise<{ status: "approved" | "needs_changes" | "error"; feedback?: string; error?: string } | null> {
|
|
3615
|
+
const diffChoice = await selectOption(ctx, `Review: ${formatRepoLabel(repo)}`, [
|
|
3616
|
+
opt("All branch changes", "Committed changes vs base branch"),
|
|
3617
|
+
opt("Last commit", "Changes in the most recent commit"),
|
|
3618
|
+
opt("Since commit", "Review all changes since a specific commit"),
|
|
3619
|
+
opt("Uncommitted changes", "Working directory changes"),
|
|
3620
|
+
opt("Back", "Return without reviewing this repo"),
|
|
3621
|
+
]);
|
|
3622
|
+
if (!diffChoice || diffChoice === "Back") return null;
|
|
3623
|
+
|
|
3624
|
+
let diffType: string;
|
|
3625
|
+
let defaultBranch: string | undefined;
|
|
3626
|
+
if (diffChoice === "All branch changes") {
|
|
3627
|
+
diffType = "branch";
|
|
3628
|
+
defaultBranch = await detectDefaultBranch(orchestrator, getRegisteredRepos(orchestrator), repo.path);
|
|
3629
|
+
} else if (diffChoice === "Last commit") {
|
|
3630
|
+
diffType = "last-commit";
|
|
3631
|
+
} else if (diffChoice === "Since commit") {
|
|
3632
|
+
const pickedHash = await pickCommitForRepo(orchestrator, ctx, repo);
|
|
3633
|
+
if (!pickedHash) return null;
|
|
3634
|
+
diffType = "branch";
|
|
3635
|
+
defaultBranch = pickedHash;
|
|
3636
|
+
} else {
|
|
3637
|
+
diffType = "uncommitted";
|
|
3638
|
+
}
|
|
3541
3639
|
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3640
|
+
return openCodeReviewInPlannotator(orchestrator, { cwd: repo.path, diffType, defaultBranch });
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
function resolveRepo(orchestrator: Orchestrator, repoPath: string): { path: string; isRoot: boolean } {
|
|
3644
|
+
return getRegisteredRepos(orchestrator).find((r) => r.path === repoPath) ?? { path: repoPath, isRoot: false };
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
function plannotatorFixBanner(repo: { path: string; isRoot: boolean }, feedback: string | undefined, more: string): string {
|
|
3648
|
+
const fb = feedback ? `\n\nFeedback:\n${feedback}` : "";
|
|
3649
|
+
return advanceBanner(
|
|
3650
|
+
`[PI-PI] Plannotator requested changes for ${formatRepoLabel(repo)}.${fb}\n\n` +
|
|
3651
|
+
"Address the user's feedback. If the feedback contains questions, answer them. If it requests changes, " +
|
|
3652
|
+
`make the changes. Then call pp_phase_complete when done.\n\n${more}`,
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3555
3655
|
|
|
3556
|
-
|
|
3656
|
+
// Single-repo review keeps the streamlined behavior: review the one repo, and on
|
|
3657
|
+
// needs_changes return the fix instruction immediately (no picker). Multi-repo
|
|
3658
|
+
// review is user-directed via the status picker below.
|
|
3659
|
+
async function runSingleRepoCursor(orchestrator: Orchestrator, ctx: any, repoPath: string): Promise<string | null> {
|
|
3660
|
+
const task = orchestrator.active!;
|
|
3661
|
+
const repo = resolveRepo(orchestrator, repoPath);
|
|
3662
|
+
while (true) {
|
|
3663
|
+
const result = await reviewOneRepoInPlannotator(orchestrator, ctx, repo);
|
|
3664
|
+
if (!result) {
|
|
3557
3665
|
task.state.plannotatorCursor = undefined;
|
|
3666
|
+
restoreMenuStepAfterReview(orchestrator, task);
|
|
3558
3667
|
saveTask(task.dir, task.state);
|
|
3559
3668
|
return null;
|
|
3560
3669
|
}
|
|
3561
|
-
if (diffChoice === "Skip this repo") {
|
|
3562
|
-
cur.index += 1;
|
|
3563
|
-
saveTask(task.dir, task.state);
|
|
3564
|
-
continue;
|
|
3565
|
-
}
|
|
3566
|
-
|
|
3567
|
-
let diffType: string;
|
|
3568
|
-
let defaultBranch: string | undefined;
|
|
3569
|
-
if (diffChoice === "All branch changes") {
|
|
3570
|
-
diffType = "branch";
|
|
3571
|
-
defaultBranch = await detectDefaultBranch(orchestrator, getRegisteredRepos(orchestrator), repo.path);
|
|
3572
|
-
} else if (diffChoice === "Last commit") {
|
|
3573
|
-
diffType = "last-commit";
|
|
3574
|
-
} else if (diffChoice === "Since commit") {
|
|
3575
|
-
const pickedHash = await pickCommitForRepo(orchestrator, ctx, repo);
|
|
3576
|
-
if (!pickedHash) continue;
|
|
3577
|
-
diffType = "branch";
|
|
3578
|
-
defaultBranch = pickedHash;
|
|
3579
|
-
} else {
|
|
3580
|
-
diffType = "uncommitted";
|
|
3581
|
-
}
|
|
3582
|
-
|
|
3583
|
-
const result = await openCodeReviewInPlannotator(orchestrator, { cwd: repo.path, diffType, defaultBranch });
|
|
3584
|
-
|
|
3585
|
-
// On error the repo is UNREVIEWED: do not advance the cursor (that would
|
|
3586
|
-
// silently drop it from the pass). Keep it as the current repo until the
|
|
3587
|
-
// user retries, explicitly skips, or stops.
|
|
3588
3670
|
if (result.status === "error") {
|
|
3589
3671
|
ctx.ui.notify(`${formatRepoLabel(repo)}: ERROR${result.error ? ` — ${result.error}` : ""}`, "warning");
|
|
3590
3672
|
const errorChoice = await selectOption(ctx, `Review failed: ${formatRepoLabel(repo)}`, [
|
|
3591
3673
|
opt("Retry", "Try reviewing this repository again"),
|
|
3592
|
-
opt("
|
|
3593
|
-
opt("Done (stop reviewing)", "Stop iterating repositories"),
|
|
3674
|
+
opt("Done (stop reviewing)", "Stop reviewing"),
|
|
3594
3675
|
]);
|
|
3595
3676
|
if (!errorChoice || errorChoice === "Done (stop reviewing)") {
|
|
3596
3677
|
task.state.plannotatorCursor = undefined;
|
|
3678
|
+
restoreMenuStepAfterReview(orchestrator, task);
|
|
3597
3679
|
saveTask(task.dir, task.state);
|
|
3598
3680
|
return null;
|
|
3599
3681
|
}
|
|
3600
|
-
if (errorChoice === "Skip this repo") {
|
|
3601
|
-
cur.index += 1;
|
|
3602
|
-
saveTask(task.dir, task.state);
|
|
3603
|
-
}
|
|
3604
|
-
// Retry: leave cur.index unchanged so the loop re-reviews this repo.
|
|
3605
3682
|
continue;
|
|
3606
3683
|
}
|
|
3684
|
+
if (result.status === "needs_changes") {
|
|
3685
|
+
task.state.plannotatorCursor = undefined;
|
|
3686
|
+
setStep(orchestrator, "llm_work");
|
|
3687
|
+
saveTask(task.dir, task.state);
|
|
3688
|
+
return plannotatorFixBanner(repo, result.feedback, "This was the last repo to review.");
|
|
3689
|
+
}
|
|
3690
|
+
ctx.ui.notify(`${formatRepoLabel(repo)}: APPROVED`, "info");
|
|
3691
|
+
task.state.plannotatorCursor = undefined;
|
|
3692
|
+
restoreMenuStepAfterReview(orchestrator, task);
|
|
3693
|
+
saveTask(task.dir, task.state);
|
|
3694
|
+
return null;
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3607
3697
|
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3698
|
+
// Multi-repo review is user-directed (#3): after each repo's review completes
|
|
3699
|
+
// (approved OR changes-requested) the loop does NOT auto-advance to the next repo
|
|
3700
|
+
// or auto-apply fixes. Instead a status picker lets the user explicitly pick the
|
|
3701
|
+
// next repo to review, start applying a changes-requested repo's fixes, or stop.
|
|
3702
|
+
async function runMultiRepoCursor(orchestrator: Orchestrator, ctx: any): Promise<string | null> {
|
|
3703
|
+
const task = orchestrator.active!;
|
|
3704
|
+
while (task.state.plannotatorCursor) {
|
|
3705
|
+
const cur = task.state.plannotatorCursor;
|
|
3706
|
+
const status = cur.status ?? {};
|
|
3707
|
+
const feedbackMap = cur.feedback ?? {};
|
|
3708
|
+
|
|
3709
|
+
const options: OptionInput[] = cur.repoPaths.map((repoPath) => {
|
|
3710
|
+
const repo = resolveRepo(orchestrator, repoPath);
|
|
3711
|
+
const st = status[repoPath];
|
|
3712
|
+
const label = st === "approved" ? "approved"
|
|
3713
|
+
: st === "changes-requested" ? "changes requested — select to apply fixes"
|
|
3714
|
+
: st === "fixes-applied" ? "fixes applied — select to re-review"
|
|
3715
|
+
: "unreviewed — select to review";
|
|
3716
|
+
return opt(formatRepoLabel(repo), label);
|
|
3717
|
+
});
|
|
3718
|
+
options.push(opt("Done (stop reviewing)", "Stop reviewing the remaining repositories"));
|
|
3613
3719
|
|
|
3614
|
-
|
|
3615
|
-
|
|
3720
|
+
const choice = await selectOption(ctx, "Plannotator review — pick a repository", options);
|
|
3721
|
+
if (!choice || choice === "Done (stop reviewing)") {
|
|
3722
|
+
task.state.plannotatorCursor = undefined;
|
|
3723
|
+
restoreMenuStepAfterReview(orchestrator, task);
|
|
3724
|
+
saveTask(task.dir, task.state);
|
|
3725
|
+
return null;
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
const repoPath = cur.repoPaths.find((p) => formatRepoLabel(resolveRepo(orchestrator, p)) === choice);
|
|
3729
|
+
if (!repoPath) continue;
|
|
3730
|
+
const repo = resolveRepo(orchestrator, repoPath);
|
|
3731
|
+
|
|
3732
|
+
// A changes-requested repo the user selects: hand off the fix instruction now
|
|
3733
|
+
// (the user explicitly starts applying fixes). Clear its pending feedback and
|
|
3734
|
+
// move it to "fixes-applied" so the picker next offers a re-review (reopening
|
|
3735
|
+
// Plannotator) rather than repeating the fix handoff. The repo stays in the
|
|
3736
|
+
// cursor so the user returns to the picker after the agent's turn.
|
|
3737
|
+
if (status[repoPath] === "changes-requested") {
|
|
3738
|
+
const feedback = feedbackMap[repoPath];
|
|
3739
|
+
delete feedbackMap[repoPath];
|
|
3740
|
+
cur.feedback = feedbackMap;
|
|
3741
|
+
status[repoPath] = "fixes-applied";
|
|
3742
|
+
cur.status = status;
|
|
3616
3743
|
setStep(orchestrator, "llm_work");
|
|
3617
3744
|
saveTask(task.dir, task.state);
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
return advanceBanner(
|
|
3623
|
-
`[PI-PI] Plannotator requested changes for ${formatRepoLabel(repo)}.${feedback}\n\n` +
|
|
3624
|
-
"Address the user's feedback. If the feedback contains questions, answer them. If it requests changes, " +
|
|
3625
|
-
`make the changes. Then call pp_phase_complete when done.\n\n${more}`,
|
|
3745
|
+
return plannotatorFixBanner(
|
|
3746
|
+
repo,
|
|
3747
|
+
feedback,
|
|
3748
|
+
"After you finish, run /pp to return to the Plannotator repository picker.",
|
|
3626
3749
|
);
|
|
3627
3750
|
}
|
|
3628
3751
|
|
|
3629
|
-
|
|
3752
|
+
// Otherwise (unreviewed or re-review of an approved repo): open Plannotator.
|
|
3753
|
+
const result = await reviewOneRepoInPlannotator(orchestrator, ctx, repo);
|
|
3754
|
+
if (!result) continue;
|
|
3755
|
+
if (result.status === "error") {
|
|
3756
|
+
ctx.ui.notify(`${formatRepoLabel(repo)}: ERROR${result.error ? ` — ${result.error}` : ""}`, "warning");
|
|
3757
|
+
continue;
|
|
3758
|
+
}
|
|
3759
|
+
cur.status = status;
|
|
3760
|
+
if (result.status === "needs_changes") {
|
|
3761
|
+
status[repoPath] = "changes-requested";
|
|
3762
|
+
if (result.feedback) {
|
|
3763
|
+
feedbackMap[repoPath] = result.feedback;
|
|
3764
|
+
cur.feedback = feedbackMap;
|
|
3765
|
+
}
|
|
3766
|
+
ctx.ui.notify(`${formatRepoLabel(repo)}: CHANGES REQUESTED — select it again to apply fixes.`, "info");
|
|
3767
|
+
} else {
|
|
3768
|
+
status[repoPath] = "approved";
|
|
3769
|
+
ctx.ui.notify(`${formatRepoLabel(repo)}: APPROVED`, "info");
|
|
3770
|
+
}
|
|
3630
3771
|
saveTask(task.dir, task.state);
|
|
3631
3772
|
}
|
|
3632
|
-
|
|
3633
|
-
// Cursor exhausted with no outstanding changes: clear it and fall back to /pp.
|
|
3634
|
-
task.state.plannotatorCursor = undefined;
|
|
3773
|
+
restoreMenuStepAfterReview(orchestrator, task);
|
|
3635
3774
|
saveTask(task.dir, task.state);
|
|
3636
3775
|
return null;
|
|
3637
3776
|
}
|
|
3638
3777
|
|
|
3778
|
+
async function runPlannotatorCursor(orchestrator: Orchestrator, ctx: any): Promise<string | null> {
|
|
3779
|
+
const task = orchestrator.active;
|
|
3780
|
+
if (!task) return null;
|
|
3781
|
+
const cursor = task.state.plannotatorCursor;
|
|
3782
|
+
if (!cursor) return null;
|
|
3783
|
+
|
|
3784
|
+
if (cursor.repoPaths.length <= 1) {
|
|
3785
|
+
const only = cursor.repoPaths[cursor.index] ?? cursor.repoPaths[0];
|
|
3786
|
+
if (!only) {
|
|
3787
|
+
task.state.plannotatorCursor = undefined;
|
|
3788
|
+
restoreMenuStepAfterReview(orchestrator, task);
|
|
3789
|
+
saveTask(task.dir, task.state);
|
|
3790
|
+
return null;
|
|
3791
|
+
}
|
|
3792
|
+
return runSingleRepoCursor(orchestrator, ctx, only);
|
|
3793
|
+
}
|
|
3794
|
+
return runMultiRepoCursor(orchestrator, ctx);
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3639
3797
|
async function showReviewMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
3640
3798
|
while (true) {
|
|
3641
3799
|
const choice = await selectOption(ctx, "Review", [
|
|
@@ -3674,19 +3832,7 @@ async function showTaskTypeMenu(
|
|
|
3674
3832
|
if (!choice || choice === "Back") return BACK;
|
|
3675
3833
|
|
|
3676
3834
|
if (choice === "New") {
|
|
3677
|
-
|
|
3678
|
-
let autonomousConfig: AutonomousConfig | undefined;
|
|
3679
|
-
if (type === "debug") {
|
|
3680
|
-
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, type);
|
|
3681
|
-
if (!modeSelection) continue;
|
|
3682
|
-
mode = modeSelection.mode;
|
|
3683
|
-
autonomousConfig = modeSelection.autonomousConfig;
|
|
3684
|
-
}
|
|
3685
|
-
await orchestrator.startTask(ctx, type, type, undefined, undefined, mode);
|
|
3686
|
-
if (orchestrator.active) {
|
|
3687
|
-
orchestrator.active.state.autonomousConfig = autonomousConfig;
|
|
3688
|
-
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
3689
|
-
}
|
|
3835
|
+
await orchestrator.startTask(ctx, type, type, undefined, undefined, undefined);
|
|
3690
3836
|
return "started";
|
|
3691
3837
|
}
|
|
3692
3838
|
|
|
@@ -3699,7 +3845,6 @@ async function showTaskMenu(orchestrator: Orchestrator, ctx: any): Promise<typeo
|
|
|
3699
3845
|
while (true) {
|
|
3700
3846
|
const choice = await selectOption(ctx, "Task", [
|
|
3701
3847
|
{ title: "Implement", description: "Want to make some changes? Research any topic or a codebase, brainstorm solutions and implement the chosen one" },
|
|
3702
|
-
{ title: "Debug", description: "Something is broken? Investigate it. If there is an issue — brainstorm solutions and fix it" },
|
|
3703
3848
|
{ title: "Brainstorm", description: "No idea where to start? Research any topic or a codebase. If there is a problem to solve — brainstorm solutions and solve it" },
|
|
3704
3849
|
{ title: "Review", description: "Want to ensure that some commits or a GitHub PR are good to go? Review it. Even fix it yourself, if you want" },
|
|
3705
3850
|
{ title: "Quick", description: "Quick freeform task — no phases, no reviews, just work" },
|
|
@@ -3708,12 +3853,6 @@ async function showTaskMenu(orchestrator: Orchestrator, ctx: any): Promise<typeo
|
|
|
3708
3853
|
]);
|
|
3709
3854
|
if (!choice || choice === "Back") return BACK;
|
|
3710
3855
|
|
|
3711
|
-
if (choice === "Debug") {
|
|
3712
|
-
const result = await showTaskTypeMenu(orchestrator, ctx, "debug");
|
|
3713
|
-
if (result === "started") return "started";
|
|
3714
|
-
continue;
|
|
3715
|
-
}
|
|
3716
|
-
|
|
3717
3856
|
if (choice === "Brainstorm") {
|
|
3718
3857
|
const result = await showTaskTypeMenu(orchestrator, ctx, "brainstorm");
|
|
3719
3858
|
if (result === "started") return "started";
|
|
@@ -3917,7 +4056,7 @@ export async function showActiveTaskMenu(
|
|
|
3917
4056
|
const isReviewPhase = phase === "review";
|
|
3918
4057
|
const hasPlannotator = phase === "plan" || phase === "implement" || isReviewPhase;
|
|
3919
4058
|
// The artifact the automated reviewers scan, mirroring reviewPresetGroupForPhase:
|
|
3920
|
-
// brainstorm
|
|
4059
|
+
// brainstorm → brainstormReviewers (research artifacts), plan → planReviewers
|
|
3921
4060
|
// (synthesized plan), implement/review → codeReviewers (code changes). This is the primary
|
|
3922
4061
|
// "Review" target named at the top level; the per-phase "Review on my own" description names
|
|
3923
4062
|
// the manual editor pass's target separately (it can differ).
|
|
@@ -3969,6 +4108,11 @@ export async function showActiveTaskMenu(
|
|
|
3969
4108
|
options.push(opt("Back to prompt", "Return to the prompt and keep working"));
|
|
3970
4109
|
|
|
3971
4110
|
const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
|
|
4111
|
+
if (task.state.reconcilePending) {
|
|
4112
|
+
headerLines.push("\n\n⚠ State files may be stale — reconcile pending; they'll be refreshed before the next review/planner run.");
|
|
4113
|
+
}
|
|
4114
|
+
const assumptionsBanner = openAssumptionsBanner(task.dir);
|
|
4115
|
+
if (assumptionsBanner) headerLines.push(`\n\n${assumptionsBanner}`);
|
|
3972
4116
|
if (summary !== "/pp") headerLines.push(`\n\n${summary}`);
|
|
3973
4117
|
const menuTitle = headerLines.join("");
|
|
3974
4118
|
const { choice, cancelReason } = await selectOptionCancelable(ctx, menuTitle, options);
|
|
@@ -4047,7 +4191,7 @@ export async function showActiveTaskMenu(
|
|
|
4047
4191
|
continue;
|
|
4048
4192
|
}
|
|
4049
4193
|
const passes = await pickMaxReviewPasses(ctx, 3);
|
|
4050
|
-
if (passes === null) continue;
|
|
4194
|
+
if (passes === null || passes === 0) continue;
|
|
4051
4195
|
finalizeReviewCycle(task);
|
|
4052
4196
|
if (task.state.reviewPassByKind?.[phase]) task.state.reviewPassByKind[phase].auto = 0;
|
|
4053
4197
|
task.state.reviewApprovedClean = false;
|
|
@@ -4134,7 +4278,7 @@ export async function showActiveTaskMenu(
|
|
|
4134
4278
|
opt(autoLabel, `Run configured reviewers over ${reviewTarget}`),
|
|
4135
4279
|
opt("Auto review until approved", `Loop reviewers over ${reviewTarget} until they unanimously approve, up to N passes`),
|
|
4136
4280
|
];
|
|
4137
|
-
const hasArtifactPlannotator = phase === "brainstorm"
|
|
4281
|
+
const hasArtifactPlannotator = phase === "brainstorm";
|
|
4138
4282
|
if (hasPlannotator) {
|
|
4139
4283
|
reviewOptions.push(opt("Review in Plannotator", phase === "plan" ? "Open plan review in browser" : "Open code diff review in browser"));
|
|
4140
4284
|
} else if (hasArtifactPlannotator) {
|
|
@@ -4152,7 +4296,7 @@ export async function showActiveTaskMenu(
|
|
|
4152
4296
|
|
|
4153
4297
|
if (reviewChoice === "Review in Plannotator") {
|
|
4154
4298
|
if (hasArtifactPlannotator) {
|
|
4155
|
-
// brainstorm
|
|
4299
|
+
// brainstorm has no diff or plan file: review the whole artifact
|
|
4156
4300
|
// set in one annotate-folder pass over the task dir (walks USER_REQUEST.md +
|
|
4157
4301
|
// RESEARCH.md + artifacts/*.md recursively). The result comes back on the
|
|
4158
4302
|
// synchronous annotate respond callback, NOT via waitForPlannotatorResult.
|
|
@@ -4201,10 +4345,11 @@ export async function showActiveTaskMenu(
|
|
|
4201
4345
|
ctx.ui.notify("No registered repositories have changes to review.", "info");
|
|
4202
4346
|
continue;
|
|
4203
4347
|
}
|
|
4204
|
-
// Start (or restart) the
|
|
4205
|
-
//
|
|
4206
|
-
//
|
|
4207
|
-
//
|
|
4348
|
+
// Start (or restart) the per-repo cursor and run it. A single eligible
|
|
4349
|
+
// repo runs the streamlined flow; multiple repos open the user-directed
|
|
4350
|
+
// status picker. On a changes-requested handoff runPlannotatorCursor
|
|
4351
|
+
// persists the cursor and returns a work instruction (exits the menu to
|
|
4352
|
+
// start the agent turn); the next /pp resumes the cursor.
|
|
4208
4353
|
task.state.plannotatorCursor = { repoPaths: eligible, index: 0 };
|
|
4209
4354
|
saveTask(task.dir, task.state);
|
|
4210
4355
|
const cursorText = await runPlannotatorCursor(orchestrator, ctx);
|
|
@@ -4247,7 +4392,7 @@ export async function showActiveTaskMenu(
|
|
|
4247
4392
|
continue;
|
|
4248
4393
|
}
|
|
4249
4394
|
const passes = await pickMaxReviewPasses(ctx, 3);
|
|
4250
|
-
if (passes === null) continue;
|
|
4395
|
+
if (passes === null || passes === 0) continue;
|
|
4251
4396
|
finalizeReviewCycle(task);
|
|
4252
4397
|
// Item 5: stop-in-phase loop. Reset the per-phase auto-pass counter so the
|
|
4253
4398
|
// cap counts THIS run's passes, and clear any stale clean flag.
|
|
@@ -4265,6 +4410,7 @@ export async function showActiveTaskMenu(
|
|
|
4265
4410
|
|
|
4266
4411
|
const reviewPreset = await pickPreset(ctx, orchestrator, getReviewPresetGroup(phase), "Review preset");
|
|
4267
4412
|
if (!reviewPreset) continue;
|
|
4413
|
+
if (!(await confirmReviewDespiteStaleState(orchestrator, ctx))) continue;
|
|
4268
4414
|
finalizeReviewCycle(task);
|
|
4269
4415
|
if (!hasEnabledReviewers(orchestrator, reviewPreset)) {
|
|
4270
4416
|
const reviewGroup = reviewPresetGroupForPhase(phase);
|