@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.
Files changed (61) hide show
  1. package/3p/pi-subagents/package.json +0 -1
  2. package/3p/pi-subagents/src/agent-manager.ts +1 -0
  3. package/3p/pi-subagents/src/index.ts +1 -0
  4. package/3p/pi-subagents/src/types.ts +8 -0
  5. package/extensions/orchestrator/agents/advisor.ts +2 -1
  6. package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
  7. package/extensions/orchestrator/agents/constraints.test.ts +43 -10
  8. package/extensions/orchestrator/agents/constraints.ts +26 -5
  9. package/extensions/orchestrator/agents/deep-debugger.ts +7 -6
  10. package/extensions/orchestrator/agents/explore.ts +1 -1
  11. package/extensions/orchestrator/agents/librarian.ts +1 -1
  12. package/extensions/orchestrator/agents/planner.ts +1 -1
  13. package/extensions/orchestrator/agents/prompts.test.ts +135 -1
  14. package/extensions/orchestrator/agents/reviewer.ts +2 -2
  15. package/extensions/orchestrator/agents/task.ts +6 -2
  16. package/extensions/orchestrator/agents/tool-routing.ts +22 -6
  17. package/extensions/orchestrator/assumptions.test.ts +77 -0
  18. package/extensions/orchestrator/assumptions.ts +85 -0
  19. package/extensions/orchestrator/cbm.ts +4 -1
  20. package/extensions/orchestrator/cbm.which.test.ts +92 -0
  21. package/extensions/orchestrator/command-handlers.ts +9 -1
  22. package/extensions/orchestrator/config.test.ts +1 -3
  23. package/extensions/orchestrator/config.ts +3 -4
  24. package/extensions/orchestrator/context.test.ts +2 -7
  25. package/extensions/orchestrator/context.ts +3 -3
  26. package/extensions/orchestrator/doctor.test.ts +35 -0
  27. package/extensions/orchestrator/doctor.ts +4 -2
  28. package/extensions/orchestrator/event-handlers.more.test.ts +185 -0
  29. package/extensions/orchestrator/event-handlers.test.ts +22 -2
  30. package/extensions/orchestrator/event-handlers.ts +156 -18
  31. package/extensions/orchestrator/flant-infra.more.test.ts +55 -0
  32. package/extensions/orchestrator/flant-infra.test.ts +0 -3
  33. package/extensions/orchestrator/flant-infra.ts +17 -7
  34. package/extensions/orchestrator/integration.test.ts +355 -173
  35. package/extensions/orchestrator/orchestrator.ts +3 -9
  36. package/extensions/orchestrator/phases/brainstorm.test.ts +25 -7
  37. package/extensions/orchestrator/phases/brainstorm.ts +35 -71
  38. package/extensions/orchestrator/phases/implementation.test.ts +22 -0
  39. package/extensions/orchestrator/phases/implementation.ts +6 -0
  40. package/extensions/orchestrator/phases/machine.test.ts +0 -28
  41. package/extensions/orchestrator/phases/machine.ts +0 -38
  42. package/extensions/orchestrator/phases/planning.test.ts +27 -1
  43. package/extensions/orchestrator/phases/planning.ts +1 -1
  44. package/extensions/orchestrator/phases/review-task.test.ts +7 -0
  45. package/extensions/orchestrator/phases/review-task.ts +1 -1
  46. package/extensions/orchestrator/phases/review.test.ts +47 -10
  47. package/extensions/orchestrator/phases/review.ts +36 -25
  48. package/extensions/orchestrator/pp-menu.from.test.ts +65 -0
  49. package/extensions/orchestrator/pp-menu.leaves.test.ts +17 -0
  50. package/extensions/orchestrator/pp-menu.test.ts +106 -49
  51. package/extensions/orchestrator/pp-menu.ts +280 -134
  52. package/extensions/orchestrator/pp-state-tools.ts +1 -1
  53. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +2 -2
  54. package/extensions/orchestrator/rate-limit-fallback.ts +1 -1
  55. package/extensions/orchestrator/state.test.ts +17 -17
  56. package/extensions/orchestrator/state.ts +35 -12
  57. package/package.json +6 -3
  58. package/scripts/lib/smoke-resolve.mjs +99 -0
  59. package/scripts/postinstall.mjs +93 -0
  60. package/scripts/test-package.sh +62 -0
  61. 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
- return execFileSync("which", ["codebase-memory-mcp"], { encoding: "utf-8", stdio: "pipe" }).trim() || null;
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" || next === "debug") {
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 and debug to brainstormReviewers", () => {
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" | "debug" | "brainstorm" | "review" | "quick";
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" || phase === "debug") return "brainstormReviewers";
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", "debug", "brainstorm", "review", "quick"];
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", "debug", { vendor: "openai", family: "gpt-mini", tier: "stupid" }),
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 both brainstorm and debug", () => {
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" | "debug" | "plan" | "implement" | "review";
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", "debug", "plan", "implement", "review"];
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/debug → brainstorm-reviews, plan → plan-reviews, else code-reviews).
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 out = execFileSync("which", [bin], { encoding: "utf-8", stdio: "pipe" }).trim();
57
- return out || null;
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
  }