@ilya-lesikov/pi-pi 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +6 -3
- package/3p/pi-plannotator/packages/shared/review-core.ts +22 -3
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +1 -1
- package/3p/pi-subagents/src/index.ts +9 -1
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +4 -1
- package/extensions/orchestrator/agents/planner.ts +13 -2
- package/extensions/orchestrator/agents/tool-routing.ts +9 -0
- package/extensions/orchestrator/command-handlers.test.ts +22 -25
- package/extensions/orchestrator/config.test.ts +1 -0
- package/extensions/orchestrator/config.ts +3 -1
- package/extensions/orchestrator/custom-footer.ts +146 -0
- package/extensions/orchestrator/event-handlers.test.ts +1 -0
- package/extensions/orchestrator/event-handlers.ts +73 -146
- package/extensions/orchestrator/flant-infra.ts +7 -2
- package/extensions/orchestrator/integration.test.ts +3 -1
- package/extensions/orchestrator/orchestrator.test.ts +1 -0
- package/extensions/orchestrator/orchestrator.ts +12 -4
- package/extensions/orchestrator/phases/machine.ts +38 -0
- package/extensions/orchestrator/phases/review-task.ts +32 -0
- package/extensions/orchestrator/pp-menu.ts +405 -49
- package/extensions/orchestrator/state.ts +8 -9
- package/extensions/orchestrator/usage-tracker.ts +306 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "fs";
|
|
1
|
+
import { existsSync, readdirSync, writeFileSync } from "fs";
|
|
2
2
|
import { join, relative } from "path";
|
|
3
3
|
import { askUser } from "../../3p/pi-ask-user/index.js";
|
|
4
4
|
import { unregisterAgentDefinitions } from "./agents/registry.js";
|
|
@@ -8,12 +8,13 @@ import {
|
|
|
8
8
|
loadCodeReviewOutputs,
|
|
9
9
|
loadPlanReviewOutputs,
|
|
10
10
|
} from "./context.js";
|
|
11
|
-
import { enterReviewCycle, finalizeReviewCycle } from "./event-handlers.js";
|
|
11
|
+
import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle } from "./event-handlers.js";
|
|
12
12
|
import { Orchestrator, deepReviewConfig } from "./orchestrator.js";
|
|
13
13
|
import { cancelPendingPlannotatorWait } from "./plannotator.js";
|
|
14
14
|
import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
|
|
15
15
|
import { spawnCodeReviewers } from "./phases/review.js";
|
|
16
16
|
import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
17
|
+
|
|
17
18
|
import {
|
|
18
19
|
listTasks,
|
|
19
20
|
loadTask,
|
|
@@ -72,6 +73,38 @@ function showStatus(orchestrator: Orchestrator, ctx: any): void {
|
|
|
72
73
|
);
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
async function abortCurrentWork(orchestrator: Orchestrator, ctx: any): Promise<void> {
|
|
77
|
+
orchestrator.abortAllSubagents();
|
|
78
|
+
ctx.abort?.();
|
|
79
|
+
await ctx.waitForIdle?.();
|
|
80
|
+
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
81
|
+
taskStore?.clearAll?.();
|
|
82
|
+
taskStore?.refreshWidget?.(ctx.ui);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string> {
|
|
86
|
+
if (!orchestrator.active) return "No active task.";
|
|
87
|
+
|
|
88
|
+
cancelPendingPlannotatorWait(orchestrator);
|
|
89
|
+
orchestrator.abortAllSubagents();
|
|
90
|
+
ctx.abort?.();
|
|
91
|
+
await ctx.waitForIdle?.();
|
|
92
|
+
|
|
93
|
+
const name = orchestrator.active.description;
|
|
94
|
+
|
|
95
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
96
|
+
unregisterAgentDefinitions(orchestrator.pi);
|
|
97
|
+
await orchestrator.cleanupActive();
|
|
98
|
+
|
|
99
|
+
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
100
|
+
taskStore?.clearAll?.();
|
|
101
|
+
taskStore?.refreshWidget?.(ctx.ui);
|
|
102
|
+
|
|
103
|
+
orchestrator.updateStatus(ctx);
|
|
104
|
+
ctx.ui.notify(`Task "${name}" paused. Use /pp → Resume to continue.`, "info");
|
|
105
|
+
return `Task "${name}" paused.`;
|
|
106
|
+
}
|
|
107
|
+
|
|
75
108
|
async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string> {
|
|
76
109
|
if (!orchestrator.active) return "No active task.";
|
|
77
110
|
|
|
@@ -162,7 +195,12 @@ export async function resumeTask(
|
|
|
162
195
|
description: task.state.description,
|
|
163
196
|
};
|
|
164
197
|
|
|
165
|
-
const modelConfig = orchestrator.config.mainModel[
|
|
198
|
+
const modelConfig = orchestrator.config.mainModel[
|
|
199
|
+
task.type === "debug" ? "debug"
|
|
200
|
+
: task.type === "brainstorm" ? "brainstorm"
|
|
201
|
+
: task.type === "review" ? "review"
|
|
202
|
+
: "implement"
|
|
203
|
+
];
|
|
166
204
|
const modelOk = await orchestrator.switchModel(ctx, modelConfig.model, modelConfig.thinking);
|
|
167
205
|
if (!modelOk) {
|
|
168
206
|
ctx.ui.notify(`Model "${modelConfig.model}" not found — using current model`, "warning");
|
|
@@ -401,6 +439,7 @@ function collectRoleAssignments(config: Partial<PiPiConfig> | null): string[] {
|
|
|
401
439
|
add("mainModel.implement", config.mainModel?.implement?.model);
|
|
402
440
|
add("mainModel.debug", config.mainModel?.debug?.model);
|
|
403
441
|
add("mainModel.brainstorm", config.mainModel?.brainstorm?.model);
|
|
442
|
+
add("mainModel.review", config.mainModel?.review?.model);
|
|
404
443
|
|
|
405
444
|
for (const [name, variant] of Object.entries(config.planners ?? {})) {
|
|
406
445
|
if (variant.enabled) add(`planners.${name}`, variant.model);
|
|
@@ -500,7 +539,14 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
500
539
|
}
|
|
501
540
|
|
|
502
541
|
if (choice.startsWith("Cache period:")) {
|
|
503
|
-
const selected = await selectOption(ctx, "Cache period", [
|
|
542
|
+
const selected = await selectOption(ctx, "Cache period", [
|
|
543
|
+
{ title: "1 day", description: "Refresh model metadata daily" },
|
|
544
|
+
{ title: "3 days", description: "Refresh model metadata every three days" },
|
|
545
|
+
{ title: "7 days", description: "Default — refresh weekly" },
|
|
546
|
+
{ title: "14 days", description: "Refresh model metadata every two weeks" },
|
|
547
|
+
{ title: "30 days", description: "Refresh model metadata monthly" },
|
|
548
|
+
{ title: "Back", description: "Return to the previous menu" },
|
|
549
|
+
]);
|
|
504
550
|
if (!selected || selected === "Back") continue;
|
|
505
551
|
const days = Number(selected.split(" ")[0]);
|
|
506
552
|
if (!Number.isFinite(days) || days <= 0) continue;
|
|
@@ -520,13 +566,158 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
520
566
|
}
|
|
521
567
|
}
|
|
522
568
|
|
|
523
|
-
|
|
569
|
+
function formatTokenCount(count: number): string {
|
|
570
|
+
if (count < 1000) return String(count);
|
|
571
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
572
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
573
|
+
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
|
|
574
|
+
return `${Math.round(count / 1000000)}M`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function formatDuration(ms: number): string {
|
|
578
|
+
const sec = Math.floor(ms / 1000);
|
|
579
|
+
if (sec < 60) return `${sec}s`;
|
|
580
|
+
const min = Math.floor(sec / 60);
|
|
581
|
+
const remSec = sec % 60;
|
|
582
|
+
if (min < 60) return remSec > 0 ? `${min}m ${remSec}s` : `${min}m`;
|
|
583
|
+
const hr = Math.floor(min / 60);
|
|
584
|
+
const remMin = min % 60;
|
|
585
|
+
return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function showUsage(ctx: any): void {
|
|
589
|
+
const tracker = (globalThis as any)[Symbol.for("pi-pi:usage-tracker")] as
|
|
590
|
+
| {
|
|
591
|
+
getTotalInputTokens(): number; getTotalOutputTokens(): number;
|
|
592
|
+
getTotalCacheReadTokens(): number; getTotalCacheWriteTokens(): number;
|
|
593
|
+
getTotalCost(): number; getCacheHitRate(): number;
|
|
594
|
+
getPerModelUsage(): Record<string, { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; turns: number }>;
|
|
595
|
+
getSubagentList(): Array<{ description: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cost: number; durationMs: number; toolUses: number }>;
|
|
596
|
+
}
|
|
597
|
+
| undefined;
|
|
598
|
+
|
|
599
|
+
if (!tracker) {
|
|
600
|
+
ctx.ui.notify("No usage data available.", "info");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const mainInput = tracker.getTotalInputTokens();
|
|
605
|
+
const mainOutput = tracker.getTotalOutputTokens();
|
|
606
|
+
const mainCacheRead = tracker.getTotalCacheReadTokens();
|
|
607
|
+
const mainCacheWrite = tracker.getTotalCacheWriteTokens();
|
|
608
|
+
const mainCost = tracker.getTotalCost();
|
|
609
|
+
const models = tracker.getPerModelUsage();
|
|
610
|
+
const subagents = tracker.getSubagentList();
|
|
611
|
+
|
|
612
|
+
let saInput = 0, saOutput = 0, saCacheRead = 0, saCacheWrite = 0, saCost = 0;
|
|
613
|
+
for (const sa of subagents) {
|
|
614
|
+
saInput += sa.inputTokens;
|
|
615
|
+
saOutput += sa.outputTokens;
|
|
616
|
+
saCacheRead += sa.cacheReadTokens;
|
|
617
|
+
saCacheWrite += sa.cacheWriteTokens;
|
|
618
|
+
saCost += sa.cost;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const totalInput = mainInput + saInput;
|
|
622
|
+
const totalOutput = mainOutput + saOutput;
|
|
623
|
+
const totalCacheRead = mainCacheRead + saCacheRead;
|
|
624
|
+
const totalCost = mainCost + saCost;
|
|
625
|
+
const totalCacheRate = (totalCacheRead + totalInput) > 0
|
|
626
|
+
? totalCacheRead / (totalCacheRead + totalInput)
|
|
627
|
+
: 0;
|
|
628
|
+
|
|
629
|
+
const byModel = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }>();
|
|
630
|
+
for (const [modelId, usage] of Object.entries(models)) {
|
|
631
|
+
byModel.set(modelId, {
|
|
632
|
+
input: usage.inputTokens, output: usage.outputTokens,
|
|
633
|
+
cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cost: 0,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (mainCost > 0 && Object.keys(models).length === 1) {
|
|
637
|
+
const entry = byModel.values().next().value;
|
|
638
|
+
if (entry) entry.cost = mainCost;
|
|
639
|
+
}
|
|
640
|
+
for (const sa of subagents) {
|
|
641
|
+
const key = sa.modelId !== "unknown" ? sa.modelId : sa.description;
|
|
642
|
+
const existing = byModel.get(key);
|
|
643
|
+
if (existing) {
|
|
644
|
+
existing.input += sa.inputTokens;
|
|
645
|
+
existing.output += sa.outputTokens;
|
|
646
|
+
existing.cacheRead += sa.cacheReadTokens;
|
|
647
|
+
existing.cacheWrite += sa.cacheWriteTokens;
|
|
648
|
+
existing.cost += sa.cost;
|
|
649
|
+
} else {
|
|
650
|
+
byModel.set(key, {
|
|
651
|
+
input: sa.inputTokens, output: sa.outputTokens,
|
|
652
|
+
cacheRead: sa.cacheReadTokens, cacheWrite: sa.cacheWriteTokens, cost: sa.cost,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const lines: string[] = ["Session usage (total):"];
|
|
658
|
+
lines.push(` Input: ${formatTokenCount(totalInput)} tokens`);
|
|
659
|
+
lines.push(` Output: ${formatTokenCount(totalOutput)} tokens`);
|
|
660
|
+
if (totalCacheRead > 0) lines.push(` Cache: ⚡${Math.round(totalCacheRate * 100)}% hit rate`);
|
|
661
|
+
if (totalCost > 0) lines.push(` Cost: $${totalCost.toFixed(3)}`);
|
|
662
|
+
|
|
663
|
+
if (byModel.size > 0) {
|
|
664
|
+
lines.push("");
|
|
665
|
+
lines.push("By model:");
|
|
666
|
+
for (const [modelId, m] of byModel) {
|
|
667
|
+
const cr = (m.cacheRead + m.input) > 0 ? Math.round(m.cacheRead / (m.cacheRead + m.input) * 100) : 0;
|
|
668
|
+
const parts = [`↑${formatTokenCount(m.input)}`, `↓${formatTokenCount(m.output)}`];
|
|
669
|
+
if (cr > 0) parts.push(`⚡${cr}%`);
|
|
670
|
+
if (m.cost > 0) parts.push(`$${m.cost.toFixed(3)}`);
|
|
671
|
+
lines.push(` ${modelId}: ${parts.join(" ")}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
lines.push("");
|
|
676
|
+
lines.push("By agent:");
|
|
677
|
+
const mainModelEntries = Object.entries(models);
|
|
678
|
+
if (mainModelEntries.length > 0) {
|
|
679
|
+
const mainParts = [`↑${formatTokenCount(mainInput)}`, `↓${formatTokenCount(mainOutput)}`];
|
|
680
|
+
const mainCR = (mainCacheRead + mainInput) > 0 ? Math.round(mainCacheRead / (mainCacheRead + mainInput) * 100) : 0;
|
|
681
|
+
if (mainCR > 0) mainParts.push(`⚡${mainCR}%`);
|
|
682
|
+
if (mainCost > 0) mainParts.push(`$${mainCost.toFixed(3)}`);
|
|
683
|
+
const mainModel = mainModelEntries.map(([id]) => id).join(", ");
|
|
684
|
+
lines.push(` Main (${mainModel}): ${mainParts.join(" ")}`);
|
|
685
|
+
}
|
|
686
|
+
for (const sa of subagents) {
|
|
687
|
+
const saCR = (sa.cacheReadTokens + sa.inputTokens) > 0
|
|
688
|
+
? Math.round(sa.cacheReadTokens / (sa.cacheReadTokens + sa.inputTokens) * 100) : 0;
|
|
689
|
+
const parts = [`↑${formatTokenCount(sa.inputTokens)}`, `↓${formatTokenCount(sa.outputTokens)}`];
|
|
690
|
+
if (saCR > 0) parts.push(`⚡${saCR}%`);
|
|
691
|
+
if (sa.cost > 0) parts.push(`$${sa.cost.toFixed(3)}`);
|
|
692
|
+
if (sa.durationMs > 0) parts.push(formatDuration(sa.durationMs));
|
|
693
|
+
if (sa.toolUses > 0) parts.push(`${sa.toolUses} tools`);
|
|
694
|
+
lines.push(` ${sa.description}: ${parts.join(" ")}`);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function showSettingsMenu(orchestrator: Orchestrator, ctx: any, showFlant = true): Promise<typeof BACK> {
|
|
524
701
|
while (true) {
|
|
525
|
-
const
|
|
526
|
-
{ title: "
|
|
527
|
-
{ title: "
|
|
528
|
-
]
|
|
702
|
+
const options: OptionInput[] = [
|
|
703
|
+
{ title: "Usage", description: "Show session token usage and cost breakdown" },
|
|
704
|
+
{ title: "LSP", description: "Language server status and controls" },
|
|
705
|
+
];
|
|
706
|
+
if (showFlant) {
|
|
707
|
+
options.push({ title: "Flant AI Infrastructure", description: "Configure corporate AI model provider" });
|
|
708
|
+
}
|
|
709
|
+
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
710
|
+
|
|
711
|
+
const choice = await selectOption(ctx, "Settings", options);
|
|
529
712
|
if (!choice || choice === "Back") return BACK;
|
|
713
|
+
if (choice === "Usage") {
|
|
714
|
+
showUsage(ctx);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (choice === "LSP") {
|
|
718
|
+
await showLspMenu(ctx);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
530
721
|
await showFlantInfraMenu(orchestrator, ctx);
|
|
531
722
|
}
|
|
532
723
|
}
|
|
@@ -643,6 +834,148 @@ async function showImplementMenu(orchestrator: Orchestrator, ctx: any): Promise<
|
|
|
643
834
|
}
|
|
644
835
|
}
|
|
645
836
|
|
|
837
|
+
function buildPrContext(parsed: any): { prUrl: string | null; prContext: string | null } {
|
|
838
|
+
const title = typeof parsed?.title === "string" ? parsed.title.trim() : "";
|
|
839
|
+
const body = typeof parsed?.body === "string" ? parsed.body.trim() : "";
|
|
840
|
+
const commentsRaw = Array.isArray(parsed?.comments)
|
|
841
|
+
? parsed.comments
|
|
842
|
+
: Array.isArray(parsed?.comments?.nodes)
|
|
843
|
+
? parsed.comments.nodes
|
|
844
|
+
: [];
|
|
845
|
+
const comments: string[] = commentsRaw
|
|
846
|
+
.map((comment: any) => {
|
|
847
|
+
const text = typeof comment?.body === "string"
|
|
848
|
+
? comment.body.trim()
|
|
849
|
+
: typeof comment?.bodyText === "string"
|
|
850
|
+
? comment.bodyText.trim()
|
|
851
|
+
: "";
|
|
852
|
+
return text;
|
|
853
|
+
})
|
|
854
|
+
.filter((text: string): text is string => text.length > 0);
|
|
855
|
+
|
|
856
|
+
const parts: string[] = [];
|
|
857
|
+
if (title) parts.push(`Title: ${title}`);
|
|
858
|
+
if (body) parts.push(`Body:\n${body}`);
|
|
859
|
+
if (comments.length > 0) {
|
|
860
|
+
parts.push(`Comments:\n${comments.map((comment, index) => `${index + 1}. ${comment}`).join("\n\n")}`);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const prUrl = typeof parsed?.url === "string" && parsed.url.trim().length > 0 ? parsed.url.trim() : null;
|
|
864
|
+
return { prUrl, prContext: parts.length > 0 ? parts.join("\n\n") : null };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function detectCurrentPrContext(orchestrator: Orchestrator): Promise<{ prUrl: string | null; prContext: string | null }> {
|
|
868
|
+
try {
|
|
869
|
+
const prResult = await orchestrator.pi.exec("gh", ["pr", "view", "--json", "url,title,body,comments"], {
|
|
870
|
+
cwd: orchestrator.cwd,
|
|
871
|
+
timeout: 10000,
|
|
872
|
+
});
|
|
873
|
+
if (prResult.code !== 0) return { prUrl: null, prContext: null };
|
|
874
|
+
const parsed = JSON.parse(prResult.stdout);
|
|
875
|
+
return buildPrContext(parsed);
|
|
876
|
+
} catch {
|
|
877
|
+
return { prUrl: null, prContext: null };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async function openReviewTaskInPlannotator(orchestrator: Orchestrator): Promise<string> {
|
|
882
|
+
if (!orchestrator.active) return "No active task.";
|
|
883
|
+
const payload: Record<string, unknown> = { cwd: orchestrator.cwd };
|
|
884
|
+
|
|
885
|
+
return await new Promise((resolve) => {
|
|
886
|
+
let handled = false;
|
|
887
|
+
orchestrator.pi.events.emit("plannotator:request", {
|
|
888
|
+
requestId: crypto.randomUUID(),
|
|
889
|
+
action: "code-review",
|
|
890
|
+
payload,
|
|
891
|
+
respond: (response: any) => {
|
|
892
|
+
handled = true;
|
|
893
|
+
if (response?.status !== "handled") {
|
|
894
|
+
resolve(`Plannotator is not available${response?.error ? `: ${response.error}` : "."}`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const approved = !!response?.result?.approved;
|
|
898
|
+
const feedback = typeof response?.result?.feedback === "string" && response.result.feedback.trim().length > 0
|
|
899
|
+
? `\n\nFeedback:\n${response.result.feedback}`
|
|
900
|
+
: "";
|
|
901
|
+
resolve(approved ? "Plannotator approved the review." : `Plannotator requested changes.${feedback}`);
|
|
902
|
+
},
|
|
903
|
+
});
|
|
904
|
+
setTimeout(() => {
|
|
905
|
+
if (!handled) resolve("Plannotator is not available.");
|
|
906
|
+
}, 30000);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function startReviewTask(
|
|
911
|
+
orchestrator: Orchestrator,
|
|
912
|
+
ctx: any,
|
|
913
|
+
userRequestContent: string,
|
|
914
|
+
researchContent: string | null,
|
|
915
|
+
description: string,
|
|
916
|
+
): Promise<"started" | typeof BACK> {
|
|
917
|
+
await orchestrator.startTask(ctx, "review", description);
|
|
918
|
+
if (!orchestrator.active || orchestrator.active.type !== "review") return BACK;
|
|
919
|
+
writeFileSync(join(orchestrator.active.dir, "USER_REQUEST.md"), userRequestContent, "utf-8");
|
|
920
|
+
if (researchContent) {
|
|
921
|
+
writeFileSync(join(orchestrator.active.dir, "RESEARCH.md"), researchContent, "utf-8");
|
|
922
|
+
}
|
|
923
|
+
return "started";
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function showReviewMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
927
|
+
while (true) {
|
|
928
|
+
const choice = await selectOption(ctx, "Review", [
|
|
929
|
+
{ title: "Current branch", description: "Review changes on current branch vs base" },
|
|
930
|
+
{ title: "Uncommitted changes", description: "Review working directory changes" },
|
|
931
|
+
{ title: "Describe", description: "Describe what to review and let the agent figure it out" },
|
|
932
|
+
{ title: "Resume", description: "Resume a previously unfinished review" },
|
|
933
|
+
{ title: "Back", description: "Return to the previous menu" },
|
|
934
|
+
]);
|
|
935
|
+
if (!choice || choice === "Back") return BACK;
|
|
936
|
+
|
|
937
|
+
if (choice === "Resume") {
|
|
938
|
+
const result = await showResumeMenu(orchestrator, ctx, "review", "No paused review tasks found.");
|
|
939
|
+
if (result === "started") return result;
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (choice === "Current branch") {
|
|
944
|
+
const base = await detectDefaultBranch(orchestrator.pi, orchestrator.cwd, orchestrator.config);
|
|
945
|
+
const pr = await detectCurrentPrContext(orchestrator);
|
|
946
|
+
|
|
947
|
+
const urLines = [`# User Request\nReview current branch changes (${base}..HEAD)`];
|
|
948
|
+
if (pr.prUrl) urLines.push(`PR: ${pr.prUrl}`);
|
|
949
|
+
urLines.push("", "## Problem", "Review and identify issues in the code changes.", "", "## Constraints", "Focus on correctness, edge cases, style, missing tests, potential bugs.");
|
|
950
|
+
const urContent = urLines.join("\n") + "\n";
|
|
951
|
+
|
|
952
|
+
let resContent: string | null = null;
|
|
953
|
+
if (pr.prContext) {
|
|
954
|
+
resContent = ["## PR Context", pr.prContext, "", "## Affected Code", "(to be filled during review)", "", "## Architecture Context", "(to be filled during review)"].join("\n") + "\n";
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const description = await promptDescription(ctx, "Describe the review (optional)", "review");
|
|
958
|
+
if (!description) continue;
|
|
959
|
+
return startReviewTask(orchestrator, ctx, urContent, resContent, description);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (choice === "Uncommitted changes") {
|
|
963
|
+
const urContent = "# User Request\nReview uncommitted changes\n\n## Problem\nReview and identify issues in uncommitted working directory changes.\n\n## Constraints\nFocus on correctness, edge cases, style, missing tests, potential bugs.\n";
|
|
964
|
+
const description = await promptDescription(ctx, "Describe the review (optional)", "review");
|
|
965
|
+
if (!description) continue;
|
|
966
|
+
return startReviewTask(orchestrator, ctx, urContent, null, description);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const input = await ctx.ui.input("Describe what to review");
|
|
970
|
+
if (input === undefined || input === null) continue;
|
|
971
|
+
const trimmed = String(input).trim();
|
|
972
|
+
if (!trimmed) continue;
|
|
973
|
+
|
|
974
|
+
const urContent = `# User Request\n${trimmed}\n\n## Problem\n${trimmed}\n\n## Constraints\nFocus on correctness, edge cases, style, missing tests, potential bugs.\n`;
|
|
975
|
+
return startReviewTask(orchestrator, ctx, urContent, null, trimmed);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
646
979
|
async function showTaskTypeMenu(
|
|
647
980
|
orchestrator: Orchestrator,
|
|
648
981
|
ctx: any,
|
|
@@ -675,13 +1008,13 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
|
|
|
675
1008
|
{ title: "Debug", description: "Diagnose an issue. Then (optionally) fix it" },
|
|
676
1009
|
{ title: "Brainstorm", description: "Explore and brainstorm. Then (optionally) plan and implement" },
|
|
677
1010
|
{ title: "Implement", description: "Brainstorm, plan and implement" },
|
|
1011
|
+
{ title: "Review", description: "Review code changes, diffs, or pull requests" },
|
|
678
1012
|
{ title: "Resume", description: "Resume a previously unfinished task" },
|
|
679
1013
|
{ title: "Subagents", description: "Manage running agents" },
|
|
680
|
-
{ title: "
|
|
681
|
-
{ title: "
|
|
682
|
-
{ title: "Close", description: "Close this menu" },
|
|
1014
|
+
{ title: "Settings", description: "LSP, Flant AI, and other configuration" },
|
|
1015
|
+
{ title: "Back", description: "Close this menu" },
|
|
683
1016
|
]);
|
|
684
|
-
if (!choice || choice === "
|
|
1017
|
+
if (!choice || choice === "Back") return undefined;
|
|
685
1018
|
|
|
686
1019
|
if (choice === "Debug") {
|
|
687
1020
|
const result = await showTaskTypeMenu(orchestrator, ctx, "debug", "Describe the task");
|
|
@@ -701,6 +1034,12 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
|
|
|
701
1034
|
continue;
|
|
702
1035
|
}
|
|
703
1036
|
|
|
1037
|
+
if (choice === "Review") {
|
|
1038
|
+
const result = await showReviewMenu(orchestrator, ctx);
|
|
1039
|
+
if (result === "started") return undefined;
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
704
1043
|
if (choice === "Resume") {
|
|
705
1044
|
const result = await showResumeMenu(orchestrator, ctx, undefined, "No paused tasks found.");
|
|
706
1045
|
if (result === "started") return undefined;
|
|
@@ -712,12 +1051,7 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
|
|
|
712
1051
|
continue;
|
|
713
1052
|
}
|
|
714
1053
|
|
|
715
|
-
|
|
716
|
-
await showLspMenu(ctx);
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
await showSettingsMenu(orchestrator, ctx);
|
|
1054
|
+
await showSettingsMenu(orchestrator, ctx, true);
|
|
721
1055
|
}
|
|
722
1056
|
}
|
|
723
1057
|
|
|
@@ -767,32 +1101,36 @@ export async function showActiveTaskMenu(
|
|
|
767
1101
|
|
|
768
1102
|
const waiting = step === "await_planners" || step === "await_reviewers";
|
|
769
1103
|
const { autoLabel, deepLabel } = getReviewLabels(orchestrator);
|
|
770
|
-
const
|
|
771
|
-
const
|
|
1104
|
+
const isReviewPhase = phase === "review";
|
|
1105
|
+
const hasPlannotator = phase === "plan" || phase === "implement" || isReviewPhase;
|
|
772
1106
|
|
|
773
1107
|
const opt = (title: string, description: string): OptionInput => ({ title, description });
|
|
774
1108
|
|
|
775
1109
|
const options: OptionInput[] = [];
|
|
1110
|
+
options.push(opt("Finish", "Complete, pause, or continue to next phase"));
|
|
776
1111
|
if (!waiting) {
|
|
777
|
-
options.push(opt("Approve & continue", "Advance to the next phase"));
|
|
778
1112
|
options.push(opt(autoLabel, "Run automated review with configured reviewers"));
|
|
779
1113
|
options.push(opt(deepLabel, "Run automated review with higher thinking level"));
|
|
780
1114
|
if (hasPlannotator) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1115
|
+
if (isReviewPhase) {
|
|
1116
|
+
options.push(opt("Review in Plannotator", "Open visual diff review in browser"));
|
|
1117
|
+
} else {
|
|
1118
|
+
options.push(opt("Review in Plannotator", "Open visual review in browser"));
|
|
1119
|
+
}
|
|
1120
|
+
if (!isReviewPhase) {
|
|
1121
|
+
options.push(opt("Review on my own", "Review manually, then continue"));
|
|
1122
|
+
}
|
|
787
1123
|
}
|
|
788
1124
|
}
|
|
789
|
-
options.push(opt("Abort", "Stop the task without completing"));
|
|
790
|
-
options.push(opt("Status", "Show current task phase, step, and timing"));
|
|
791
1125
|
options.push(opt("Subagents", "Manage running agents"));
|
|
792
|
-
options.push(opt("
|
|
1126
|
+
options.push(opt("Status", "Show current task phase, step, and timing"));
|
|
1127
|
+
options.push(opt("Settings", "LSP and other configuration"));
|
|
1128
|
+
options.push(opt("Back", "Return to the prompt and keep working"));
|
|
793
1129
|
|
|
794
1130
|
const choice = await selectOption(ctx, summary, options);
|
|
795
|
-
if (!choice
|
|
1131
|
+
if (!choice || choice === "Back") {
|
|
1132
|
+
return "";
|
|
1133
|
+
}
|
|
796
1134
|
|
|
797
1135
|
if (choice === "Status") {
|
|
798
1136
|
showStatus(orchestrator, ctx);
|
|
@@ -802,18 +1140,37 @@ export async function showActiveTaskMenu(
|
|
|
802
1140
|
await showSubagentsMenu(ctx);
|
|
803
1141
|
continue;
|
|
804
1142
|
}
|
|
805
|
-
if (choice === "
|
|
806
|
-
await
|
|
1143
|
+
if (choice === "Settings") {
|
|
1144
|
+
await showSettingsMenu(orchestrator, ctx, false);
|
|
807
1145
|
continue;
|
|
808
1146
|
}
|
|
809
|
-
if (choice === "Abort" || choice === "Finish") {
|
|
810
|
-
const text = await finishTask(orchestrator, ctx);
|
|
811
|
-
return mode === "tool" ? text : "";
|
|
812
|
-
}
|
|
813
1147
|
|
|
814
|
-
|
|
1148
|
+
if (mode === "command") {
|
|
1149
|
+
await abortCurrentWork(orchestrator, ctx);
|
|
1150
|
+
}
|
|
815
1151
|
|
|
816
|
-
if (choice === "
|
|
1152
|
+
if (choice === "Finish") {
|
|
1153
|
+
const canContinue = phase !== "implement" && !waiting;
|
|
1154
|
+
const continueLabel = phase === "plan" ? "Continue to implement" : "Continue to plan & implement";
|
|
1155
|
+
const finishOptions: OptionInput[] = [];
|
|
1156
|
+
if (canContinue) {
|
|
1157
|
+
finishOptions.push(opt(continueLabel, "Approve and advance to the next phase"));
|
|
1158
|
+
}
|
|
1159
|
+
finishOptions.push(opt("Complete", "Mark task as done and clean up"));
|
|
1160
|
+
finishOptions.push(opt("Pause", "Suspend task to resume later"));
|
|
1161
|
+
finishOptions.push(opt("Back", "Return to the previous menu"));
|
|
1162
|
+
|
|
1163
|
+
const finishChoice = await selectOption(ctx, "Finish", finishOptions);
|
|
1164
|
+
if (!finishChoice || finishChoice === "Back") continue;
|
|
1165
|
+
if (finishChoice === "Pause") {
|
|
1166
|
+
const text = await pauseTask(orchestrator, ctx);
|
|
1167
|
+
return mode === "tool" ? text : "";
|
|
1168
|
+
}
|
|
1169
|
+
if (finishChoice === "Complete") {
|
|
1170
|
+
const text = await finishTask(orchestrator, ctx);
|
|
1171
|
+
return mode === "tool" ? text : "";
|
|
1172
|
+
}
|
|
1173
|
+
finalizeReviewCycle(task);
|
|
817
1174
|
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
818
1175
|
if (!result.ok) return `Transition blocked: ${result.error}`;
|
|
819
1176
|
if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) return "";
|
|
@@ -822,6 +1179,14 @@ export async function showActiveTaskMenu(
|
|
|
822
1179
|
return "";
|
|
823
1180
|
}
|
|
824
1181
|
|
|
1182
|
+
if (choice === "Review in Plannotator" && isReviewPhase) {
|
|
1183
|
+
const text = await openReviewTaskInPlannotator(orchestrator);
|
|
1184
|
+
ctx.ui.notify(text, "info");
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
finalizeReviewCycle(task);
|
|
1189
|
+
|
|
825
1190
|
if (choice === autoLabel || choice === deepLabel || choice === "Review in Plannotator") {
|
|
826
1191
|
const kind = choice === autoLabel ? "auto" as const : choice === deepLabel ? "auto-deep" as const : "plannotator" as const;
|
|
827
1192
|
if (kind !== "plannotator" && !hasEnabledReviewers(orchestrator, kind)) {
|
|
@@ -845,15 +1210,6 @@ export async function showActiveTaskMenu(
|
|
|
845
1210
|
}
|
|
846
1211
|
return continueMessage;
|
|
847
1212
|
}
|
|
848
|
-
|
|
849
|
-
if (choice === "Back to prompt") {
|
|
850
|
-
if (phase === "plan") {
|
|
851
|
-
setStep(orchestrator, "synthesize");
|
|
852
|
-
} else {
|
|
853
|
-
setStep(orchestrator, "llm_work");
|
|
854
|
-
}
|
|
855
|
-
return continueMessage;
|
|
856
|
-
}
|
|
857
1213
|
}
|
|
858
1214
|
}
|
|
859
1215
|
|
|
@@ -13,12 +13,13 @@ function getLockfileFs(): typeof import("fs") | undefined {
|
|
|
13
13
|
}
|
|
14
14
|
import type { TimeoutConfig } from "./config.js";
|
|
15
15
|
|
|
16
|
-
export type TaskType = "implement" | "debug" | "brainstorm";
|
|
16
|
+
export type TaskType = "implement" | "debug" | "brainstorm" | "review";
|
|
17
17
|
|
|
18
18
|
export type ImplementPhase = "brainstorm" | "plan" | "implement" | "done";
|
|
19
19
|
export type DebugPhase = "debug" | "plan" | "implement" | "done";
|
|
20
20
|
export type BrainstormPhase = "brainstorm" | "plan" | "implement" | "done";
|
|
21
|
-
export type
|
|
21
|
+
export type ReviewPhase = "review" | "plan" | "implement" | "done";
|
|
22
|
+
export type Phase = ImplementPhase | DebugPhase | BrainstormPhase | ReviewPhase;
|
|
22
23
|
|
|
23
24
|
export interface TaskState {
|
|
24
25
|
phase: Phase;
|
|
@@ -31,6 +32,7 @@ export interface TaskState {
|
|
|
31
32
|
from: string | null;
|
|
32
33
|
description: string;
|
|
33
34
|
startedAt: string;
|
|
35
|
+
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface TaskInfo {
|
|
@@ -60,7 +62,7 @@ export function createTask(cwd: string, type: TaskType, description: string): st
|
|
|
60
62
|
mkdirSync(taskDir, { recursive: true });
|
|
61
63
|
|
|
62
64
|
const state: TaskState = {
|
|
63
|
-
phase: type === "implement" ? "brainstorm" : type === "debug" ? "debug" : "brainstorm",
|
|
65
|
+
phase: type === "implement" ? "brainstorm" : type === "debug" ? "debug" : type === "review" ? "review" : "brainstorm",
|
|
64
66
|
step: "llm_work",
|
|
65
67
|
reviewCycle: null,
|
|
66
68
|
reviewPass: 0,
|
|
@@ -77,10 +79,7 @@ export function loadTask(taskDir: string): TaskState {
|
|
|
77
79
|
const sp = taskStatePath(taskDir);
|
|
78
80
|
const raw = readFileSync(sp, "utf-8");
|
|
79
81
|
try {
|
|
80
|
-
const state = JSON.parse(raw) as TaskState
|
|
81
|
-
if (state.phase === "planning") {
|
|
82
|
-
state.phase = "plan";
|
|
83
|
-
}
|
|
82
|
+
const state = JSON.parse(raw) as TaskState;
|
|
84
83
|
return state;
|
|
85
84
|
} catch (err: any) {
|
|
86
85
|
throw new Error(`Failed to parse ${sp}: ${err.message}`);
|
|
@@ -95,7 +94,7 @@ export function listTasks(cwd: string, type?: TaskType): TaskInfo[] {
|
|
|
95
94
|
const base = stateDir(cwd);
|
|
96
95
|
if (!existsSync(base)) return [];
|
|
97
96
|
|
|
98
|
-
const types: TaskType[] = type ? [type] : ["implement", "debug", "brainstorm"];
|
|
97
|
+
const types: TaskType[] = type ? [type] : ["implement", "debug", "brainstorm", "review"];
|
|
99
98
|
const results: TaskInfo[] = [];
|
|
100
99
|
|
|
101
100
|
for (const t of types) {
|
|
@@ -196,7 +195,7 @@ export function taskName(taskDir: string): string {
|
|
|
196
195
|
const state = loadTask(taskDir);
|
|
197
196
|
let desc = state.description ?? "";
|
|
198
197
|
|
|
199
|
-
if (["implement", "debug", "brainstorm"].includes(desc)) {
|
|
198
|
+
if (["implement", "debug", "brainstorm", "review"].includes(desc)) {
|
|
200
199
|
const urPath = join(taskDir, "USER_REQUEST.md");
|
|
201
200
|
if (existsSync(urPath)) {
|
|
202
201
|
const content = readFileSync(urPath, "utf-8");
|