@ilya-lesikov/pi-pi 0.7.0 → 0.9.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 (75) hide show
  1. package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
  2. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
  3. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
  4. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
  5. package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
  6. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
  7. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
  8. package/3p/pi-subagents/src/agent-manager.ts +34 -1
  9. package/3p/pi-subagents/src/agent-runner.ts +66 -33
  10. package/3p/pi-subagents/src/index.ts +3 -38
  11. package/3p/pi-subagents/src/types.ts +4 -0
  12. package/extensions/orchestrator/agents/advisor.ts +35 -0
  13. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
  14. package/extensions/orchestrator/agents/code-reviewer.ts +27 -11
  15. package/extensions/orchestrator/agents/constraints.test.ts +82 -0
  16. package/extensions/orchestrator/agents/constraints.ts +66 -2
  17. package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
  18. package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
  19. package/extensions/orchestrator/agents/planner.ts +9 -8
  20. package/extensions/orchestrator/agents/prompts.test.ts +120 -0
  21. package/extensions/orchestrator/agents/reviewer.ts +48 -0
  22. package/extensions/orchestrator/agents/task.ts +6 -20
  23. package/extensions/orchestrator/agents/tool-routing.ts +39 -1
  24. package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
  25. package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
  26. package/extensions/orchestrator/command-handlers.test.ts +47 -0
  27. package/extensions/orchestrator/command-handlers.ts +44 -28
  28. package/extensions/orchestrator/config.test.ts +40 -1
  29. package/extensions/orchestrator/config.ts +18 -2
  30. package/extensions/orchestrator/context.test.ts +54 -0
  31. package/extensions/orchestrator/context.ts +81 -2
  32. package/extensions/orchestrator/custom-footer.test.ts +91 -0
  33. package/extensions/orchestrator/custom-footer.ts +20 -20
  34. package/extensions/orchestrator/event-handlers.test.ts +412 -2
  35. package/extensions/orchestrator/event-handlers.ts +556 -227
  36. package/extensions/orchestrator/flant-infra.test.ts +25 -0
  37. package/extensions/orchestrator/flant-infra.ts +91 -0
  38. package/extensions/orchestrator/index.ts +1 -1
  39. package/extensions/orchestrator/integration.test.ts +411 -20
  40. package/extensions/orchestrator/messages.test.ts +30 -0
  41. package/extensions/orchestrator/messages.ts +6 -0
  42. package/extensions/orchestrator/model-registry.test.ts +48 -1
  43. package/extensions/orchestrator/model-registry.ts +43 -1
  44. package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  45. package/extensions/orchestrator/orchestrator.test.ts +113 -0
  46. package/extensions/orchestrator/orchestrator.ts +197 -60
  47. package/extensions/orchestrator/phases/brainstorm.ts +20 -27
  48. package/extensions/orchestrator/phases/implementation.test.ts +11 -0
  49. package/extensions/orchestrator/phases/implementation.ts +4 -6
  50. package/extensions/orchestrator/phases/machine.test.ts +36 -0
  51. package/extensions/orchestrator/phases/machine.ts +11 -1
  52. package/extensions/orchestrator/phases/planning.test.ts +16 -0
  53. package/extensions/orchestrator/phases/planning.ts +11 -4
  54. package/extensions/orchestrator/phases/review-task.test.ts +20 -0
  55. package/extensions/orchestrator/phases/review-task.ts +47 -22
  56. package/extensions/orchestrator/phases/review.test.ts +34 -0
  57. package/extensions/orchestrator/phases/review.ts +44 -6
  58. package/extensions/orchestrator/plannotator.ts +9 -6
  59. package/extensions/orchestrator/pp-menu.test.ts +207 -1
  60. package/extensions/orchestrator/pp-menu.ts +514 -402
  61. package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
  62. package/extensions/orchestrator/pp-state-tools.ts +249 -0
  63. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
  64. package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
  65. package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
  66. package/extensions/orchestrator/state.test.ts +9 -0
  67. package/extensions/orchestrator/state.ts +15 -0
  68. package/extensions/orchestrator/test-helpers.ts +4 -1
  69. package/extensions/orchestrator/transition-controller.test.ts +100 -0
  70. package/extensions/orchestrator/transition-controller.ts +61 -3
  71. package/extensions/orchestrator/usage-tracker.ts +5 -1
  72. package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
  73. package/extensions/orchestrator/validate-artifacts.ts +2 -2
  74. package/package.json +1 -1
  75. package/AGENTS.md +0 -28
@@ -2,6 +2,7 @@
2
2
  * agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
3
3
  */
4
4
 
5
+ import { AsyncLocalStorage } from "node:async_hooks";
5
6
  import type { Model } from "@earendil-works/pi-ai";
6
7
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
8
  import {
@@ -27,6 +28,53 @@ const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
27
28
  const TRACER_KEY = Symbol.for("pi-pi:tracer");
28
29
  const SUBAGENT_SESSION_KEY = Symbol.for("pi-pi:subagent-session");
29
30
 
31
+ // Concurrency-safe subagent lineage. The previous approach derived parent/depth
32
+ // from the mutable process-global SUBAGENT_SESSION_KEY, which is racy for
33
+ // CONCURRENT siblings: MAIN spawns opus/gpt/gemini in parallel, each runAgent's
34
+ // synchronous prologue read whatever the *previous sibling* had just written to
35
+ // the global — so gpt recorded opus as its parent, gemini recorded gpt, forming a
36
+ // bogus opus→gpt→gemini chain with depth 3-6 instead of three depth-1 children of
37
+ // MAIN. AsyncLocalStorage instead propagates DOWN the async call chain (a nested
38
+ // spawn inherits its true parent's store) but NOT ACROSS sibling promises (each
39
+ // sibling started from MAIN sees no store → parent=undefined, depth=1).
40
+ interface SubagentLineage {
41
+ depth: number;
42
+ subagentId?: string;
43
+ }
44
+ const subagentLineage = new AsyncLocalStorage<SubagentLineage>();
45
+
46
+ // The process-global SUBAGENT_SESSION_KEY marker is read elsewhere purely for
47
+ // truthiness ("is any subagent running in this process?", e.g. orchestrator nudge
48
+ // gates). Manage it with a ref-count so concurrent in-process siblings set/clear
49
+ // it correctly regardless of completion order (a plain save/restore would leak a
50
+ // stale marker whenever the last sibling to finish wasn't the last to start).
51
+ let activeSubagentRuns = 0;
52
+ let markerBeforeFirstRun: unknown;
53
+ function enterSubagentMarker(): void {
54
+ if (activeSubagentRuns === 0) {
55
+ // Snapshot whatever was there before our first concurrent run (undefined in
56
+ // the main process; the orchestrator's persistent bootstrap { depth: 1 } in a
57
+ // subagent process) so we can restore it exactly when the last run exits.
58
+ markerBeforeFirstRun = (globalThis as any)[SUBAGENT_SESSION_KEY];
59
+ }
60
+ activeSubagentRuns++;
61
+ const marker = (globalThis as any)[SUBAGENT_SESSION_KEY];
62
+ if (typeof marker !== "object" || marker === null) {
63
+ (globalThis as any)[SUBAGENT_SESSION_KEY] = { depth: 1 };
64
+ }
65
+ }
66
+ function exitSubagentMarker(): void {
67
+ activeSubagentRuns = Math.max(0, activeSubagentRuns - 1);
68
+ if (activeSubagentRuns === 0) {
69
+ if (markerBeforeFirstRun === undefined) {
70
+ delete (globalThis as any)[SUBAGENT_SESSION_KEY];
71
+ } else {
72
+ (globalThis as any)[SUBAGENT_SESSION_KEY] = markerBeforeFirstRun;
73
+ }
74
+ markerBeforeFirstRun = undefined;
75
+ }
76
+ }
77
+
30
78
  interface PiPiTracer {
31
79
  openSubagent(meta: {
32
80
  subagentId: string;
@@ -45,11 +93,6 @@ function getTracer(): PiPiTracer | undefined {
45
93
  return (globalThis as any)[TRACER_KEY];
46
94
  }
47
95
 
48
- function subagentDepth(): number {
49
- const marker = (globalThis as any)[SUBAGENT_SESSION_KEY];
50
- return typeof marker?.depth === "number" ? marker.depth : 1;
51
- }
52
-
53
96
  function traceSubagentEvent(subagentId: string | undefined, event: AgentSessionEvent, turnIndex: number): void {
54
97
  if (!subagentId) return;
55
98
  const tracer = getTracer();
@@ -242,21 +285,22 @@ export async function runAgent(
242
285
 
243
286
  // Resolve working directory: worktree override > parent cwd
244
287
  const effectiveCwd = options.cwd ?? ctx.cwd;
245
- // Shared marker with extensions/orchestrator/index.ts (SUBAGENT_SESSION_KEY).
246
- // Canonical shape is { depth: number }; a legacy boolean true is tolerated as depth 1.
247
- const subagentSessionKey = Symbol.for("pi-pi:subagent-session");
248
- const previousSubagentSession = (globalThis as any)[subagentSessionKey];
249
- const previousDepth = typeof previousSubagentSession === "object" && previousSubagentSession !== null
250
- ? ((previousSubagentSession as { depth?: number }).depth ?? 0)
251
- : previousSubagentSession
252
- ? 1
253
- : 0;
254
- const parentSubagentId = typeof previousSubagentSession === "object" && previousSubagentSession !== null
255
- ? (previousSubagentSession as { subagentId?: string }).subagentId
256
- : undefined;
257
- const subagentSessionState = { depth: previousDepth + 1, subagentId: options.subagentId };
258
- (globalThis as any)[subagentSessionKey] = subagentSessionState;
259
288
 
289
+ // Derive lineage from the async-context store (see subagentLineage above).
290
+ // Reading it BEFORE entering the child's own als.run means concurrent siblings
291
+ // each see their real parent (MAIN → undefined store), not a leaked sibling.
292
+ const parentLineage = subagentLineage.getStore();
293
+ const parentSubagentId = parentLineage?.subagentId;
294
+ const traceDepth = (parentLineage?.depth ?? 0) + 1;
295
+ const childLineage: SubagentLineage = { depth: traceDepth, subagentId: options.subagentId };
296
+
297
+ // Mark the process as "running a subagent" for truthiness readers (see
298
+ // enter/exitSubagentMarker). Lineage (parent/depth) comes from als above, so
299
+ // this marker no longer participates in nesting math and its old sibling-race
300
+ // can no longer corrupt the org chart.
301
+ enterSubagentMarker();
302
+
303
+ return subagentLineage.run(childLineage, async () => {
260
304
  try {
261
305
  const env = await detectEnv(options.pi, effectiveCwd);
262
306
 
@@ -460,7 +504,7 @@ export async function runAgent(
460
504
  description: options.subagentDescription,
461
505
  parentToolCallId: options.parentToolCallId,
462
506
  parentSubagentId,
463
- depth: subagentDepth(),
507
+ depth: traceDepth,
464
508
  systemPrompt,
465
509
  effectivePrompt,
466
510
  });
@@ -496,20 +540,9 @@ export async function runAgent(
496
540
  }
497
541
  return { responseText, session, aborted, steered: softLimitReached };
498
542
  } finally {
499
- const currentDepth = typeof (globalThis as any)[subagentSessionKey] === "object" && (globalThis as any)[subagentSessionKey] !== null
500
- ? ((((globalThis as any)[subagentSessionKey]) as { depth?: number }).depth ?? subagentSessionState.depth)
501
- : subagentSessionState.depth;
502
- const nextDepth = Math.max(0, currentDepth - 1);
503
- if (nextDepth === 0) {
504
- if (previousSubagentSession === undefined) {
505
- delete (globalThis as any)[subagentSessionKey];
506
- } else {
507
- (globalThis as any)[subagentSessionKey] = previousSubagentSession;
508
- }
509
- } else {
510
- (globalThis as any)[subagentSessionKey] = { depth: nextDepth };
511
- }
543
+ exitSubagentMarker();
512
544
  }
545
+ });
513
546
  }
514
547
 
515
548
  /**
@@ -402,13 +402,7 @@ Use get_subagent_result for full output.`,
402
402
  }
403
403
 
404
404
  // Background completion: route through group join or send individual nudge
405
- const firstProgressSeen = new Set<string>();
406
- const firstTurnSeen = new Set<string>();
407
-
408
405
  const manager = new AgentManager((record) => {
409
- firstProgressSeen.delete(record.id);
410
- firstTurnSeen.delete(record.id);
411
-
412
406
  let eventData: ReturnType<typeof buildEventData> | undefined;
413
407
  try {
414
408
  eventData = buildEventData(record);
@@ -907,36 +901,9 @@ Guidelines:
907
901
  const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
908
902
 
909
903
  let id: string;
910
- const emitFirstTool = (toolName: string) => {
911
- if (firstProgressSeen.has(id)) return;
912
- firstProgressSeen.add(id);
913
- pi.events.emit("subagents:first_tool", {
914
- id,
915
- type: subagentType,
916
- description: params.description,
917
- toolName,
918
- });
919
- };
920
- const emitFirstTurn = (turnCount: number) => {
921
- if (firstTurnSeen.has(id)) return;
922
- firstTurnSeen.add(id);
923
- pi.events.emit("subagents:first_turn", {
924
- id,
925
- type: subagentType,
926
- description: params.description,
927
- turnCount,
928
- });
929
- };
930
- const originalBgToolActivity = bgCallbacks.onToolActivity;
931
- bgCallbacks.onToolActivity = (activity) => {
932
- originalBgToolActivity(activity);
933
- if (activity.type === "start") emitFirstTool(activity.toolName);
934
- };
935
- const originalBgTurnEnd = bgCallbacks.onTurnEnd;
936
- bgCallbacks.onTurnEnd = (turnCount) => {
937
- originalBgTurnEnd(turnCount);
938
- emitFirstTurn(turnCount);
939
- };
904
+ // first_tool/first_turn are now emitted centrally by AgentManager.startAgent
905
+ // (the single choke point for ALL spawn paths, including RPC-spawned panels),
906
+ // so no per-branch wiring is needed here.
940
907
 
941
908
  // Wrap onSessionCreated to wire output file streaming.
942
909
  // The callback lazily reads record.outputFile (set right after spawn)
@@ -944,8 +911,6 @@ Guidelines:
944
911
  const origBgOnSession = bgCallbacks.onSessionCreated;
945
912
  bgCallbacks.onSessionCreated = (session: any) => {
946
913
  origBgOnSession(session);
947
- firstProgressSeen.delete(id);
948
- firstTurnSeen.delete(id);
949
914
  const rec = manager.getRecord(id);
950
915
  if (rec?.outputFile) {
951
916
  rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd);
@@ -81,6 +81,10 @@ export interface AgentRecord {
81
81
  worktreeResult?: { hasChanges: boolean; branch?: string };
82
82
  /** The tool_use_id from the original Agent tool call. */
83
83
  toolCallId?: string;
84
+ /** Whether the subagents:first_tool event has been emitted for this run. */
85
+ firstToolEmitted?: boolean;
86
+ /** Whether the subagents:first_turn event has been emitted for this run. */
87
+ firstTurnEmitted?: boolean;
84
88
  /** Path to the streaming output transcript file. */
85
89
  outputFile?: string;
86
90
  /** Cleanup function for the output file stream subscription. */
@@ -0,0 +1,35 @@
1
+ import type { PiPiConfig } from "../config.js";
2
+ import { resolveModel } from "../model-registry.js";
3
+ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-routing.js";
4
+
5
+ export function createAdvisorAgent(config: PiPiConfig) {
6
+ return {
7
+ frontmatter: {
8
+ description: "Deep-reasoning advisor for design decisions and 'why is this broken' analysis (pi-pi)",
9
+ tools: `read, bash, grep, find, ls, lsp, ast_search, ${ALL_CBM_TOOLS}, ${EXA_TOOLS}`,
10
+ model: resolveModel(config.agents.subagents.simple.advisor.model),
11
+ thinking: config.agents.subagents.simple.advisor.thinking,
12
+ max_turns: 120,
13
+ prompt_mode: "replace",
14
+ },
15
+ prompt: [
16
+ "<constraints>",
17
+ "You are a deep-reasoning ADVISOR. You investigate one hard question — a design decision, an architecture tradeoff, a \"why is this broken\", or a correctness/soundness judgment — and return a reasoned recommendation backed by evidence.",
18
+ "These rules override your default helpfulness. Strict compliance is required.",
19
+ "You are READ-ONLY: you MUST NOT modify any file. Diagnose and advise; do NOT change code.",
20
+ "</constraints>",
21
+ "",
22
+ PRINCIPLES_BLOCK,
23
+ "",
24
+ TOOLS_BLOCK,
25
+ "",
26
+ "<task>",
27
+ "- Verify every claim with tool calls — read the actual code. Never reason from memory about this codebase.",
28
+ "- Generate multiple competing hypotheses or approaches before converging. Surface and question hidden assumptions.",
29
+ "- Scope recommendations by effort: name the quick fix vs the thorough one.",
30
+ "- Structure your answer: Diagnosis (what is actually true, with file:line evidence) → Options & tradeoffs → Recommendation.",
31
+ "- Be honest about uncertainty. If evidence is thin, say so and state what would resolve it.",
32
+ "</task>",
33
+ ].join("\n"),
34
+ };
35
+ }
@@ -1,5 +1,5 @@
1
1
  import type { VariantConfig } from "../config.js";
2
- import { loadAllContextFiles } from "../context.js";
2
+ import { loadAllContextFiles, formatManifestBlock } from "../context.js";
3
3
  import { resolveModel, getModelInfo } from "../model-registry.js";
4
4
  import type { RepoInfo } from "../repo-utils.js";
5
5
  import { buildRepoContext } from "./repo-context.js";
@@ -8,7 +8,7 @@ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-
8
8
  export function createBrainstormReviewerAgent(
9
9
  variant: string,
10
10
  variants: Record<string, VariantConfig>,
11
- taskArtifacts: { userRequest: string; research: string; artifacts?: { name: string; content: string }[] },
11
+ taskArtifacts: { userRequest: string; research: string; artifacts?: { name: string; content: string }[]; manifest?: { title: string; path: string }[] },
12
12
  outputPath: string,
13
13
  contextDirs: string[],
14
14
  phase?: string,
@@ -64,10 +64,10 @@ export function createBrainstormReviewerAgent(
64
64
  "- INACCURACIES: (claims that don't match the code)",
65
65
  "- SUGGESTIONS: (improvements, not required)",
66
66
  "",
67
- "subagent_type is REQUIRED when spawning subagents — calls without it are rejected:",
68
- '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
69
- '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
70
- "Spawn multiple Explore agents in parallel for broad searches.",
67
+ "You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
68
+ '- Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
69
+ '- Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
70
+ "Spawn multiple explore agents in parallel for broad searches. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
71
71
  "</task>",
72
72
  "",
73
73
  "# MANDATORY: Write your review to this exact file using the write tool:",
@@ -88,7 +88,7 @@ export function createBrainstormReviewerAgent(
88
88
  : []),
89
89
  ...(repoContext ? [repoContext] : []),
90
90
  "",
91
- "The artifacts above are already in your context. Do NOT re-read them from disk.",
91
+ formatManifestBlock(taskArtifacts.manifest ?? []),
92
92
  ].join("\n"),
93
93
  };
94
94
  }
@@ -1,5 +1,5 @@
1
1
  import type { VariantConfig } from "../config.js";
2
- import { loadAllContextFiles } from "../context.js";
2
+ import { loadAllContextFiles, formatManifestBlock } from "../context.js";
3
3
  import { resolveModel, getModelInfo } from "../model-registry.js";
4
4
  import type { RepoInfo } from "../repo-utils.js";
5
5
  import { buildRepoContext } from "./repo-context.js";
@@ -8,7 +8,7 @@ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-
8
8
  export function createCodeReviewerAgent(
9
9
  variant: string,
10
10
  variants: Record<string, VariantConfig>,
11
- taskArtifacts: { userRequest: string; research: string; synthesizedPlan: string },
11
+ taskArtifacts: { userRequest: string; research: string; synthesizedPlan?: string; manifest?: { title: string; path: string }[] },
12
12
  outputPath: string,
13
13
  contextDirs: string[],
14
14
  phase?: string,
@@ -21,6 +21,9 @@ export function createCodeReviewerAgent(
21
21
  const contextFiles = loadAllContextFiles(contextDirs, "codeReviewer", "system", phase, getModelInfo(variantConfig.model));
22
22
  const contextBlock = contextFiles.map((f) => f.content).join("\n\n");
23
23
  const repoContext = buildRepoContext(repos);
24
+ // A standalone review task (phase "review") has no synthesized plan: review the
25
+ // diff against USER_REQUEST.md/RESEARCH.md, not an implementation plan.
26
+ const hasPlan = typeof taskArtifacts.synthesizedPlan === "string" && taskArtifacts.synthesizedPlan.trim().length > 0;
24
27
 
25
28
  return {
26
29
  frontmatter: {
@@ -53,11 +56,15 @@ export function createCodeReviewerAgent(
53
56
  "3. Read changed files for full context",
54
57
  "4. Run lsp diagnostics on changed files",
55
58
  "5. Use lsp findReferences to check callers of modified functions",
56
- "6. Check the implementation against the plan",
59
+ hasPlan
60
+ ? "6. Check the implementation against the plan"
61
+ : "6. Check the changes against USER_REQUEST.md and RESEARCH.md (there is no implementation plan for a standalone review)",
57
62
  "",
58
63
  "Review criteria:",
59
64
  "- Bugs: logic errors, off-by-ones, null handling, race conditions",
60
- "- Correctness: does it match the plan and user request?",
65
+ hasPlan
66
+ ? "- Correctness: does it match the plan and user request?"
67
+ : "- Correctness: does it match the user request and the reviewed scope?",
61
68
  "- Quality: error handling, edge cases, type safety",
62
69
  "- Missing: untested paths, unhandled errors, incomplete implementations",
63
70
  "",
@@ -81,10 +88,20 @@ export function createCodeReviewerAgent(
81
88
  "- MINOR: (nice to have)",
82
89
  "- OPEN QUESTIONS: (low-confidence concerns, speculative follow-ups)",
83
90
  "",
84
- "subagent_type is REQUIRED when spawning subagents calls without it are rejected:",
85
- '- Agent(subagent_type="Explore", ...) codebase research. Prefer this for most lookups. Fast and cheap.',
86
- '- Agent(subagent_type="Librarian", ...) external docs, library APIs, web research.',
87
- "Spawn multiple Explore agents in parallel for broad searches.",
91
+ "After the findings, include a machine-readable ANCHORS block so the synthesizer can place",
92
+ "the findings at their exact locations (in source AI_COMMENT markers and/or GitHub PR line comments).",
93
+ "Emit one line per actionable finding (CRITICAL/MAJOR/MINOR), in this EXACT format:",
94
+ "ANCHORS:",
95
+ "<relative/path/from/repo/root>:<line> — <severity>: <one-line finding>",
96
+ "Rules:",
97
+ "- Use the repo-relative path (as `git diff` shows it) and a single 1-based line number on the NEW side of the diff.",
98
+ "- One finding per line; omit findings you cannot pin to a concrete file:line (keep those in OPEN QUESTIONS).",
99
+ "- If there are no actionable findings, write `ANCHORS:` followed by `(none)`.",
100
+ "",
101
+ "You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
102
+ '- Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
103
+ '- Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
104
+ "Spawn multiple explore agents in parallel for broad searches. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
88
105
  "</task>",
89
106
  "",
90
107
  // --- dynamic suffix ---
@@ -97,11 +114,10 @@ export function createCodeReviewerAgent(
97
114
  "=== RESEARCH ===",
98
115
  taskArtifacts.research,
99
116
  "",
100
- "=== SYNTHESIZED PLAN ===",
101
- taskArtifacts.synthesizedPlan,
117
+ ...(hasPlan ? ["=== SYNTHESIZED PLAN ===", taskArtifacts.synthesizedPlan as string, ""] : []),
102
118
  ...(repoContext ? [repoContext] : []),
103
119
  "",
104
- "The artifacts above are already in your context. Do NOT re-read them from disk.",
120
+ formatManifestBlock(taskArtifacts.manifest ?? []),
105
121
  ].join("\n"),
106
122
  };
107
123
  }
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { closingBlockInstruction, completionLine, constraintsBlock } from "./constraints.js";
3
+
4
+ describe("completionLine", () => {
5
+ it("guided plan and implement instruct calling pp_phase_complete on completion", () => {
6
+ for (const phase of ["plan", "implement"] as const) {
7
+ const line = completionLine(phase, "guided");
8
+ expect(line).toContain("call pp_phase_complete");
9
+ expect(line).not.toContain("Do NOT advance on your own");
10
+ expect(line).not.toMatch(/do not.*wait for.*input/i);
11
+ }
12
+ });
13
+
14
+ it("guided review and debug stay hands-off (no unprompted self-complete)", () => {
15
+ for (const phase of ["review", "debug"] as const) {
16
+ const line = completionLine(phase, "guided");
17
+ expect(line).toContain("Do NOT advance on your own or call pp_phase_complete unprompted");
18
+ }
19
+ });
20
+
21
+ it("guided brainstorm never self-completes", () => {
22
+ const line = completionLine("brainstorm", "guided");
23
+ expect(line).toContain("Do NOT call pp_phase_complete yourself");
24
+ });
25
+
26
+ it("autonomous phases always self-complete regardless of phase", () => {
27
+ for (const phase of ["plan", "implement", "review", "brainstorm"] as const) {
28
+ expect(completionLine(phase, "autonomous")).toContain("call pp_phase_complete");
29
+ }
30
+ });
31
+
32
+ it("guided handoff phases require the standardized closing block", () => {
33
+ for (const phase of ["brainstorm", "review", "debug"] as const) {
34
+ const line = completionLine(phase, "guided");
35
+ expect(line).toContain("the standardized block");
36
+ expect(line).toContain("Advance via the /pp menu");
37
+ }
38
+ });
39
+
40
+ it("autonomous phases do not emit the prose closing block", () => {
41
+ expect(completionLine("brainstorm", "autonomous")).not.toContain("Advance via the /pp menu");
42
+ });
43
+ });
44
+
45
+ describe("closingBlockInstruction", () => {
46
+ it("spells out the exact block with a blank line between summary and advance, no rules", () => {
47
+ const block = closingBlockInstruction("brainstorm");
48
+ expect(block).toContain("Advance via the /pp menu to move into plan");
49
+ expect(block).not.toContain("────");
50
+ expect(block).toContain("/pp");
51
+ expect(block).toContain("✅ <one-sentence summary of what this phase produced>\n\n▶ Advance via the /pp menu to move into plan.");
52
+ });
53
+
54
+ it("prepends the Review Summary schema ONLY for the review phase, before the closing block", () => {
55
+ const review = closingBlockInstruction("review");
56
+ expect(review).toContain("## Review Summary");
57
+ expect(review).toContain("| # | Severity | Location | Finding |");
58
+ expect(review).toContain("BLOCKER");
59
+ // The ✅/▶ lines must remain the final lines of the block.
60
+ expect(review.trimEnd().endsWith("▶ Advance via the /pp menu to move into plan.")).toBe(true);
61
+ expect(review.indexOf("## Review Summary")).toBeLessThan(review.indexOf("✅ <one-sentence summary"));
62
+ });
63
+
64
+ it("does NOT emit the Review Summary schema for brainstorm or debug closes", () => {
65
+ for (const phase of ["brainstorm", "debug"] as const) {
66
+ expect(closingBlockInstruction(phase)).not.toContain("## Review Summary");
67
+ }
68
+ });
69
+ });
70
+
71
+ describe("constraintsBlock", () => {
72
+ it("guided implement block tells the agent to self-complete", () => {
73
+ const block = constraintsBlock("implement", "guided");
74
+ expect(block).toContain("call pp_phase_complete");
75
+ expect(block).not.toContain("Do NOT advance on your own");
76
+ });
77
+
78
+ it("guided plan block tells the agent to self-complete", () => {
79
+ const block = constraintsBlock("plan", "guided");
80
+ expect(block).toContain("call pp_phase_complete");
81
+ });
82
+ });
@@ -3,6 +3,14 @@ import type { Phase, TaskMode } from "../state.js";
3
3
  const READONLY_CONSTRAINT =
4
4
  "You MUST NOT edit, create, or delete any project file (source, tests, config, docs) — only files under .pp/state/ may be written — and you MUST NOT run state-changing shell commands. If you find a fix worth making, record it in your output; do NOT apply it here.";
5
5
 
6
+ // The review phase is read-only EXCEPT for publishing findings: the main agent may
7
+ // insert or remove `AI_COMMENT:` markers in source files (file comments), and may
8
+ // run `gh` to post/read GitHub PR line comments AND bundled pull-request reviews
9
+ // (PR comments) — and nothing else (no fixes, no other edits, no other state-changing
10
+ // commands). This is the reviewer→user mirror of the user→reviewer `AI_REVIEW:` markers.
11
+ const REVIEW_READONLY_CONSTRAINT =
12
+ "You MUST NOT edit, create, or delete any project file (source, tests, config, docs) — only files under .pp/state/ may be written — and you MUST NOT run state-changing shell commands, WITH ONE EXCEPTION: when publishing review findings you MAY insert or remove `AI_COMMENT:` markers in source files (in each file's native comment syntax) and MAY run `gh` to post or read GitHub PR comments — including line comments and bundled pull-request reviews (a review body plus its line comments) — and nothing else. Do NOT apply fixes or make any other source change. If you find a fix worth making, record it in your output.";
13
+
6
14
  const IMPLEMENT_CONSTRAINT =
7
15
  "Implement only the approved plan. Do NOT add scope or change plan items without recording why in the plan. If the same fix fails 3 times, stop and re-plan — do NOT keep retrying the same approach.";
8
16
 
@@ -16,9 +24,62 @@ export function isReadOnlyPhase(phase: Phase): boolean {
16
24
  export function phaseConstraint(phase: Phase): string {
17
25
  if (phase === "implement") return IMPLEMENT_CONSTRAINT;
18
26
  if (phase === "quick") return QUICK_CONSTRAINT;
27
+ if (phase === "review") return REVIEW_READONLY_CONSTRAINT;
19
28
  return READONLY_CONSTRAINT;
20
29
  }
21
30
 
31
+ // Guided phases that stop and hand back to the user (brainstorm, review, debug) end their turn
32
+ // with prose rather than a tool call. To keep that handoff consistent, the model must close with
33
+ // this exact block. NEXT_PHASE_LABEL supplies the phase the /pp menu advances into.
34
+ const NEXT_PHASE_LABEL: Partial<Record<Phase, string>> = {
35
+ brainstorm: "plan",
36
+ review: "plan",
37
+ debug: "plan",
38
+ };
39
+
40
+ // The structured summary a review must print when it finishes, on BOTH review finish paths
41
+ // (the review-cycle synthesis in review.ts and the ordinary review-task close here). Counts
42
+ // reconcile with the `ANCHORS:` block: when it is `(none)`, Anchored=0 and every finding is
43
+ // Un-anchorable. Location is `file:line`, or `file:—` for un-anchorable findings.
44
+ export const REVIEW_SUMMARY_SCHEMA = [
45
+ "## Review Summary",
46
+ "",
47
+ "Scope: <repos/PR/branch/range + change size>",
48
+ "",
49
+ "| Repo | PR | Findings | Anchored | Un-anchorable |",
50
+ "|------|----|----------|----------|---------------|",
51
+ "| <owner/repo> | #<n> | <total> | <in-diff> | <not-in-diff> |",
52
+ "",
53
+ "### Findings",
54
+ "| # | Severity | Location | Finding |",
55
+ "|---|----------|----------|---------|",
56
+ "| 1 | BLOCKER | <file:line or file:—> | <one line> |",
57
+ "",
58
+ "Next step: /pp → Next → Publish to post these as GitHub PR comments or file comments.",
59
+ ].join("\n");
60
+
61
+ // Instruction wrapping REVIEW_SUMMARY_SCHEMA: severity vocabulary is fixed and the summary
62
+ // degrades gracefully to a single-repo / no-PR row when only one repo (or no PR) was resolved.
63
+ export const reviewSummaryInstruction = [
64
+ "Before the closing block, print a structured Review Summary in EXACTLY this shape (fill in the values; severity is one of BLOCKER / MAJOR / MINOR, uppercase):",
65
+ "",
66
+ REVIEW_SUMMARY_SCHEMA,
67
+ "",
68
+ "Counts MUST reconcile with the `ANCHORS:` block: an `(none)` block means Anchored=0 and every finding is Un-anchorable. Fill the table from whatever repos/PR you resolved; degrade to a single-repo / no-PR row when there is only one repo or no PR.",
69
+ ].join("\n");
70
+
71
+ export function closingBlockInstruction(phase: Phase): string {
72
+ const next = NEXT_PHASE_LABEL[phase] ?? "the next phase";
73
+ const summary = phase === "review" ? reviewSummaryInstruction + "\n\n" : "";
74
+ return [
75
+ summary +
76
+ "End that turn with EXACTLY this block, verbatim, as the final lines of your message (fill the summary line with one sentence; change nothing else):",
77
+ "✅ <one-sentence summary of what this phase produced>",
78
+ "",
79
+ `▶ Advance via the /pp menu to move into ${next}.`,
80
+ ].join("\n");
81
+ }
82
+
22
83
  export function completionLine(phase: Phase, mode: TaskMode): string {
23
84
  if (phase === "quick") {
24
85
  return "When the user's request is complete, call pp_phase_complete. Do NOT stop and wait for the user before then.";
@@ -27,9 +88,12 @@ export function completionLine(phase: Phase, mode: TaskMode): string {
27
88
  return "There is no user driving this phase. The moment its work is complete, call pp_phase_complete — do NOT pause, ask for confirmation, or wait for input. Never end a turn with prose: every turn ends in a tool call.";
28
89
  }
29
90
  if (phase === "brainstorm") {
30
- return "This is a conversation. Do NOT call pp_phase_complete yourself — keep going until the user ends it or advances via the /pp menu.";
91
+ return "This is a conversation. Do NOT call pp_phase_complete yourself — keep going until the user ends it or advances via the /pp menu. When you have delivered a complete answer and are handing back for the user to advance, close with the standardized block. " + closingBlockInstruction(phase);
92
+ }
93
+ if (phase === "plan" || phase === "implement") {
94
+ return "When you judge this phase complete, call pp_phase_complete — the extension opens the advance gate for the user to review and confirm. Do NOT instead stop and ask the user to run /pp manually.";
31
95
  }
32
- return "When the work is complete, stop and let the user review and advance it via the /pp menu. Do NOT advance on your own or call pp_phase_complete unprompted.";
96
+ return "When the work is complete, stop and let the user review and advance it via the /pp menu. Do NOT advance on your own or call pp_phase_complete unprompted. Close with the standardized block. " + closingBlockInstruction(phase);
33
97
  }
34
98
 
35
99
  const PHASE_IDENTITY: Record<string, string> = {
@@ -0,0 +1,35 @@
1
+ import type { PiPiConfig } from "../config.js";
2
+ import { resolveModel } from "../model-registry.js";
3
+ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-routing.js";
4
+
5
+ export function createDeepDebuggerAgent(config: PiPiConfig) {
6
+ return {
7
+ frontmatter: {
8
+ description: "Deep root-cause analysis for HARD, persistent failures — not every error (pi-pi)",
9
+ tools: `read, write, edit, bash, grep, find, ls, lsp, ast_search, ${ALL_CBM_TOOLS}, ${EXA_TOOLS}`,
10
+ model: resolveModel(config.agents.subagents.simple["deep-debugger"].model),
11
+ thinking: config.agents.subagents.simple["deep-debugger"].thinking,
12
+ max_turns: 120,
13
+ prompt_mode: "replace",
14
+ },
15
+ prompt: [
16
+ "<constraints>",
17
+ "You are a DEEP DEBUGGER. You do root-cause analysis on hard, persistent failures — failing tests, build/compile errors, regressions, flaky behavior — that quick attempts have NOT resolved. Do NOT engage for trivial or first-attempt errors.",
18
+ "These rules override your default helpfulness. Strict compliance is required.",
19
+ "You have write/edit access for DIAGNOSIS ONLY: creating repro scripts, adding temporary logging, or running experiments. You MUST NOT write the actual fix in the source code — find the root cause and recommend the fix; do NOT apply it. Remove any temporary diagnostic artifacts you create.",
20
+ "</constraints>",
21
+ "",
22
+ PRINCIPLES_BLOCK,
23
+ "",
24
+ TOOLS_BLOCK,
25
+ "",
26
+ "<task>",
27
+ "- Reproduce/inspect first: run the failing command, read the actual error and stack trace, check recent changes (git diff, cbm_changes).",
28
+ "- Form competing hypotheses. For each, gather evidence FOR and AGAINST with tool calls. Do not commit to the first plausible cause.",
29
+ "- Trace the failure to its true root, not the surface symptom. Use lsp findReferences / cbm_trace to follow the chain.",
30
+ "- Report: Symptom → Hypotheses considered (with evidence) → Root cause (with file:line proof) → Minimal recommended fix.",
31
+ "- If you cannot prove the root cause, say so: report the narrowed-down suspects and the single most useful next probe.",
32
+ "</task>",
33
+ ].join("\n"),
34
+ };
35
+ }
@@ -1,5 +1,5 @@
1
1
  import type { VariantConfig } from "../config.js";
2
- import { loadAllContextFiles } from "../context.js";
2
+ import { loadAllContextFiles, formatManifestBlock } from "../context.js";
3
3
  import { resolveModel, getModelInfo } from "../model-registry.js";
4
4
  import type { RepoInfo } from "../repo-utils.js";
5
5
  import { buildRepoContext } from "./repo-context.js";
@@ -8,7 +8,7 @@ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-
8
8
  export function createPlanReviewerAgent(
9
9
  variant: string,
10
10
  variants: Record<string, VariantConfig>,
11
- taskArtifacts: { userRequest: string; research: string; synthesizedPlan: string },
11
+ taskArtifacts: { userRequest: string; research: string; synthesizedPlan: string; manifest?: { title: string; path: string }[] },
12
12
  outputPath: string,
13
13
  contextDirs: string[],
14
14
  phase?: string,
@@ -63,10 +63,10 @@ export function createPlanReviewerAgent(
63
63
  "- BLOCKERS: (critical issues that must be fixed)",
64
64
  "- SUGGESTIONS: (improvements, not required)",
65
65
  "",
66
- "subagent_type is REQUIRED when spawning subagents — calls without it are rejected:",
67
- '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
68
- '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
69
- "Spawn multiple Explore agents in parallel for broad searches.",
66
+ "You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
67
+ '- Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
68
+ '- Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
69
+ "Spawn multiple explore agents in parallel for broad searches. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
70
70
  "</task>",
71
71
  "",
72
72
  // --- dynamic suffix ---
@@ -87,7 +87,7 @@ export function createPlanReviewerAgent(
87
87
  taskArtifacts.synthesizedPlan,
88
88
  ...(repoContext ? [repoContext] : []),
89
89
  "",
90
- "The artifacts above are already in your context. Do NOT re-read them from disk.",
90
+ formatManifestBlock(taskArtifacts.manifest ?? []),
91
91
  ].join("\n"),
92
92
  };
93
93
  }