@ilya-lesikov/pi-pi 0.11.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/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.test.ts +0 -3
- package/extensions/orchestrator/flant-infra.ts +0 -1
- 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 +279 -133
- 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 +2 -2
- package/scripts/postinstall.mjs +93 -0
- package/scripts/postinstall.sh +0 -18
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { openAssumptionsBanner, readAssumptions, terminalAssumptionsSummary } from "./assumptions.js";
|
|
6
|
+
|
|
7
|
+
function taskDirWith(content?: string): string {
|
|
8
|
+
const dir = mkdtempSync(join(tmpdir(), "pp-assumptions-"));
|
|
9
|
+
if (content !== undefined) {
|
|
10
|
+
mkdirSync(join(dir, "artifacts"), { recursive: true });
|
|
11
|
+
writeFileSync(join(dir, "artifacts", "ASSUMPTIONS.md"), content, "utf-8");
|
|
12
|
+
}
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("readAssumptions", () => {
|
|
17
|
+
it("treats an absent file as no assumptions", () => {
|
|
18
|
+
expect(readAssumptions(taskDirWith())).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("treats an explicit _No open assumptions._ sentinel as none", () => {
|
|
22
|
+
expect(readAssumptions(taskDirWith("# Assumptions\n\n_No open assumptions._\n"))).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("parses list items and classifies confidence and resolved status", () => {
|
|
26
|
+
const md = [
|
|
27
|
+
"# Assumptions",
|
|
28
|
+
"",
|
|
29
|
+
"- statement: API is idempotent; confidence: low; basis: docs; invalidation test: retry; decision impact: skip lock; status: open",
|
|
30
|
+
"- statement: config lives in root; confidence: high; basis: grep; status: resolved",
|
|
31
|
+
].join("\n");
|
|
32
|
+
const entries = readAssumptions(taskDirWith(md));
|
|
33
|
+
expect(entries).toHaveLength(2);
|
|
34
|
+
expect(entries[0].confidence).toBe("low");
|
|
35
|
+
expect(entries[0].resolved).toBe(false);
|
|
36
|
+
expect(entries[1].confidence).toBe("high");
|
|
37
|
+
expect(entries[1].resolved).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("openAssumptionsBanner", () => {
|
|
42
|
+
it("is null when there are no open low/med entries", () => {
|
|
43
|
+
expect(openAssumptionsBanner(taskDirWith())).toBeNull();
|
|
44
|
+
const resolvedHigh = "# A\n\n- statement: x; confidence: high; status: resolved\n";
|
|
45
|
+
expect(openAssumptionsBanner(taskDirWith(resolvedHigh))).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("surfaces a count when open low/med entries exist", () => {
|
|
49
|
+
const md = [
|
|
50
|
+
"# A",
|
|
51
|
+
"- statement: a; confidence: low; status: open",
|
|
52
|
+
"- statement: b; confidence: med; status: open",
|
|
53
|
+
"- statement: c; confidence: high; status: open",
|
|
54
|
+
].join("\n");
|
|
55
|
+
const banner = openAssumptionsBanner(taskDirWith(md));
|
|
56
|
+
expect(banner).toContain("2 unresolved");
|
|
57
|
+
expect(banner).toContain("ASSUMPTIONS.md");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("terminalAssumptionsSummary", () => {
|
|
62
|
+
it("states none when empty", () => {
|
|
63
|
+
expect(terminalAssumptionsSummary(taskDirWith())).toBe("Assumptions: none recorded.");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("lists every recorded assumption with status regardless of confidence", () => {
|
|
67
|
+
const md = [
|
|
68
|
+
"# A",
|
|
69
|
+
"- statement: a; confidence: low; status: open",
|
|
70
|
+
"- statement: b; confidence: high; status: resolved",
|
|
71
|
+
].join("\n");
|
|
72
|
+
const summary = terminalAssumptionsSummary(taskDirWith(md));
|
|
73
|
+
expect(summary).toContain("Assumptions recorded this run (2)");
|
|
74
|
+
expect(summary).toContain("[open, low]");
|
|
75
|
+
expect(summary).toContain("[resolved, high]");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const ASSUMPTIONS_PATH = "artifacts/ASSUMPTIONS.md";
|
|
5
|
+
|
|
6
|
+
// The optional artifact the main agent writes unverified assumptions to (see
|
|
7
|
+
// agents/constraints.ts assumptionsRule). Absence — or an explicit
|
|
8
|
+
// `_No open assumptions._` sentinel — means there are none. Entries are markdown
|
|
9
|
+
// list items carrying statement/confidence/basis/invalidation test/decision
|
|
10
|
+
// impact plus an open|resolved status marker.
|
|
11
|
+
export interface AssumptionEntry {
|
|
12
|
+
text: string;
|
|
13
|
+
confidence: "low" | "med" | "high" | "unknown";
|
|
14
|
+
resolved: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assumptionsFile(taskDir: string): string {
|
|
18
|
+
return join(taskDir, "artifacts", "ASSUMPTIONS.md");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Split the file into top-level list items (each assumption). A list item starts
|
|
22
|
+
// with `-`/`*`/`1.`; continuation/detail lines are folded into the current item
|
|
23
|
+
// so multi-line entries parse as one.
|
|
24
|
+
function parseEntries(content: string): AssumptionEntry[] {
|
|
25
|
+
const entries: AssumptionEntry[] = [];
|
|
26
|
+
let current: string[] | null = null;
|
|
27
|
+
const flush = () => {
|
|
28
|
+
if (!current) return;
|
|
29
|
+
const text = current.join(" ").trim();
|
|
30
|
+
if (text) entries.push(classify(text));
|
|
31
|
+
current = null;
|
|
32
|
+
};
|
|
33
|
+
for (const raw of content.split("\n")) {
|
|
34
|
+
const line = raw.trimEnd();
|
|
35
|
+
if (/^\s*([-*]|\d+\.)\s+/.test(line)) {
|
|
36
|
+
flush();
|
|
37
|
+
current = [line.replace(/^\s*([-*]|\d+\.)\s+/, "")];
|
|
38
|
+
} else if (current && line.trim()) {
|
|
39
|
+
current.push(line.trim());
|
|
40
|
+
} else {
|
|
41
|
+
flush();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
flush();
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function classify(text: string): AssumptionEntry {
|
|
49
|
+
const lower = text.toLowerCase();
|
|
50
|
+
const resolved = /\bresolved\b/.test(lower) && !/\bstatus:\s*open\b/.test(lower);
|
|
51
|
+
let confidence: AssumptionEntry["confidence"] = "unknown";
|
|
52
|
+
if (/confidence:?\s*low\b/.test(lower) || /\blow confidence\b/.test(lower)) confidence = "low";
|
|
53
|
+
else if (/confidence:?\s*med(ium)?\b/.test(lower) || /\bmed(ium)? confidence\b/.test(lower)) confidence = "med";
|
|
54
|
+
else if (/confidence:?\s*high\b/.test(lower) || /\bhigh confidence\b/.test(lower)) confidence = "high";
|
|
55
|
+
return { text, confidence, resolved };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readAssumptions(taskDir: string): AssumptionEntry[] {
|
|
59
|
+
const file = assumptionsFile(taskDir);
|
|
60
|
+
if (!existsSync(file)) return [];
|
|
61
|
+
const content = readFileSync(file, "utf-8");
|
|
62
|
+
if (/^\s*_no open assumptions\._\s*$/im.test(content) && parseEntries(content).length === 0) return [];
|
|
63
|
+
return parseEntries(content);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A one-line /pp gate banner when there are open low/med-confidence assumptions
|
|
67
|
+
// the user should double-check; null when there is nothing worth surfacing.
|
|
68
|
+
export function openAssumptionsBanner(taskDir: string): string | null {
|
|
69
|
+
const open = readAssumptions(taskDir).filter((e) => !e.resolved && (e.confidence === "low" || e.confidence === "med"));
|
|
70
|
+
if (open.length === 0) return null;
|
|
71
|
+
return `⚠ ${open.length} unresolved low/medium-confidence assumption${open.length === 1 ? "" : "s"} recorded — review ${ASSUMPTIONS_PATH} before advancing.`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// End-of-run summary for an autonomous task that never hit a /pp gate: lists every
|
|
75
|
+
// recorded assumption (open and resolved, all confidence levels), or states none.
|
|
76
|
+
export function terminalAssumptionsSummary(taskDir: string): string {
|
|
77
|
+
const entries = readAssumptions(taskDir);
|
|
78
|
+
if (entries.length === 0) return "Assumptions: none recorded.";
|
|
79
|
+
const lines = entries.map((e) => {
|
|
80
|
+
const status = e.resolved ? "resolved" : "open";
|
|
81
|
+
const conf = e.confidence === "unknown" ? "" : `, ${e.confidence}`;
|
|
82
|
+
return `- [${status}${conf}] ${e.text}`;
|
|
83
|
+
});
|
|
84
|
+
return [`Assumptions recorded this run (${entries.length}):`, ...lines].join("\n");
|
|
85
|
+
}
|
|
@@ -6,7 +6,10 @@ import { getLogger } from "./log.js";
|
|
|
6
6
|
|
|
7
7
|
function findCbmBin(): string | null {
|
|
8
8
|
try {
|
|
9
|
-
|
|
9
|
+
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
10
|
+
const out = execFileSync(whichCmd, ["codebase-memory-mcp"], { encoding: "utf-8", stdio: "pipe" });
|
|
11
|
+
const first = out.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
|
|
12
|
+
return first || null;
|
|
10
13
|
} catch {
|
|
11
14
|
return null;
|
|
12
15
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
execFileSync: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("child_process", async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal<typeof import("child_process")>();
|
|
9
|
+
return { ...actual, execFileSync: mocks.execFileSync };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
import { registerCbmTools } from "./cbm.js";
|
|
13
|
+
|
|
14
|
+
const CBM_BIN_KEY = Symbol.for("pi-pi:cbm-bin");
|
|
15
|
+
const CBM_DAEMON_KEY = Symbol.for("pi-pi:cbm-daemon");
|
|
16
|
+
|
|
17
|
+
function setPlatform(platform: NodeJS.Platform) {
|
|
18
|
+
Object.defineProperty(process, "platform", { value: platform, configurable: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function seedFakeDaemon() {
|
|
22
|
+
(globalThis as any)[CBM_DAEMON_KEY] = {
|
|
23
|
+
ensureIndexed: vi.fn().mockResolvedValue("repo"),
|
|
24
|
+
callTool: vi.fn().mockResolvedValue({}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("cbm platform-aware executable resolution", () => {
|
|
29
|
+
const originalPlatform = process.platform;
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
setPlatform(originalPlatform);
|
|
33
|
+
(globalThis as any)[CBM_BIN_KEY] = undefined;
|
|
34
|
+
(globalThis as any)[CBM_DAEMON_KEY] = undefined;
|
|
35
|
+
mocks.execFileSync.mockReset();
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("uses `which` on posix and resolves the trimmed path", () => {
|
|
40
|
+
setPlatform("linux");
|
|
41
|
+
seedFakeDaemon();
|
|
42
|
+
mocks.execFileSync.mockImplementation((cmd: string, args: string[]) => {
|
|
43
|
+
if (cmd === "which" && args[0] === "codebase-memory-mcp") return "/usr/bin/codebase-memory-mcp\n";
|
|
44
|
+
throw new Error(`unexpected: ${cmd}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const registered = registerCbmTools({ registerTool: vi.fn() } as any, "/repo");
|
|
48
|
+
|
|
49
|
+
expect(registered).toBe(true);
|
|
50
|
+
expect(mocks.execFileSync).toHaveBeenCalledWith("which", ["codebase-memory-mcp"], expect.anything());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns unavailable on posix when `which` throws (not found)", () => {
|
|
54
|
+
setPlatform("linux");
|
|
55
|
+
mocks.execFileSync.mockImplementation(() => {
|
|
56
|
+
throw new Error("not found");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const registered = registerCbmTools({ registerTool: vi.fn() } as any, "/repo");
|
|
60
|
+
|
|
61
|
+
expect(registered).toBe(false);
|
|
62
|
+
expect(mocks.execFileSync).toHaveBeenCalledWith("which", ["codebase-memory-mcp"], expect.anything());
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("uses `where` on win32 and takes the first non-empty line", () => {
|
|
66
|
+
setPlatform("win32");
|
|
67
|
+
seedFakeDaemon();
|
|
68
|
+
mocks.execFileSync.mockImplementation((cmd: string, args: string[]) => {
|
|
69
|
+
if (cmd === "where" && args[0] === "codebase-memory-mcp") {
|
|
70
|
+
return "C:\\bin\\codebase-memory-mcp.exe\r\nC:\\other\\codebase-memory-mcp.exe\r\n";
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`unexpected: ${cmd}`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const registered = registerCbmTools({ registerTool: vi.fn() } as any, "/repo");
|
|
76
|
+
|
|
77
|
+
expect(registered).toBe(true);
|
|
78
|
+
expect(mocks.execFileSync).toHaveBeenCalledWith("where", ["codebase-memory-mcp"], expect.anything());
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns unavailable on win32 when `where` throws (not found)", () => {
|
|
82
|
+
setPlatform("win32");
|
|
83
|
+
mocks.execFileSync.mockImplementation(() => {
|
|
84
|
+
throw new Error("INFO: Could not find files");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const registered = registerCbmTools({ registerTool: vi.fn() } as any, "/repo");
|
|
88
|
+
|
|
89
|
+
expect(registered).toBe(false);
|
|
90
|
+
expect(mocks.execFileSync).toHaveBeenCalledWith("where", ["codebase-memory-mcp"], expect.anything());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -6,6 +6,7 @@ import { spawnPlanners } from "./phases/planning.js";
|
|
|
6
6
|
import { Orchestrator } from "./orchestrator.js";
|
|
7
7
|
import { groupFilesByRepo } from "./repo-utils.js";
|
|
8
8
|
import { getEffectiveMode, saveTask } from "./state.js";
|
|
9
|
+
import { terminalAssumptionsSummary } from "./assumptions.js";
|
|
9
10
|
import { getLogger } from "./log.js";
|
|
10
11
|
import { handleSpawnResult } from "./spawn-cleanup.js";
|
|
11
12
|
|
|
@@ -97,7 +98,7 @@ export async function transitionToNextPhase(
|
|
|
97
98
|
orchestrator.active.state.step = "spawn_planners";
|
|
98
99
|
} else if (next === "implement") {
|
|
99
100
|
orchestrator.active.state.step = "llm_work";
|
|
100
|
-
} else if (next === "brainstorm"
|
|
101
|
+
} else if (next === "brainstorm") {
|
|
101
102
|
orchestrator.active.state.step = "llm_work";
|
|
102
103
|
} else if (next === "done") {
|
|
103
104
|
orchestrator.active.state.step = null;
|
|
@@ -111,6 +112,13 @@ export async function transitionToNextPhase(
|
|
|
111
112
|
const name = orchestrator.active.description;
|
|
112
113
|
const type = orchestrator.active.type;
|
|
113
114
|
|
|
115
|
+
// Terminal assumptions summary (#B/e): an autonomous run can reach done without
|
|
116
|
+
// ever hitting a /pp gate (where the banner surfaces), so emit the full list of
|
|
117
|
+
// recorded assumptions once here. Read before cleanup while the task dir is live.
|
|
118
|
+
if (getEffectiveMode(orchestrator.active.state) === "autonomous") {
|
|
119
|
+
ctx.ui.notify(terminalAssumptionsSummary(orchestrator.active.dir), "info");
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
orchestrator.abortAllSubagents();
|
|
115
123
|
unregisterAgentDefinitions(orchestrator.pi);
|
|
116
124
|
await orchestrator.cleanupActive();
|
|
@@ -203,7 +203,6 @@ describe("loadConfig", () => {
|
|
|
203
203
|
|
|
204
204
|
expect(config.agents.orchestrators.implement.model).toBe("custom/implement");
|
|
205
205
|
expect(config.agents.orchestrators.plan.model).toBe("anthropic/claude-opus-latest");
|
|
206
|
-
expect(config.agents.orchestrators.debug.model).toBe("openai/gpt-latest");
|
|
207
206
|
const planners = resolvePreset(config, "planners");
|
|
208
207
|
expect(planners.opus.enabled).toBe(false);
|
|
209
208
|
expect(planners.opus.model).toBe("anthropic/claude-opus-latest");
|
|
@@ -553,9 +552,8 @@ describe("config write helpers", () => {
|
|
|
553
552
|
});
|
|
554
553
|
|
|
555
554
|
describe("reviewPresetGroupForPhase", () => {
|
|
556
|
-
it("routes brainstorm
|
|
555
|
+
it("routes brainstorm to brainstormReviewers", () => {
|
|
557
556
|
expect(reviewPresetGroupForPhase("brainstorm")).toBe("brainstormReviewers");
|
|
558
|
-
expect(reviewPresetGroupForPhase("debug")).toBe("brainstormReviewers");
|
|
559
557
|
});
|
|
560
558
|
|
|
561
559
|
it("routes plan to planReviewers", () => {
|
|
@@ -6,7 +6,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import { isValidLogLevel, getLogger, type LogLevel } from "./log.js";
|
|
7
7
|
|
|
8
8
|
export type DurationValue = string | number;
|
|
9
|
-
export type OrchestratorRole = "implement" | "plan" | "
|
|
9
|
+
export type OrchestratorRole = "implement" | "plan" | "brainstorm" | "review" | "quick";
|
|
10
10
|
export type SimpleSubagentRole = "explore" | "librarian" | "task";
|
|
11
11
|
export type PresetGroupKey = "planners" | "codeReviewers" | "planReviewers" | "brainstormReviewers";
|
|
12
12
|
// Dynamic on-demand subagent pools (item 2): advisor / reviewer / deep-debugger
|
|
@@ -105,7 +105,7 @@ export type PresetGroup = PresetGroupKey;
|
|
|
105
105
|
export type VariantConfig = PresetAgentConfig;
|
|
106
106
|
|
|
107
107
|
export function reviewPresetGroupForPhase(phase: string): PresetGroupKey {
|
|
108
|
-
if (phase === "brainstorm"
|
|
108
|
+
if (phase === "brainstorm") return "brainstormReviewers";
|
|
109
109
|
if (phase === "plan") return "planReviewers";
|
|
110
110
|
return "codeReviewers";
|
|
111
111
|
}
|
|
@@ -113,7 +113,7 @@ export type TimeoutConfig = NormalizedPiPiConfig["performance"]["internals"];
|
|
|
113
113
|
|
|
114
114
|
export const PRESET_GROUPS = ["planners", "codeReviewers", "planReviewers", "brainstormReviewers"] as const;
|
|
115
115
|
|
|
116
|
-
const ORCHESTRATOR_ROLES: OrchestratorRole[] = ["implement", "plan", "
|
|
116
|
+
const ORCHESTRATOR_ROLES: OrchestratorRole[] = ["implement", "plan", "brainstorm", "review", "quick"];
|
|
117
117
|
const SIMPLE_SUBAGENT_ROLES: SimpleSubagentRole[] = ["explore", "librarian", "task"];
|
|
118
118
|
|
|
119
119
|
const DEFAULT_CONFIG: PiPiConfig = {
|
|
@@ -129,7 +129,6 @@ const DEFAULT_CONFIG: PiPiConfig = {
|
|
|
129
129
|
orchestrators: {
|
|
130
130
|
implement: { model: "anthropic/claude-opus-latest", thinking: "high" },
|
|
131
131
|
plan: { model: "anthropic/claude-opus-latest", thinking: "high" },
|
|
132
|
-
debug: { model: "openai/gpt-latest", thinking: "high" },
|
|
133
132
|
brainstorm: { model: "anthropic/claude-opus-latest", thinking: "high" },
|
|
134
133
|
review: { model: "anthropic/claude-opus-latest", thinking: "high" },
|
|
135
134
|
quick: { model: "anthropic/claude-opus-latest", thinking: "high" },
|
|
@@ -189,7 +189,7 @@ describe("loadContextFiles", () => {
|
|
|
189
189
|
loadContextFiles(cwd, "codeReviewer", "context", "review", { vendor: "google", family: "gpt-mini", tier: "stupid" }),
|
|
190
190
|
).toEqual([]);
|
|
191
191
|
expect(
|
|
192
|
-
loadContextFiles(cwd, "codeReviewer", "context", "
|
|
192
|
+
loadContextFiles(cwd, "codeReviewer", "context", "plan", { vendor: "openai", family: "gpt-mini", tier: "stupid" }),
|
|
193
193
|
).toEqual([]);
|
|
194
194
|
});
|
|
195
195
|
|
|
@@ -530,17 +530,12 @@ describe("context regressions", () => {
|
|
|
530
530
|
]);
|
|
531
531
|
});
|
|
532
532
|
|
|
533
|
-
it("loadPhaseReviewOutputs reads brainstorm-reviews for
|
|
533
|
+
it("loadPhaseReviewOutputs reads brainstorm-reviews for brainstorm", () => {
|
|
534
534
|
const taskDir = makeTempDir();
|
|
535
535
|
const brainstormReviewsDir = join(taskDir, "brainstorm-reviews");
|
|
536
536
|
mkdirSync(brainstormReviewsDir, { recursive: true });
|
|
537
537
|
writeFileSync(join(brainstormReviewsDir, "001_alpha_round-1.md"), "artifact review", "utf-8");
|
|
538
538
|
|
|
539
|
-
// Debug must resolve to the brainstorm-reviews dir (regression: it previously
|
|
540
|
-
// fell through to the empty code-reviews dir, discarding reviewer outputs).
|
|
541
|
-
expect(loadPhaseReviewOutputs(taskDir, "debug", 1).map((r) => r.name)).toEqual([
|
|
542
|
-
"001_alpha_round-1.md",
|
|
543
|
-
]);
|
|
544
539
|
expect(loadPhaseReviewOutputs(taskDir, "brainstorm", 1).map((r) => r.name)).toEqual([
|
|
545
540
|
"001_alpha_round-1.md",
|
|
546
541
|
]);
|
|
@@ -10,7 +10,7 @@ import { reviewPresetGroupForPhase } from "./config.js";
|
|
|
10
10
|
type AgentType = "main" | "explore" | "librarian" | "planner" | "planReviewer" | "task" | "codeReviewer" | "brainstormReviewer" | "advisor" | "deep-debugger" | "reviewer";
|
|
11
11
|
type AgentGroup = "all" | "subagents";
|
|
12
12
|
type InjectMode = "system" | "context";
|
|
13
|
-
type PhaseFilter = "brainstorm" | "
|
|
13
|
+
type PhaseFilter = "brainstorm" | "plan" | "implement" | "review";
|
|
14
14
|
type VendorFilter = "anthropic" | "openai" | "google" | "unknown";
|
|
15
15
|
type FamilyFilter = "opus" | "fable" | "sonnet" | "haiku" | "gpt" | "gpt-mini" | "gemini-pro" | "gemini-flash" | "unknown";
|
|
16
16
|
type TierFilter = "stupid" | "regular" | "smart" | "xsmart" | "unknown";
|
|
@@ -34,7 +34,7 @@ interface Frontmatter {
|
|
|
34
34
|
const VALID_INJECT_MODES: readonly string[] = ["system", "context"];
|
|
35
35
|
const VALID_AGENTS: readonly string[] = ["main", "explore", "librarian", "planner", "planReviewer", "task", "codeReviewer", "brainstormReviewer", "advisor", "deep-debugger", "reviewer"];
|
|
36
36
|
const VALID_AGENT_GROUPS: readonly string[] = ["all", "subagents"];
|
|
37
|
-
const VALID_PHASES: readonly string[] = ["brainstorm", "
|
|
37
|
+
const VALID_PHASES: readonly string[] = ["brainstorm", "plan", "implement", "review"];
|
|
38
38
|
const VALID_VENDORS: readonly string[] = ["anthropic", "openai", "google", "unknown"];
|
|
39
39
|
const VALID_FAMILIES: readonly string[] = ["opus", "fable", "sonnet", "haiku", "gpt", "gpt-mini", "gemini-pro", "gemini-flash", "unknown"];
|
|
40
40
|
const VALID_TIERS: readonly string[] = ["stupid", "regular", "smart", "xsmart", "unknown"];
|
|
@@ -388,7 +388,7 @@ export function loadPlanReviewOutputs(taskDir: string, pass: number): { name: st
|
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
// Single source for phase→review-output loading, mirroring reviewPresetGroupForPhase
|
|
391
|
-
// (brainstorm
|
|
391
|
+
// (brainstorm → brainstorm-reviews, plan → plan-reviews, else code-reviews).
|
|
392
392
|
// Imported by both the /pp menu and the review-cycle completion path so the two
|
|
393
393
|
// cannot drift.
|
|
394
394
|
export function loadPhaseReviewOutputs(taskDir: string, phase: string, pass: number): { name: string; content: string }[] {
|
|
@@ -381,6 +381,41 @@ describe("runDoctor", () => {
|
|
|
381
381
|
expect(report).toContain("gh: /usr/local/bin/gh");
|
|
382
382
|
});
|
|
383
383
|
|
|
384
|
+
it("resolves binaries via `where` on win32, taking the first line", async () => {
|
|
385
|
+
const originalPlatform = process.platform;
|
|
386
|
+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
387
|
+
const cwd = makeTempDir("pi-pi-doctor-win-");
|
|
388
|
+
const ctx = createCtx();
|
|
389
|
+
const orchestrator = {
|
|
390
|
+
cwd,
|
|
391
|
+
config: createConfig(),
|
|
392
|
+
active: null,
|
|
393
|
+
} as any;
|
|
394
|
+
|
|
395
|
+
mocks.execFileSync.mockImplementation((command: string, args: string[]) => {
|
|
396
|
+
if (command !== "where") throw new Error(`Unexpected command: ${command}`);
|
|
397
|
+
const bin = args[0];
|
|
398
|
+
if (bin === "git") return "C:\\Program Files\\Git\\cmd\\git.exe\r\nC:\\other\\git.exe\r\n";
|
|
399
|
+
if (bin === "gh") return "C:\\bin\\gh.exe\r\n";
|
|
400
|
+
if (bin === "codebase-memory-mcp") throw new Error("INFO: Could not find files");
|
|
401
|
+
if (bin === "sg") throw new Error("INFO: Could not find files");
|
|
402
|
+
if (bin === "node") return "C:\\Program Files\\nodejs\\node.exe\r\n";
|
|
403
|
+
if (bin === "npm") return "C:\\Program Files\\nodejs\\npm.cmd\r\n";
|
|
404
|
+
throw new Error(`${bin} not found`);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
await runDoctor(orchestrator, ctx);
|
|
409
|
+
} finally {
|
|
410
|
+
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const [report] = ctx.ui.notify.mock.calls[0] as [string, string];
|
|
414
|
+
expect(report).toContain("git: C:\\Program Files\\Git\\cmd\\git.exe");
|
|
415
|
+
expect(report).not.toContain("C:\\other\\git.exe");
|
|
416
|
+
expect(report).toContain("gh: C:\\bin\\gh.exe");
|
|
417
|
+
});
|
|
418
|
+
|
|
384
419
|
it("checks current working directory as repo when no task is active", async () => {
|
|
385
420
|
const cwd = makeTempDir("pi-pi-doctor-cwd-repo-");
|
|
386
421
|
mkdirSync(join(cwd, ".git"), { recursive: true });
|
|
@@ -53,8 +53,10 @@ function flantCacheDir(): string {
|
|
|
53
53
|
|
|
54
54
|
function which(bin: string): string | null {
|
|
55
55
|
try {
|
|
56
|
-
const
|
|
57
|
-
|
|
56
|
+
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
57
|
+
const out = execFileSync(whichCmd, [bin], { encoding: "utf-8", stdio: "pipe" });
|
|
58
|
+
const first = out.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
|
|
59
|
+
return first || null;
|
|
58
60
|
} catch {
|
|
59
61
|
return null;
|
|
60
62
|
}
|