@ilya-lesikov/pi-pi 0.7.0 → 0.8.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 (59) 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 +7 -7
  15. package/extensions/orchestrator/agents/constraints.test.ts +44 -0
  16. package/extensions/orchestrator/agents/constraints.ts +3 -0
  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 +23 -1
  24. package/extensions/orchestrator/command-handlers.ts +1 -1
  25. package/extensions/orchestrator/config.ts +5 -2
  26. package/extensions/orchestrator/context.test.ts +54 -0
  27. package/extensions/orchestrator/context.ts +65 -2
  28. package/extensions/orchestrator/event-handlers.test.ts +97 -1
  29. package/extensions/orchestrator/event-handlers.ts +176 -43
  30. package/extensions/orchestrator/flant-infra.test.ts +25 -0
  31. package/extensions/orchestrator/flant-infra.ts +91 -0
  32. package/extensions/orchestrator/index.ts +1 -1
  33. package/extensions/orchestrator/integration.test.ts +107 -12
  34. package/extensions/orchestrator/messages.test.ts +30 -0
  35. package/extensions/orchestrator/messages.ts +6 -0
  36. package/extensions/orchestrator/model-registry.test.ts +48 -1
  37. package/extensions/orchestrator/model-registry.ts +43 -1
  38. package/extensions/orchestrator/orchestrator.test.ts +113 -0
  39. package/extensions/orchestrator/orchestrator.ts +151 -2
  40. package/extensions/orchestrator/phases/brainstorm.ts +9 -20
  41. package/extensions/orchestrator/phases/implementation.test.ts +11 -0
  42. package/extensions/orchestrator/phases/implementation.ts +4 -6
  43. package/extensions/orchestrator/phases/planning.test.ts +16 -0
  44. package/extensions/orchestrator/phases/planning.ts +11 -4
  45. package/extensions/orchestrator/phases/review-task.ts +1 -4
  46. package/extensions/orchestrator/phases/review.test.ts +8 -0
  47. package/extensions/orchestrator/phases/review.ts +4 -3
  48. package/extensions/orchestrator/plannotator.ts +9 -6
  49. package/extensions/orchestrator/pp-menu.ts +168 -54
  50. package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
  51. package/extensions/orchestrator/pp-state-tools.ts +249 -0
  52. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
  53. package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
  54. package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
  55. package/extensions/orchestrator/test-helpers.ts +4 -1
  56. package/extensions/orchestrator/usage-tracker.ts +5 -1
  57. package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
  58. package/extensions/orchestrator/validate-artifacts.ts +2 -2
  59. package/package.json +1 -1
@@ -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,
@@ -81,10 +81,10 @@ export function createCodeReviewerAgent(
81
81
  "- MINOR: (nice to have)",
82
82
  "- OPEN QUESTIONS: (low-confidence concerns, speculative follow-ups)",
83
83
  "",
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.",
84
+ "You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — 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. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
88
88
  "</task>",
89
89
  "",
90
90
  // --- dynamic suffix ---
@@ -101,7 +101,7 @@ export function createCodeReviewerAgent(
101
101
  taskArtifacts.synthesizedPlan,
102
102
  ...(repoContext ? [repoContext] : []),
103
103
  "",
104
- "The artifacts above are already in your context. Do NOT re-read them from disk.",
104
+ formatManifestBlock(taskArtifacts.manifest ?? []),
105
105
  ].join("\n"),
106
106
  };
107
107
  }
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { 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
+
33
+ describe("constraintsBlock", () => {
34
+ it("guided implement block tells the agent to self-complete", () => {
35
+ const block = constraintsBlock("implement", "guided");
36
+ expect(block).toContain("call pp_phase_complete");
37
+ expect(block).not.toContain("Do NOT advance on your own");
38
+ });
39
+
40
+ it("guided plan block tells the agent to self-complete", () => {
41
+ const block = constraintsBlock("plan", "guided");
42
+ expect(block).toContain("call pp_phase_complete");
43
+ });
44
+ });
@@ -29,6 +29,9 @@ export function completionLine(phase: Phase, mode: TaskMode): string {
29
29
  if (phase === "brainstorm") {
30
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.";
31
31
  }
32
+ if (phase === "plan" || phase === "implement") {
33
+ 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.";
34
+ }
32
35
  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.";
33
36
  }
34
37
 
@@ -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
  }
@@ -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 createPlannerAgent(
9
9
  variant: string,
10
10
  variants: Record<string, VariantConfig>,
11
- taskArtifacts: { userRequest: string; research: string },
11
+ taskArtifacts: { userRequest: string; research: string; manifest?: { title: string; path: string }[] },
12
12
  outputPath: string,
13
13
  contextDirs: string[],
14
14
  phase?: string,
@@ -51,14 +51,15 @@ export function createPlannerAgent(
51
51
  "- ## Scope: 2-4 lines — what changes, what doesn't, critical constraints",
52
52
  "- ## Checklist: each item is - [ ] <outcome> — Done when: <observable condition>",
53
53
  " Each item = one independently verifiable outcome. No code snippets or file-by-file instructions.",
54
+ "- ## Pattern constraints: include whenever the task adds a type, function, parser, annotation, config key, enum, or user-facing value. For each, name the CLOSEST EXISTING analog (found by behavior, not filename) and the conventions the implementer MUST mirror: data shape (prefer one existing shape over parallel/duplicated state), spelling/casing of user-facing values (match existing — never invent a new casing), and parser/validation/error-handling shape. Acceptance criteria, not suggestions. Omit only if the task adds none of the above.",
54
55
  "- ## Blockers: unresolved issues blocking implementation (omit if none)",
55
56
  "- No other top-level sections allowed",
56
- "- Describe outcomes, not code-level mechanics",
57
+ "- Describe outcomes, not code-level mechanics, EXCEPT in ## Pattern constraints where the concrete analog and conventions are required",
57
58
  "",
58
- "subagent_type is REQUIRED when spawning subagents — calls without it are rejected:",
59
- '- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
60
- '- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
61
- "Spawn multiple Explore agents in parallel for broad searches.",
59
+ "You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
60
+ '- Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
61
+ '- Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
62
+ "Spawn multiple explore agents in parallel for broad searches. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
62
63
  "</task>",
63
64
  "",
64
65
  // --- dynamic suffix ---
@@ -74,7 +75,7 @@ export function createPlannerAgent(
74
75
  taskArtifacts.research,
75
76
  ...(repoContext ? [repoContext] : []),
76
77
  "",
77
- "The artifacts above are already in your context. Do NOT re-read them from disk.",
78
+ formatManifestBlock(taskArtifacts.manifest ?? []),
78
79
  ].join("\n"),
79
80
  };
80
81
  }
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getDefaultConfig, resolvePreset } from "../config.js";
3
+ import { DELEGATION_BLOCK } from "./tool-routing.js";
4
+ import { createAdvisorAgent } from "./advisor.js";
5
+ import { createDeepDebuggerAgent } from "./deep-debugger.js";
6
+ import { createReviewerAgent } from "./reviewer.js";
7
+ import { createTaskAgent } from "./task.js";
8
+ import { createPlannerAgent } from "./planner.js";
9
+ import { createPlanReviewerAgent } from "./plan-reviewer.js";
10
+ import { createCodeReviewerAgent } from "./code-reviewer.js";
11
+ import { createBrainstormReviewerAgent } from "./brainstorm-reviewer.js";
12
+
13
+ const config = getDefaultConfig();
14
+
15
+ describe("DELEGATION_BLOCK", () => {
16
+ it("covers all six free-form agents with lowercase registry names", () => {
17
+ for (const name of ["explore", "librarian", "task", "advisor", "deep-debugger", "reviewer"]) {
18
+ expect(DELEGATION_BLOCK).toContain(name);
19
+ }
20
+ });
21
+
22
+ it("states the reviewer and deep-debugger gating explicitly", () => {
23
+ expect(DELEGATION_BLOCK).toContain("ONLY when the user explicitly asks");
24
+ expect(DELEGATION_BLOCK).toMatch(/deep-debugger diagnoses/i);
25
+ expect(DELEGATION_BLOCK).toContain("must NOT write the actual fix");
26
+ });
27
+ });
28
+
29
+ describe("new free-form agent factories", () => {
30
+ it("advisor is read-only (no write/edit) and reasons in Diagnosis/Options/Recommendation", () => {
31
+ const a = createAdvisorAgent(config);
32
+ expect(a.frontmatter.tools).not.toContain("write");
33
+ expect(a.frontmatter.tools).not.toContain("edit");
34
+ expect(a.prompt).toContain("READ-ONLY");
35
+ expect(a.prompt).toContain("Diagnosis");
36
+ expect(a.prompt).toContain("Recommendation");
37
+ });
38
+
39
+ it("deep-debugger has write/edit but restricts writes to diagnosis only", () => {
40
+ const d = createDeepDebuggerAgent(config);
41
+ expect(d.frontmatter.tools).toContain("write");
42
+ expect(d.frontmatter.tools).toContain("edit");
43
+ expect(d.prompt).toContain("DIAGNOSIS ONLY");
44
+ expect(d.prompt).toContain("MUST NOT write the actual fix");
45
+ });
46
+
47
+ it("reviewer is read-only, retains bash for git diff, and is verdict-first", () => {
48
+ const r = createReviewerAgent(config);
49
+ expect(r.frontmatter.tools).toContain("bash");
50
+ expect(r.frontmatter.tools).not.toContain("write");
51
+ expect(r.frontmatter.tools).not.toContain("edit");
52
+ expect(r.prompt).toContain("git diff");
53
+ expect(r.prompt).toContain("VERY FIRST LINE");
54
+ expect(r.frontmatter.description).toContain("only when the user asks");
55
+ });
56
+ });
57
+
58
+ describe("task factory no longer bakes artifacts and stays explore/librarian-only", () => {
59
+ it("takes only config (no baked artifact arg) and does not inline USER REQUEST / SYNTHESIZED PLAN", () => {
60
+ expect(createTaskAgent.length).toBe(1);
61
+ const t = createTaskAgent(config);
62
+ expect(t.prompt).not.toContain("=== USER REQUEST ===");
63
+ expect(t.prompt).not.toContain("=== SYNTHESIZED PLAN ===");
64
+ expect(t.prompt).not.toContain("Do NOT re-read them from disk");
65
+ expect(t.prompt).toContain("ONLY explore/librarian");
66
+ expect(t.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
67
+ });
68
+ });
69
+
70
+ describe("phased factory prompts: manifest guidance replaces the do-not-re-read trailer", () => {
71
+ const planners = resolvePreset(config, "planners");
72
+ const planReviewers = resolvePreset(config, "planReviewers");
73
+ const codeReviewers = resolvePreset(config, "codeReviewers");
74
+ const brainstormReviewers = resolvePreset(config, "brainstormReviewers");
75
+ const manifest = [{ title: "Design Doc", path: "/t/artifacts/design.md" }];
76
+
77
+ it("planner lists manifest paths and restricts spawns to explore/librarian", () => {
78
+ const p = createPlannerAgent("opus", planners, { userRequest: "u", research: "r", manifest }, "/out.md", []);
79
+ expect(p.prompt).toContain("/t/artifacts/design.md");
80
+ expect(p.prompt).toContain("read them from disk with the read tool");
81
+ expect(p.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
82
+ });
83
+
84
+ it("plan-reviewer lists manifest paths and restricts spawns", () => {
85
+ const p = createPlanReviewerAgent(
86
+ "opus",
87
+ planReviewers,
88
+ { userRequest: "u", research: "r", synthesizedPlan: "p", manifest },
89
+ "/out.md",
90
+ [],
91
+ );
92
+ expect(p.prompt).toContain("/t/artifacts/design.md");
93
+ expect(p.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
94
+ });
95
+
96
+ it("code-reviewer lists manifest paths and restricts spawns", () => {
97
+ const c = createCodeReviewerAgent(
98
+ "opus",
99
+ codeReviewers,
100
+ { userRequest: "u", research: "r", synthesizedPlan: "p", manifest },
101
+ "/out.md",
102
+ [],
103
+ );
104
+ expect(c.prompt).toContain("/t/artifacts/design.md");
105
+ expect(c.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
106
+ });
107
+
108
+ it("brainstorm-reviewer restricts spawns and lists manifest paths when provided", () => {
109
+ const b = createBrainstormReviewerAgent(
110
+ "opus",
111
+ brainstormReviewers,
112
+ { userRequest: "u", research: "r", manifest },
113
+ "/out.md",
114
+ [],
115
+ );
116
+ expect(b.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
117
+ expect(b.prompt).toContain("/t/artifacts/design.md");
118
+ expect(b.prompt).toContain("read them from disk with the read tool");
119
+ });
120
+ });