@ilya-lesikov/pi-pi 0.9.0 → 0.11.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 (108) hide show
  1. package/3p/pi-ask-user/index.ts +71 -124
  2. package/3p/pi-ask-user/single-select-layout.ts +3 -14
  3. package/3p/pi-subagents/package.json +12 -7
  4. package/3p/pi-subagents/src/agent-manager.ts +243 -79
  5. package/3p/pi-subagents/src/agent-runner.ts +501 -365
  6. package/3p/pi-subagents/src/agent-types.ts +46 -57
  7. package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
  8. package/3p/pi-subagents/src/custom-agents.ts +30 -6
  9. package/3p/pi-subagents/src/default-agents.ts +25 -43
  10. package/3p/pi-subagents/src/enabled-models.ts +180 -0
  11. package/3p/pi-subagents/src/index.ts +599 -839
  12. package/3p/pi-subagents/src/model-resolver.ts +26 -7
  13. package/3p/pi-subagents/src/output-file.ts +21 -2
  14. package/3p/pi-subagents/src/prompts.ts +17 -3
  15. package/3p/pi-subagents/src/schedule-store.ts +153 -0
  16. package/3p/pi-subagents/src/schedule.ts +365 -0
  17. package/3p/pi-subagents/src/settings.ts +261 -0
  18. package/3p/pi-subagents/src/skill-loader.ts +77 -54
  19. package/3p/pi-subagents/src/status-note.ts +25 -0
  20. package/3p/pi-subagents/src/types.ts +97 -6
  21. package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
  22. package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
  23. package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
  24. package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
  25. package/3p/pi-subagents/src/usage.ts +60 -0
  26. package/3p/pi-subagents/src/worktree.ts +49 -20
  27. package/extensions/orchestrator/agents/advisor.ts +13 -8
  28. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
  29. package/extensions/orchestrator/agents/code-reviewer.ts +8 -3
  30. package/extensions/orchestrator/agents/constraints.test.ts +26 -0
  31. package/extensions/orchestrator/agents/constraints.ts +12 -2
  32. package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
  33. package/extensions/orchestrator/agents/explore.ts +12 -6
  34. package/extensions/orchestrator/agents/librarian.ts +13 -5
  35. package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
  36. package/extensions/orchestrator/agents/planner.ts +8 -3
  37. package/extensions/orchestrator/agents/pools.test.ts +56 -0
  38. package/extensions/orchestrator/agents/prompts.test.ts +52 -10
  39. package/extensions/orchestrator/agents/registry.test.ts +245 -0
  40. package/extensions/orchestrator/agents/registry.ts +145 -10
  41. package/extensions/orchestrator/agents/reviewer.ts +14 -8
  42. package/extensions/orchestrator/agents/task.ts +12 -6
  43. package/extensions/orchestrator/agents/tool-routing.ts +213 -85
  44. package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
  45. package/extensions/orchestrator/ast-search.test.ts +124 -0
  46. package/extensions/orchestrator/billing-spoof.test.ts +67 -0
  47. package/extensions/orchestrator/billing-spoof.ts +95 -0
  48. package/extensions/orchestrator/cbm.more.test.ts +358 -0
  49. package/extensions/orchestrator/command-handlers.ts +6 -0
  50. package/extensions/orchestrator/commands.test.ts +15 -2
  51. package/extensions/orchestrator/commands.ts +1 -1
  52. package/extensions/orchestrator/config.test.ts +89 -1
  53. package/extensions/orchestrator/config.ts +102 -19
  54. package/extensions/orchestrator/context.test.ts +46 -0
  55. package/extensions/orchestrator/context.ts +18 -5
  56. package/extensions/orchestrator/custom-footer.test.ts +24 -10
  57. package/extensions/orchestrator/custom-footer.ts +4 -2
  58. package/extensions/orchestrator/doctor.more.test.ts +315 -0
  59. package/extensions/orchestrator/doctor.ts +6 -2
  60. package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
  61. package/extensions/orchestrator/event-handlers.test.ts +96 -9
  62. package/extensions/orchestrator/event-handlers.ts +344 -151
  63. package/extensions/orchestrator/exa.more.test.ts +118 -0
  64. package/extensions/orchestrator/flant-infra.more.test.ts +381 -0
  65. package/extensions/orchestrator/flant-infra.test.ts +127 -0
  66. package/extensions/orchestrator/flant-infra.ts +126 -41
  67. package/extensions/orchestrator/index.test.ts +76 -0
  68. package/extensions/orchestrator/index.ts +2 -0
  69. package/extensions/orchestrator/integration.test.ts +183 -65
  70. package/extensions/orchestrator/model-registry.test.ts +2 -1
  71. package/extensions/orchestrator/model-registry.ts +12 -2
  72. package/extensions/orchestrator/orchestrator.test.ts +67 -0
  73. package/extensions/orchestrator/orchestrator.ts +119 -27
  74. package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
  75. package/extensions/orchestrator/phases/brainstorm.ts +43 -6
  76. package/extensions/orchestrator/phases/implementation.ts +2 -0
  77. package/extensions/orchestrator/phases/machine.test.ts +17 -1
  78. package/extensions/orchestrator/phases/machine.ts +4 -1
  79. package/extensions/orchestrator/phases/planning.test.ts +47 -1
  80. package/extensions/orchestrator/phases/planning.ts +18 -3
  81. package/extensions/orchestrator/phases/review-task.ts +5 -0
  82. package/extensions/orchestrator/phases/review.test.ts +10 -0
  83. package/extensions/orchestrator/phases/review.ts +22 -7
  84. package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
  85. package/extensions/orchestrator/phases/verdict.ts +6 -5
  86. package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
  87. package/extensions/orchestrator/plannotator.test.ts +38 -1
  88. package/extensions/orchestrator/plannotator.ts +50 -3
  89. package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
  90. package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
  91. package/extensions/orchestrator/pp-menu.test.ts +114 -7
  92. package/extensions/orchestrator/pp-menu.ts +580 -91
  93. package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
  94. package/extensions/orchestrator/pp-state-tools.ts +65 -0
  95. package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
  96. package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
  97. package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
  98. package/extensions/orchestrator/review-files.test.ts +26 -0
  99. package/extensions/orchestrator/review-files.ts +3 -0
  100. package/extensions/orchestrator/state.test.ts +73 -1
  101. package/extensions/orchestrator/state.ts +95 -23
  102. package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
  103. package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
  104. package/extensions/orchestrator/validate-artifacts.ts +1 -1
  105. package/package.json +9 -2
  106. package/scripts/lib/smoke-resolve.mjs +99 -0
  107. package/scripts/test-3p.sh +52 -0
  108. package/scripts/test-package.sh +62 -0
@@ -7,6 +7,7 @@ import {
7
7
  loadTask,
8
8
  saveTask,
9
9
  lockTask,
10
+ validateFromPath,
10
11
  getEffectivePhaseMode,
11
12
  type TaskType,
12
13
  type TaskMode,
@@ -19,7 +20,7 @@ import { planningSystemPrompt, spawnPlanners } from "./phases/planning.js";
19
20
  import { implementationSystemPrompt } from "./phases/implementation.js";
20
21
  import { reviewSystemPrompt as reviewCycleSystemPrompt } from "./phases/review.js";
21
22
  import { reviewSystemPrompt as reviewTaskSystemPrompt } from "./phases/review-task.js";
22
- import { registerAgentDefinitions, unregisterAgentDefinitions } from "./agents/registry.js";
23
+ import { registerAgentDefinitions, unregisterAgentDefinitions, encodePoolVariant } from "./agents/registry.js";
23
24
  import { createExploreAgent } from "./agents/explore.js";
24
25
  import { createLibrarianAgent } from "./agents/librarian.js";
25
26
  import { createTaskAgent } from "./agents/task.js";
@@ -57,6 +58,10 @@ export interface ActiveTask {
57
58
  export class Orchestrator {
58
59
  active: ActiveTask | null = null;
59
60
  config!: NormalizedPiPiConfig;
61
+ // Non-null when loadConfig threw on session_start. While set, only a minimal
62
+ // read-only /pp path is available and `config` holds a rendering-only default
63
+ // fallback (NOT the user's config, which is invalid).
64
+ configError: string | null = null;
60
65
  cwd = "";
61
66
  spawnedAgentIds = new Set<string>();
62
67
  agentDescriptions = new Map<string, string>();
@@ -126,6 +131,12 @@ export class Orchestrator {
126
131
  // legitimately parked on a human is not aborted. Set on dialogue open, cleared
127
132
  // in finally on every exit (resolve, ESC/cancel, error).
128
133
  interactivePromptOpen = false;
134
+ // One-shot review-ready instruction (item 9). While a menu/ask turn is live,
135
+ // the review-ready banner must NOT be queued as a followUp (ESC/abort flushes
136
+ // the queue into the editor input — the stray-banner bug). Instead it is
137
+ // stashed here and delivered as a FRESH idle-gated turn once the dialogue
138
+ // closes. Cleared after delivery so it fires exactly once.
139
+ pendingReviewReady: string | null = null;
129
140
  // Set SYNCHRONOUSLY the moment a sub-429 is detected (before any async dialog),
130
141
  // and cleared once the decision resolves. The autonomous planner/reviewer
131
142
  // auto-retry consults this to avoid re-spawning a failed variant on the still-
@@ -288,6 +299,30 @@ export class Orchestrator {
288
299
  attempt(0);
289
300
  }
290
301
 
302
+ // Deliver the review-ready instruction WITHOUT leaking it into the editor on
303
+ // ESC/abort (item 9). If a menu/ask dialogue is live, the followUp queue would
304
+ // be dumped into the prompt input by restoreQueuedMessagesToEditor on abort —
305
+ // so stash the message and deliver it as a fresh idle-gated turn once the
306
+ // dialogue closes (flushPendingReviewReady). If nothing is open, deliver now.
307
+ deliverReviewReady(text: string): void {
308
+ if (this.interactivePromptOpen) {
309
+ this.pendingReviewReady = text;
310
+ return;
311
+ }
312
+ this.pendingReviewReady = null;
313
+ this.sendUserMessageWhenIdle(text, this.activeTaskToken);
314
+ }
315
+
316
+ // Deliver a stashed review-ready instruction (if any) as a fresh idle-gated
317
+ // turn. One-shot: cleared before sending so it never re-fires on a later idle.
318
+ // Called when a menu/ask dialogue closes (including ESC/abort).
319
+ flushPendingReviewReady(): void {
320
+ const text = this.pendingReviewReady;
321
+ if (!text || !this.active) return;
322
+ this.pendingReviewReady = null;
323
+ this.sendUserMessageWhenIdle(text, this.activeTaskToken);
324
+ }
325
+
291
326
  truncateResult(result: string): string {
292
327
  const trimmed = result.trim();
293
328
  if (!trimmed) return "";
@@ -474,13 +509,28 @@ export class Orchestrator {
474
509
  setLogLevel(this.config.general.logLevel);
475
510
  ensureGitignore(this.cwd);
476
511
 
512
+ // Validate the fork source BEFORE creating the new task so an invalid or
513
+ // escaping path cannot leave a half-created task behind. validateFromPath
514
+ // resolves against .pp/state/, so feed it the stateDir-relative form (the
515
+ // same shape stored in state.from) rather than the absolute dir.
516
+ let validatedFromDir: string | undefined;
517
+ if (fromTaskDir) {
518
+ const fromRel = relative(join(this.cwd, ".pp", "state"), fromTaskDir);
519
+ const validation = validateFromPath(this.cwd, fromRel);
520
+ if (!validation.ok) {
521
+ ctx.ui.notify(validation.reason, "error");
522
+ return;
523
+ }
524
+ validatedFromDir = validation.dir;
525
+ }
526
+
477
527
  const dir = createTask(this.cwd, type, description, mode);
478
528
  const state = loadTask(dir);
479
529
 
480
- if (fromTaskDir) {
481
- const srcUr = join(fromTaskDir, "USER_REQUEST.md");
482
- const srcRes = join(fromTaskDir, "RESEARCH.md");
483
- const srcArtifacts = join(fromTaskDir, "artifacts");
530
+ if (validatedFromDir) {
531
+ const srcUr = join(validatedFromDir, "USER_REQUEST.md");
532
+ const srcRes = join(validatedFromDir, "RESEARCH.md");
533
+ const srcArtifacts = join(validatedFromDir, "artifacts");
484
534
  if (existsSync(srcUr)) {
485
535
  const originalUr = readFileSync(srcUr, "utf-8");
486
536
  const implNote =
@@ -501,7 +551,7 @@ export class Orchestrator {
501
551
  copyFileSync(join(srcArtifacts, f), join(destArtifacts, f));
502
552
  }
503
553
  }
504
- state.from = relative(join(this.cwd, ".pp", "state"), fromTaskDir);
554
+ state.from = relative(join(this.cwd, ".pp", "state"), validatedFromDir);
505
555
  if (skipBrainstorm && type === "implement") {
506
556
  state.phase = "plan";
507
557
  state.initialPhase = "plan";
@@ -636,6 +686,7 @@ export class Orchestrator {
636
686
  this.pendingNudges.clear();
637
687
  this.phaseStartTime = 0;
638
688
  this.userGatePending = false;
689
+ this.pendingReviewReady = null;
639
690
  this.failedPlannerVariants = [];
640
691
  this.failedReviewerVariants = [];
641
692
  this.plannerFailureDialogPending = false;
@@ -695,9 +746,6 @@ export class Orchestrator {
695
746
  const explore = createExploreAgent(this.config);
696
747
  const librarian = createLibrarianAgent(this.config);
697
748
  const taskAgent = createTaskAgent(this.config);
698
- const advisor = createAdvisorAgent(this.config);
699
- const deepDebugger = createDeepDebuggerAgent(this.config);
700
- const reviewer = createReviewerAgent(this.config);
701
749
  const phase = this.active?.state.phase;
702
750
  const repos = this.active?.state.repos ?? [];
703
751
  log.debug({ s: "agents", phase, repoCount: repos.length }, "registering agent definitions");
@@ -734,27 +782,60 @@ export class Orchestrator {
734
782
  ...taskAgent,
735
783
  prompt: appendContext("task", taskAgent.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.task.model))),
736
784
  },
737
- {
738
- type: "advisor",
739
- variant: null,
740
- ...advisor,
741
- prompt: appendContext("advisor", advisor.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.advisor.model))),
742
- },
743
- {
744
- type: "deep-debugger",
745
- variant: null,
746
- ...deepDebugger,
747
- prompt: appendContext("deep-debugger", deepDebugger.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple["deep-debugger"].model))),
748
- },
749
- {
750
- type: "reviewer",
751
- variant: null,
752
- ...reviewer,
753
- prompt: appendContext("reviewer", reviewer.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.reviewer.model))),
754
- },
785
+ ...this.buildPoolAgentDefinitions(appendContext),
755
786
  ]);
756
787
  }
757
788
 
789
+ // Register one model-named subagent per ENABLED entry in each on-demand pool
790
+ // (advisors / reviewers / deep-debuggers). The variant token encodes the
791
+ // model+thinking so the caller can see exactly what each is; the base `type`
792
+ // (advisor/reviewer/deep-debugger) drives context-file lookup. A collision
793
+ // after name sanitization is skipped (do not silently merge two entries).
794
+ private buildPoolAgentDefinitions(
795
+ appendContext: (agentType: string, prompt: string, modelInfo: { vendor: string; family: string; tier: string }) => string,
796
+ ): Array<{ type: string; variant: string; frontmatter: any; prompt: string }> {
797
+ const pools = this.config.agents.subagents.pools;
798
+ const defs: Array<{ type: string; variant: string; frontmatter: any; prompt: string }> = [];
799
+ const seen = new Set<string>();
800
+ const add = (
801
+ baseType: "advisor" | "reviewer" | "deep-debugger",
802
+ entry: { model: string; thinking: string; enabled?: boolean },
803
+ make: (e: { model: string; thinking: string }) => { frontmatter: any; prompt: string },
804
+ ) => {
805
+ if (entry.enabled === false) return;
806
+ const variant = encodePoolVariant(resolveModel(entry.model), entry.thinking);
807
+ const name = `${baseType}_${variant}`;
808
+ if (seen.has(name)) {
809
+ getLogger().warn({ s: "agents", name }, "pool entry collides after sanitization; skipping");
810
+ return;
811
+ }
812
+ seen.add(name);
813
+ const agent = make(entry);
814
+ defs.push({
815
+ type: baseType,
816
+ variant,
817
+ frontmatter: agent.frontmatter,
818
+ prompt: appendContext(baseType, agent.prompt, getModelInfo(resolveModel(entry.model))),
819
+ });
820
+ };
821
+ for (const e of pools.advisors) add("advisor", e, createAdvisorAgent);
822
+ for (const e of pools.reviewers) add("reviewer", e, createReviewerAgent);
823
+ for (const e of pools.deepDebuggers) add("deep-debugger", e, createDeepDebuggerAgent);
824
+ return defs;
825
+ }
826
+
827
+ // The orchestrator (main-agent) model config that applies to a phase — the
828
+ // source of truth for the thinking level shown in the main agent's identity
829
+ // block. Mirrors the phase→model selection in injectContextAndArtifacts.
830
+ mainAgentConfigForPhase(phase: Phase | undefined): { model: string; thinking: string } {
831
+ const o = this.config.agents.orchestrators;
832
+ if (phase === "debug" && this.active?.type === "debug") return o.debug;
833
+ if (phase === "brainstorm" && this.active?.type === "brainstorm") return o.brainstorm;
834
+ if (phase === "review" && this.active?.type === "review") return o.review;
835
+ if (phase === "plan") return o.plan;
836
+ return o.implement;
837
+ }
838
+
758
839
  injectContextAndArtifacts(taskDir: string, phase: Phase): void {
759
840
  const log = getLogger();
760
841
  log.debug({ s: "context", taskDir, phase }, "injecting context and artifacts");
@@ -831,6 +912,17 @@ export class Orchestrator {
831
912
  }
832
913
  return [...seen.entries()].filter(([, count]) => count > 1).map(([name]) => name);
833
914
  }
915
+
916
+ applySubagentConcurrency(): void {
917
+ const mgr = (globalThis as any)[Symbol.for("pi-subagents:manager")];
918
+ const limit = this.config?.agents?.maxConcurrentSubagents;
919
+ if (typeof mgr?.setMaxConcurrent !== "function" || typeof limit !== "number") {
920
+ getLogger().debug({ s: "config", limit }, "subagents manager unavailable; skipped concurrency apply");
921
+ return;
922
+ }
923
+ mgr.setMaxConcurrent(limit);
924
+ getLogger().debug({ s: "config", limit }, "applied subagent concurrency limit");
925
+ }
834
926
  }
835
927
 
836
928
  export function ensureGitignore(cwd: string): void {
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { brainstormSystemPrompt } from "./brainstorm.js";
2
+ import { mkdtempSync, rmSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { brainstormSystemPrompt, spawnBrainstormReviewers } from "./brainstorm.js";
6
+ import { getDefaultConfig } from "../config.js";
3
7
 
4
8
  describe("brainstormSystemPrompt", () => {
5
9
  it("debug prompt body is pure procedure (no completion/menu restatements)", () => {
@@ -16,3 +20,28 @@ describe("brainstormSystemPrompt", () => {
16
20
  expect(prompt).not.toContain("/pp");
17
21
  });
18
22
  });
23
+
24
+ describe("spawnBrainstormReviewers missing prerequisites", () => {
25
+ it("emits a displayed error and spawns nothing when artifacts are missing", async () => {
26
+ const taskDir = mkdtempSync(join(tmpdir(), "pi-pi-brainstorm-spawn-"));
27
+ try {
28
+ const sent: any[] = [];
29
+ const send = ((msg: any, _mode: any) => { sent.push(msg); }) as any;
30
+ const result = await spawnBrainstormReviewers(
31
+ {} as any,
32
+ taskDir,
33
+ taskDir,
34
+ "task-id",
35
+ getDefaultConfig(),
36
+ 1,
37
+ send,
38
+ );
39
+ expect(result.spawned).toBe(0);
40
+ expect(sent).toHaveLength(1);
41
+ expect(sent[0].customType).toBe("pp-brainstorm-reviews-error");
42
+ expect(sent[0].display).toBe(true);
43
+ } finally {
44
+ rmSync(taskDir, { recursive: true, force: true });
45
+ }
46
+ });
47
+ });
@@ -7,12 +7,30 @@ import { createBrainstormReviewerAgent } from "../agents/brainstorm-reviewer.js"
7
7
  import { getContextDirs, getArtifactManifest } from "../context.js";
8
8
  import type { RepoInfo } from "../repo-utils.js";
9
9
  import type { TaskType } from "../state.js";
10
+ import { isReviewFileForRound } from "../review-files.js";
10
11
  import type { PhaseSend } from "../transition-controller.js";
11
12
 
12
13
  function isEnabled(value: { enabled?: boolean } | undefined): boolean {
13
14
  return value?.enabled !== false;
14
15
  }
15
16
 
17
+ // The streamlined interactive-phase flow (design-decisions §3b). driverFamily is
18
+ // the phase's own orchestrator model family; defaultAdvisor is the family-differing
19
+ // advisor to consult by default (Claude-driven → a GPT-family advisor; GPT-driven
20
+ // → a Claude-family advisor). Advisors are model-named pool subagents; pick one
21
+ // from the roster in the delegation guidance.
22
+ export function interactiveFlowBlock(driverFamily: string, defaultAdvisor: string): string {
23
+ return [
24
+ "# Flow (minimize interruptions):",
25
+ "1. CLARIFY UP-FRONT: if the request is ambiguous, ask your clarifying question(s) now, at the very start — batch them. If it's clear, skip straight to step 2.",
26
+ "2. WORK AUTONOMOUSLY: research, explore, and design without stopping to ask. Delegate to subagents (parallel explores for broad searches). Do NOT interrupt mid-flow with questions — collect uncertainties for step 4 instead. Only a genuine blocker (you cannot proceed at all) justifies an ask here.",
27
+ `3. CONSULT AN ADVISOR: before presenting, get an independent second opinion from an advisor whose model family differs from yours. You run on ${driverFamily}, so default to ${defaultAdvisor}. Escalate to more advisors for hard or high-stakes calls.`,
28
+ "4. CLARIFY AT THE END: surface any remaining decisions as focused asks — one at a time.",
29
+ "5. APPROVE COMMITTED SPECIFICS: before finalizing, when your output commits to concrete, costly-to-reverse or opinion-heavy choices — exact wording, structure, naming, default values, or interface signatures — show the ACTUAL proposed text/values inline in your message, then ask for explicit approval. Don't silently invent and bury them.",
30
+ "6. PRESENT RESULTS: end with a structured summary (what you found, the decisions, the recommended direction) and hand back with the standard closing block.",
31
+ ].join("\n");
32
+ }
33
+
16
34
  export function brainstormSystemPrompt(taskType: TaskType, taskDescription: string, taskDir: string, cwd: string): string {
17
35
  const registerReposInstruction = `First, register all git repositories you'll work in using pp_register_repo (including the root: ${cwd}). For each, determine the base branch by examining the current branch and remote tracking.`;
18
36
  if (taskType === "debug") {
@@ -23,7 +41,9 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
23
41
  registerReposInstruction,
24
42
  "",
25
43
  "Read-only diagnosis mode. You MAY use write/edit for diagnosis only (repro/test/analysis files) — never to implement the actual fix or feature.",
26
- "",
44
+ "",
45
+ interactiveFlowBlock("GPT", "a Claude-family advisor"),
46
+ "",
27
47
  "# Your job:",
28
48
  "1. Clarify the problem with the user if needed",
29
49
  "2. Delegate research to subagents where useful (see the delegation guidance in your system prompt), and use bash to run commands, check logs, reproduce issues",
@@ -54,6 +74,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
54
74
  " ## Open Questions",
55
75
  " <Unresolved items needing user input. Omit section if none.>",
56
76
  "",
77
+ "This is the LAST interactive phase. If the task continues in autonomous mode, the downstream plan/implement phases cannot ask the user anything — so resolve every Open Question now (answer it, or mark it DECIDED:/ASSUMED: with rationale). Do NOT defer questions to the plan phase.",
57
78
  "These files are validated programmatically. Missing sections or unexpected sections will be rejected.",
58
79
  "Use pp_write_state_file / pp_edit_state_file (NOT the generic write/edit) for .pp state files — they keep the output compact and validate structure.",
59
80
  "",
@@ -78,11 +99,19 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
78
99
  "Your primary job is to TALK WITH THE USER. Explore ideas, analyze tradeoffs, answer questions, discuss approaches.",
79
100
  "Do NOT rush to produce artifacts or finish. Stay in the conversation until the user is satisfied.",
80
101
  "",
102
+ "# Flow (minimize interruptions — the streamlined flow, adapted to conversation):",
103
+ "1. CLARIFY UP-FRONT: if the topic is ambiguous, ask your clarifying question(s) at the very start — batch them into one focused round rather than dripping them out.",
104
+ "2. WORK AUTONOMOUSLY IN THE MIDDLE: once the topic is clear, research/explore/design without stopping to ask. Delegate to subagents (parallel explores for broad searches) and use tools directly for quick lookups. Do NOT interrupt with piecemeal mid-flow questions — collect uncertainties and raise them together at a natural checkpoint.",
105
+ "3. CONSULT AN ADVISOR: before landing on a recommendation, get an independent second opinion from an advisor whose model family differs from yours. You run on Claude, so default to a GPT-family advisor. Escalate to more advisors for hard or high-stakes calls.",
106
+ "4. CLARIFY AT THE END: before finalizing, surface any remaining open decisions as focused asks — one at a time. Don't bury unresolved questions inside an approval prompt or the summary.",
107
+ "5. APPROVE COMMITTED SPECIFICS: when you commit to concrete, costly-to-reverse or opinion-heavy choices — exact wording, structure, naming, default values, or interface signatures — show the ACTUAL proposed text/values inline, then get explicit approval. Don't silently invent and bury them.",
108
+ "6. PRESENT RESULTS: give conclusions as a clear, structured summary (findings, decisions, recommended direction) — don't just dump raw results.",
109
+ "This is still a conversation: the user drives it and may steer at any time. The flow shapes HOW you work between their messages — it does not forbid responding to what they say.",
110
+ "",
81
111
  "# How to work:",
82
- "- Discuss the topic with the user. Ask clarifying questions. Propose approaches. Analyze tradeoffs.",
112
+ "- Discuss the topic with the user. Propose approaches. Analyze tradeoffs.",
83
113
  "- Delegate research to subagents where useful (see the delegation guidance in your system prompt).",
84
114
  "- Use tools directly for quick lookups (cbm_search, lsp, ast_search, grep, etc.)",
85
- "- Present findings to the user and discuss them. Don't just dump raw results.",
86
115
  "",
87
116
  "# Optional artifacts (only when the conversation naturally produces them):",
88
117
  "If the discussion leads to a clear action plan or the user asks you to capture conclusions,",
@@ -112,6 +141,8 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
112
141
  "exists, but exploring the design space: weigh the viable approaches and their tradeoffs so the plan phase inherits a clear",
113
142
  "direction rather than an open-ended problem.",
114
143
  "",
144
+ interactiveFlowBlock("Claude", "a GPT-family advisor"),
145
+ "",
115
146
  "# Steps:",
116
147
  "1. Clarify requirements with the user if anything is ambiguous",
117
148
  "2. Delegate research to subagents where useful (see the delegation guidance in your system prompt) — spawn multiple explores in parallel for broad searches",
@@ -123,7 +154,7 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
123
154
  " - ast_search: find structural patterns across the codebase",
124
155
  "4. Explore design options: identify the viable approaches, weigh their tradeoffs, and land on a recommended direction (capture the reasoning in RESEARCH.md / an artifact)",
125
156
  "5. Ask the user follow-up questions as needed",
126
- "6. Actively drive every Open Question to resolution — chase down answers via research or by asking the user; the Open Questions section should be empty (or every entry marked DECIDED) before you hand off, not a passive backlog",
157
+ "6. Actively drive every Open Question to resolution — chase down answers via research or by asking the user; the Open Questions section should be empty (or every entry marked DECIDED:/ASSUMED: with rationale) before you hand off, not a passive backlog. This is the LAST interactive phase: if the task continues in autonomous mode, the downstream plan/implement phases cannot ask the user anything, so a deferred question becomes an unanswered one. Resolve or explicitly ASSUME each now — do NOT defer to the plan phase.",
127
158
  "7. Write findings into RESEARCH.md as results come back — don't wait for all subagents",
128
159
  "8. Keep USER_REQUEST.md current: update it whenever the user's request changes or clarifies, so it reflects what the user actually wants — don't write it once and leave it stale",
129
160
  "",
@@ -170,7 +201,13 @@ export async function spawnBrainstormReviewers(
170
201
  ): Promise<{ spawned: number; files: string[]; agentIds: string[]; failedVariants: string[] }> {
171
202
  const urPath = join(taskDir, "USER_REQUEST.md");
172
203
  const resPath = join(taskDir, "RESEARCH.md");
173
- if (!existsSync(urPath) || !existsSync(resPath)) return { spawned: 0, files: [], agentIds: [], failedVariants: [] };
204
+ if (!existsSync(urPath) || !existsSync(resPath)) {
205
+ send(
206
+ { customType: "pp-brainstorm-reviews-error", content: "Cannot start artifact review: USER_REQUEST.md or RESEARCH.md is missing.", display: true },
207
+ "context",
208
+ );
209
+ return { spawned: 0, files: [], agentIds: [], failedVariants: [] };
210
+ }
174
211
 
175
212
  const userRequest = readFileSync(urPath, "utf-8");
176
213
  const research = readFileSync(resPath, "utf-8");
@@ -243,7 +280,7 @@ export async function spawnBrainstormReviewers(
243
280
  await Promise.allSettled(results);
244
281
 
245
282
  const reviewOutputFiles = existsSync(reviewsDir)
246
- ? readdirSync(reviewsDir).filter((f) => f.includes(`round-${round}`) && f.endsWith(".md"))
283
+ ? readdirSync(reviewsDir).filter((f) => isReviewFileForRound(f, round))
247
284
  : [];
248
285
 
249
286
  if (reviewOutputFiles.length > 0) {
@@ -6,6 +6,8 @@ export function implementationSystemPrompt(taskDir: string, cwd: string): string
6
6
  "",
7
7
  "The plan has been approved. Implement it.",
8
8
  "",
9
+ "If you need clarification, batch it early rather than interrupting mid-work. Avoid mid-flight questions; you run off the approved USER_REQUEST/RESEARCH/plan. A genuine blocker is the only reason to stop and ask.",
10
+ "",
9
11
  "USER_REQUEST.md, RESEARCH.md, and the synthesized plan are already provided in your context above — do NOT re-read them from disk.",
10
12
  "",
11
13
  "# Instructions:",
@@ -2,7 +2,8 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
2
2
  import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
- import { canTransition, nextPhase, phasePipeline, validateExitCriteria } from "./machine.js";
5
+ import { canTransition, nextPhase, phasePipeline, validateExitCriteria, PLAN_TEMPLATE } from "./machine.js";
6
+ import { PLAN_ALLOWED_SECTIONS } from "../validate-artifacts.js";
6
7
 
7
8
  const tempDirs: string[] = [];
8
9
 
@@ -346,3 +347,18 @@ Fix bug.
346
347
  expect(validateExitCriteria(dir, "quick", "quick")).toEqual({ ok: true });
347
348
  });
348
349
  });
350
+
351
+ describe("PLAN_TEMPLATE / validator alignment", () => {
352
+ it("only uses section headings the plan validator allows", () => {
353
+ const headings = PLAN_TEMPLATE.split("\n")
354
+ .filter((l) => l.startsWith("## "))
355
+ .map((l) => l.slice(3).trim());
356
+ for (const h of headings) {
357
+ expect(PLAN_ALLOWED_SECTIONS).toContain(h);
358
+ }
359
+ });
360
+
361
+ it("includes the optional Pattern constraints section", () => {
362
+ expect(PLAN_TEMPLATE).toContain("## Pattern constraints");
363
+ });
364
+ });
@@ -30,7 +30,7 @@ const RESEARCH_TEMPLATE = [
30
30
  "<Unresolved items needing user input. Omit section if none.>",
31
31
  ].join("\n");
32
32
 
33
- const PLAN_TEMPLATE = [
33
+ export const PLAN_TEMPLATE = [
34
34
  "# Plan",
35
35
  "",
36
36
  "## Scope",
@@ -39,6 +39,9 @@ const PLAN_TEMPLATE = [
39
39
  "## Checklist",
40
40
  "- [ ] <Outcome> — Done when: <observable condition>",
41
41
  "",
42
+ "## Pattern constraints",
43
+ "<Closest existing analog + conventions to mirror. Omit section if none.>",
44
+ "",
42
45
  "## Blockers",
43
46
  "<Unresolved issues. Omit section if none.>",
44
47
  ].join("\n");
@@ -1,5 +1,9 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { planningSystemPrompt } from "./planning.js";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { planningSystemPrompt, spawnPlanReviewers } from "./planning.js";
6
+ import { getDefaultConfig } from "../config.js";
3
7
 
4
8
  describe("planningSystemPrompt self-complete directive", () => {
5
9
  it("guided synthesis instructs the agent to call pp_phase_complete when synthesis is complete", () => {
@@ -14,3 +18,45 @@ describe("planningSystemPrompt self-complete directive", () => {
14
18
  expect(prompt).not.toContain("Do NOT instead ask the user to run /pp manually");
15
19
  });
16
20
  });
21
+
22
+ describe("spawnPlanReviewers missing prerequisites", () => {
23
+ async function run(setup: (dir: string) => void) {
24
+ const taskDir = mkdtempSync(join(tmpdir(), "pi-pi-plan-spawn-"));
25
+ setup(taskDir);
26
+ const sent: any[] = [];
27
+ const send = ((msg: any, _mode: any) => { sent.push(msg); }) as any;
28
+ try {
29
+ const result = await spawnPlanReviewers(
30
+ {} as any,
31
+ taskDir,
32
+ taskDir,
33
+ "task-id",
34
+ getDefaultConfig(),
35
+ 1,
36
+ send,
37
+ );
38
+ return { result, sent };
39
+ } finally {
40
+ rmSync(taskDir, { recursive: true, force: true });
41
+ }
42
+ }
43
+
44
+ it("emits a displayed error when USER_REQUEST/RESEARCH are missing", async () => {
45
+ const { result, sent } = await run(() => {});
46
+ expect(result.spawned).toBe(0);
47
+ expect(sent).toHaveLength(1);
48
+ expect(sent[0].customType).toBe("pp-plan-reviews-error");
49
+ expect(sent[0].display).toBe(true);
50
+ });
51
+
52
+ it("emits a displayed error when the synthesized plan is missing", async () => {
53
+ const { result, sent } = await run((dir) => {
54
+ writeFileSync(join(dir, "USER_REQUEST.md"), "# User Request\n", "utf-8");
55
+ writeFileSync(join(dir, "RESEARCH.md"), "## Affected Code\n", "utf-8");
56
+ });
57
+ expect(result.spawned).toBe(0);
58
+ expect(sent).toHaveLength(1);
59
+ expect(sent[0].customType).toBe("pp-plan-reviews-error");
60
+ expect(sent[0].content).toContain("no synthesized plan");
61
+ });
62
+ });
@@ -8,6 +8,7 @@ import { createPlanReviewerAgent } from "../agents/plan-reviewer.js";
8
8
  import { getContextDirs, getLatestSynthesizedPlan, getArtifactManifest } from "../context.js";
9
9
  import type { RepoInfo } from "../repo-utils.js";
10
10
  import { validatePlan } from "../validate-artifacts.js";
11
+ import { isReviewFileForRound } from "../review-files.js";
11
12
  import type { TaskMode } from "../state.js";
12
13
  import type { PhaseSend } from "../transition-controller.js";
13
14
 
@@ -28,6 +29,8 @@ export function planningSystemPrompt(taskDir: string, mode: TaskMode): string {
28
29
  return [
29
30
  "[PI-PI — PLAN PHASE]",
30
31
  "",
32
+ "If you need clarification, batch it early rather than interrupting mid-work. Avoid mid-flight questions; you run off the approved USER_REQUEST/RESEARCH/plan. A genuine blocker is the only reason to stop and ask.",
33
+ "",
31
34
  "You are a SYNTHESIZER: you MERGE the planner outputs into one plan. Do NOT write your own plan from scratch.",
32
35
  "Planning subagents are working in parallel to create plans.",
33
36
  `They will write their outputs to ${plansDir}/. Wait for the notification that all planners completed.`,
@@ -190,12 +193,24 @@ export async function spawnPlanReviewers(
190
193
  ): Promise<{ spawned: number; files: string[]; agentIds: string[]; failedVariants: string[] }> {
191
194
  const urPath = join(taskDir, "USER_REQUEST.md");
192
195
  const resPath = join(taskDir, "RESEARCH.md");
193
- if (!existsSync(urPath) || !existsSync(resPath)) return { spawned: 0, files: [], agentIds: [], failedVariants: [] };
196
+ if (!existsSync(urPath) || !existsSync(resPath)) {
197
+ send(
198
+ { customType: "pp-plan-reviews-error", content: "Cannot start plan review: USER_REQUEST.md or RESEARCH.md is missing.", display: true },
199
+ "context",
200
+ );
201
+ return { spawned: 0, files: [], agentIds: [], failedVariants: [] };
202
+ }
194
203
 
195
204
  const userRequest = readFileSync(urPath, "utf-8");
196
205
  const research = readFileSync(resPath, "utf-8");
197
206
  const synthesizedPlan = getLatestSynthesizedPlan(taskDir);
198
- if (!synthesizedPlan) return { spawned: 0, files: [], agentIds: [], failedVariants: [] };
207
+ if (!synthesizedPlan) {
208
+ send(
209
+ { customType: "pp-plan-reviews-error", content: "Cannot start plan review: no synthesized plan found.", display: true },
210
+ "context",
211
+ );
212
+ return { spawned: 0, files: [], agentIds: [], failedVariants: [] };
213
+ }
199
214
 
200
215
  const planReviewsDir = join(taskDir, "plan-reviews");
201
216
  if (!existsSync(planReviewsDir)) {
@@ -258,7 +273,7 @@ export async function spawnPlanReviewers(
258
273
  await Promise.allSettled(results);
259
274
 
260
275
  const actualReviewFiles = existsSync(planReviewsDir)
261
- ? readdirSync(planReviewsDir).filter((f) => f.startsWith(`${timestamp}`) && f.includes(`round-${pass}`) && f.endsWith(".md"))
276
+ ? readdirSync(planReviewsDir).filter((f) => f.startsWith(`${timestamp}`) && isReviewFileForRound(f, pass))
262
277
  : [];
263
278
 
264
279
  if (actualReviewFiles.length > 0) {
@@ -1,9 +1,13 @@
1
+ import { interactiveFlowBlock } from "./brainstorm.js";
2
+
1
3
  export function reviewSystemPrompt(taskDir: string, cwd: string): string {
2
4
  return [
3
5
  "[PI-PI — REVIEW PHASE]",
4
6
  "",
5
7
  `First, register all git repositories you'll work in using pp_register_repo (including the root: ${cwd}). For each, determine the base branch by examining the current branch and remote tracking.`,
6
8
  "",
9
+ interactiveFlowBlock("Claude", "a GPT-family advisor"),
10
+ "",
7
11
  "You are reviewing code changes. The user will describe what to review in their first chat message",
8
12
  "(e.g. a branch, a commit range, uncommitted changes, or a GitHub PR URL). There is NO pre-seeded",
9
13
  "USER_REQUEST.md — you must capture the user's request and your findings into the state files yourself.",
@@ -38,6 +42,7 @@ export function reviewSystemPrompt(taskDir: string, cwd: string): string {
38
42
  " ## Open Questions",
39
43
  " <Low-confidence concerns or items needing user input. Omit section if none.>",
40
44
  "",
45
+ "This is the LAST interactive phase. If the task continues in autonomous mode, the downstream plan/implement phases cannot ask the user anything — so resolve every Open Question now (answer it, or mark it DECIDED:/ASSUMED: with rationale). Do NOT defer questions to the plan phase.",
41
46
  "These files are validated programmatically. Missing sections or unexpected sections will be rejected.",
42
47
  "Use pp_write_state_file / pp_edit_state_file (NOT the generic write/edit) for these .pp state files — they keep the output compact and validate structure.",
43
48
  "Keep USER_REQUEST.md current: update it whenever the user clarifies the review scope, so it never goes stale.",
@@ -23,6 +23,16 @@ describe("reviewSystemPrompt apply_feedback wording", () => {
23
23
  expect(auto).not.toContain("pp_phase_complete");
24
24
  });
25
25
 
26
+ it("debug uses the artifact-review branch: DEBUG header, brainstorm-reviews dir, no code-review tail", () => {
27
+ const prompt = reviewSystemPrompt("/tmp/task", 1, "debug", "guided");
28
+ expect(prompt).toContain("DEBUG REVIEW CYCLE");
29
+ expect(prompt).toContain("Debug reviewer outputs are ready.");
30
+ expect(prompt).toContain("/tmp/task/brainstorm-reviews/");
31
+ expect(prompt).not.toContain("code-reviews");
32
+ expect(prompt).not.toContain("Create a fix plan");
33
+ expect(prompt).not.toContain("pp_phase_complete");
34
+ });
35
+
26
36
  it("brainstorm apply-feedback prompt permits artifact updates and steers to the compact tools", () => {
27
37
  const prompt = reviewSystemPrompt("/tmp/task", 1, "brainstorm", "autonomous");
28
38
  expect(prompt).toContain("artifacts/");