@ilya-lesikov/pi-pi 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/3p/pi-subagents/package.json +0 -1
  2. package/3p/pi-subagents/src/agent-manager.ts +1 -0
  3. package/3p/pi-subagents/src/index.ts +1 -0
  4. package/3p/pi-subagents/src/types.ts +8 -0
  5. package/extensions/orchestrator/agents/advisor.ts +2 -1
  6. package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
  7. package/extensions/orchestrator/agents/constraints.test.ts +43 -10
  8. package/extensions/orchestrator/agents/constraints.ts +26 -5
  9. package/extensions/orchestrator/agents/deep-debugger.ts +7 -6
  10. package/extensions/orchestrator/agents/explore.ts +1 -1
  11. package/extensions/orchestrator/agents/librarian.ts +1 -1
  12. package/extensions/orchestrator/agents/planner.ts +1 -1
  13. package/extensions/orchestrator/agents/prompts.test.ts +135 -1
  14. package/extensions/orchestrator/agents/reviewer.ts +2 -2
  15. package/extensions/orchestrator/agents/task.ts +6 -2
  16. package/extensions/orchestrator/agents/tool-routing.ts +22 -6
  17. package/extensions/orchestrator/assumptions.test.ts +77 -0
  18. package/extensions/orchestrator/assumptions.ts +85 -0
  19. package/extensions/orchestrator/cbm.ts +4 -1
  20. package/extensions/orchestrator/cbm.which.test.ts +92 -0
  21. package/extensions/orchestrator/command-handlers.ts +9 -1
  22. package/extensions/orchestrator/config.test.ts +1 -3
  23. package/extensions/orchestrator/config.ts +3 -4
  24. package/extensions/orchestrator/context.test.ts +2 -7
  25. package/extensions/orchestrator/context.ts +3 -3
  26. package/extensions/orchestrator/doctor.test.ts +35 -0
  27. package/extensions/orchestrator/doctor.ts +4 -2
  28. package/extensions/orchestrator/event-handlers.more.test.ts +185 -0
  29. package/extensions/orchestrator/event-handlers.test.ts +22 -2
  30. package/extensions/orchestrator/event-handlers.ts +156 -18
  31. package/extensions/orchestrator/flant-infra.more.test.ts +55 -0
  32. package/extensions/orchestrator/flant-infra.test.ts +0 -3
  33. package/extensions/orchestrator/flant-infra.ts +17 -7
  34. package/extensions/orchestrator/integration.test.ts +355 -173
  35. package/extensions/orchestrator/orchestrator.ts +3 -9
  36. package/extensions/orchestrator/phases/brainstorm.test.ts +25 -7
  37. package/extensions/orchestrator/phases/brainstorm.ts +35 -71
  38. package/extensions/orchestrator/phases/implementation.test.ts +22 -0
  39. package/extensions/orchestrator/phases/implementation.ts +6 -0
  40. package/extensions/orchestrator/phases/machine.test.ts +0 -28
  41. package/extensions/orchestrator/phases/machine.ts +0 -38
  42. package/extensions/orchestrator/phases/planning.test.ts +27 -1
  43. package/extensions/orchestrator/phases/planning.ts +1 -1
  44. package/extensions/orchestrator/phases/review-task.test.ts +7 -0
  45. package/extensions/orchestrator/phases/review-task.ts +1 -1
  46. package/extensions/orchestrator/phases/review.test.ts +47 -10
  47. package/extensions/orchestrator/phases/review.ts +36 -25
  48. package/extensions/orchestrator/pp-menu.from.test.ts +65 -0
  49. package/extensions/orchestrator/pp-menu.leaves.test.ts +17 -0
  50. package/extensions/orchestrator/pp-menu.test.ts +106 -49
  51. package/extensions/orchestrator/pp-menu.ts +280 -134
  52. package/extensions/orchestrator/pp-state-tools.ts +1 -1
  53. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +2 -2
  54. package/extensions/orchestrator/rate-limit-fallback.ts +1 -1
  55. package/extensions/orchestrator/state.test.ts +17 -17
  56. package/extensions/orchestrator/state.ts +35 -12
  57. package/package.json +6 -3
  58. package/scripts/lib/smoke-resolve.mjs +99 -0
  59. package/scripts/postinstall.mjs +93 -0
  60. package/scripts/test-package.sh +62 -0
  61. package/scripts/postinstall.sh +0 -18
@@ -13,7 +13,6 @@
13
13
  "url": "https://github.com/tintinweb/pi-subagents/issues"
14
14
  },
15
15
  "keywords": [
16
- "pi-package",
17
16
  "pi",
18
17
  "pi-extension",
19
18
  "subagent",
@@ -174,6 +174,7 @@ export class AgentManager {
174
174
  // have no inline surface — stay visible instead of vanishing.
175
175
  isBackground: options.isBackground,
176
176
  invocation: options.invocation,
177
+ resolvedModelId: options.model ? `${options.model.provider}/${options.model.id}` : undefined,
177
178
  };
178
179
  this.agents.set(id, record);
179
180
 
@@ -381,6 +381,7 @@ export default function (pi: ExtensionAPI) {
381
381
  toolUses: record.toolUses,
382
382
  durationMs,
383
383
  tokens,
384
+ modelId: record.resolvedModelId,
384
385
  };
385
386
  }
386
387
 
@@ -126,6 +126,14 @@ export interface AgentRecord {
126
126
  isBackground?: boolean;
127
127
  /** Resolved spawn params, captured for UI display. Fixed at spawn time. */
128
128
  invocation?: AgentInvocation;
129
+ /**
130
+ * Fully-qualified model id (`provider/id`) this agent ran under, captured at
131
+ * spawn from the resolved model. Surfaced in lifecycle events so usage can be
132
+ * attributed to a real model instead of falling back to the description.
133
+ * Absent when the caller supplied no explicit model (the agent inherits the
134
+ * parent model).
135
+ */
136
+ resolvedModelId?: string;
129
137
  }
130
138
 
131
139
  export interface AgentInvocation {
@@ -8,7 +8,7 @@ export function createAdvisorAgent(entry: PoolEntry) {
8
8
  const info = getModelInfo(model);
9
9
  return {
10
10
  frontmatter: {
11
- description: "Deep-reasoning advisor for design decisions and 'why is this broken' analysis (pi-pi)",
11
+ description: "Deep-reasoning advisor that investigates ONE hard judgment call — a design/architecture tradeoff or a 'why is this broken' and returns a reasoned, evidence-backed recommendation; best for genuine judgment, not for locating code (use explore), fetching external docs (use librarian), or applying edits (pi-pi)",
12
12
  tools,
13
13
  model,
14
14
  thinking: entry.thinking,
@@ -34,6 +34,7 @@ export function createAdvisorAgent(entry: PoolEntry) {
34
34
  "- Scope recommendations by effort: name the quick fix vs the thorough one.",
35
35
  "- Structure your answer: Diagnosis (what is actually true, with file:line evidence) → Options & tradeoffs → Recommendation.",
36
36
  "- Be honest about uncertainty. If evidence is thin, say so and state what would resolve it.",
37
+ "- Take a position. On every judgment, say where you land AND what evidence would change your mind. Do NOT validate or hedge without committing to a view — empty agreement and non-answers are worthless to the caller. This targets the behavior, not any wordlist, so it holds in any language (illustrative anti-patterns: 'that could work', 'it depends' offered with no position taken).",
37
38
  "</task>",
38
39
  ].join("\n"),
39
40
  };
@@ -76,6 +76,7 @@ export function createCodeReviewerAgent(
76
76
  "Evidence requirements:",
77
77
  "- Every CRITICAL or MAJOR finding MUST cite file:line or backtick-quoted code",
78
78
  "- Never assert a problem without reading the actual code first",
79
+ "- You are read-only and MUST NOT run tests/builds: support findings ONLY with what your granted tools produce — the diff, the code you read, and lsp diagnostics. A concern that would need a test run, build, or runtime output you cannot obtain is NOT a finding — move it to Open Questions and state what evidence would settle it.",
79
80
  "- If you can't prove it with evidence, move it to Open Questions",
80
81
  "",
81
82
  "Perspectives to check:",
@@ -11,8 +11,8 @@ describe("completionLine", () => {
11
11
  }
12
12
  });
13
13
 
14
- it("guided review and debug stay hands-off (no unprompted self-complete)", () => {
15
- for (const phase of ["review", "debug"] as const) {
14
+ it("guided review stays hands-off (no unprompted self-complete)", () => {
15
+ for (const phase of ["review"] as const) {
16
16
  const line = completionLine(phase, "guided");
17
17
  expect(line).toContain("Do NOT advance on your own or call pp_phase_complete unprompted");
18
18
  }
@@ -23,8 +23,8 @@ describe("completionLine", () => {
23
23
  expect(line).toContain("Do NOT call pp_phase_complete yourself");
24
24
  });
25
25
 
26
- it("guided brainstorm/review/debug carve out a /pp banner exception", () => {
27
- for (const phase of ["brainstorm", "review", "debug"] as const) {
26
+ it("guided brainstorm/review carve out a /pp banner exception", () => {
27
+ for (const phase of ["brainstorm", "review"] as const) {
28
28
  expect(completionLine(phase, "guided")).toContain(
29
29
  "a /pp menu banner",
30
30
  );
@@ -36,7 +36,7 @@ describe("completionLine", () => {
36
36
  for (const phase of ["plan", "implement"] as const) {
37
37
  expect(completionLine(phase, "guided")).not.toContain("a /pp menu banner");
38
38
  }
39
- for (const phase of ["brainstorm", "review", "debug", "plan", "implement"] as const) {
39
+ for (const phase of ["brainstorm", "review", "plan", "implement"] as const) {
40
40
  expect(completionLine(phase, "autonomous")).not.toContain("a /pp menu banner");
41
41
  }
42
42
  });
@@ -48,7 +48,7 @@ describe("completionLine", () => {
48
48
  });
49
49
 
50
50
  it("guided handoff phases require the standardized closing block", () => {
51
- for (const phase of ["brainstorm", "review", "debug"] as const) {
51
+ for (const phase of ["brainstorm", "review"] as const) {
52
52
  const line = completionLine(phase, "guided");
53
53
  expect(line).toContain("the standardized block");
54
54
  expect(line).toContain("Advance via the /pp menu");
@@ -58,6 +58,18 @@ describe("completionLine", () => {
58
58
  it("autonomous phases do not emit the prose closing block", () => {
59
59
  expect(completionLine("brainstorm", "autonomous")).not.toContain("Advance via the /pp menu");
60
60
  });
61
+
62
+ it("quick phase does not self-complete for a mere question/answer", () => {
63
+ const line = completionLine("quick", "guided");
64
+ expect(line).toContain("ONLY after you have actually performed the requested work");
65
+ expect(line).toMatch(/do NOT call pp_phase_complete/i);
66
+ expect(line).toContain("/pp");
67
+ });
68
+
69
+ it("quick phase still self-completes after real work", () => {
70
+ const line = completionLine("quick", "guided");
71
+ expect(line).toContain("call pp_phase_complete");
72
+ });
61
73
  });
62
74
 
63
75
  describe("closingBlockInstruction", () => {
@@ -79,8 +91,8 @@ describe("closingBlockInstruction", () => {
79
91
  expect(review.indexOf("## Review Summary")).toBeLessThan(review.indexOf("✅ <one-sentence summary"));
80
92
  });
81
93
 
82
- it("does NOT emit the Review Summary schema for brainstorm or debug closes", () => {
83
- for (const phase of ["brainstorm", "debug"] as const) {
94
+ it("does NOT emit the Review Summary schema for brainstorm closes", () => {
95
+ for (const phase of ["brainstorm"] as const) {
84
96
  expect(closingBlockInstruction(phase)).not.toContain("## Review Summary");
85
97
  }
86
98
  });
@@ -98,11 +110,32 @@ describe("constraintsBlock", () => {
98
110
  expect(block).toContain("call pp_phase_complete");
99
111
  });
100
112
 
101
- it("guided brainstorm/review/debug blocks embed the /pp banner exception", () => {
102
- for (const phase of ["brainstorm", "review", "debug"] as const) {
113
+ it("guided brainstorm/review blocks embed the /pp banner exception", () => {
114
+ for (const phase of ["brainstorm", "review"] as const) {
103
115
  const block = constraintsBlock(phase, "guided");
104
116
  expect(block).toContain("a /pp menu banner");
105
117
  expect(block).toContain("e.g. an auto-review loop");
106
118
  }
107
119
  });
120
+
121
+ it("guided phases tell the agent to ask rather than silently assume", () => {
122
+ for (const phase of ["brainstorm", "plan", "implement", "review"] as const) {
123
+ const block = constraintsBlock(phase, "guided");
124
+ expect(block).toContain("do NOT silently assume");
125
+ expect(block).toContain("ask the");
126
+ expect(block).not.toContain("artifacts/ASSUMPTIONS.md");
127
+ }
128
+ });
129
+
130
+ it("autonomous phases record unverified assumptions in artifacts/ASSUMPTIONS.md with the fixed fields", () => {
131
+ for (const phase of ["brainstorm", "plan", "implement", "review"] as const) {
132
+ const block = constraintsBlock(phase, "autonomous");
133
+ expect(block).toContain("artifacts/ASSUMPTIONS.md");
134
+ for (const field of ["statement", "confidence", "basis", "invalidation test", "decision impact"]) {
135
+ expect(block).toContain(field);
136
+ }
137
+ expect(block).toContain("_No open assumptions._");
138
+ expect(block).toContain("never fabricate");
139
+ }
140
+ });
108
141
  });
@@ -26,8 +26,30 @@ const UNIVERSAL_RULES = [
26
26
  "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, then get explicit approval. Don't silently invent and bury such choices.",
27
27
  ].join("\n");
28
28
 
29
+ // Assumption-surfacing (#B). Interactive phases: ask instead of guessing. Autonomous
30
+ // phases have no one to ask, so every unverified assumption is logged to the optional
31
+ // artifacts/ASSUMPTIONS.md (absent = none) with a fixed field shape, and the end-of-run
32
+ // summary lists them all. Mode-aware because the two behaviors are mutually exclusive.
33
+ function assumptionsRule(mode: TaskMode): string {
34
+ if (mode === "autonomous") {
35
+ return (
36
+ "Assumptions: there is no one to ask, so do NOT silently assume. Whenever you commit to an " +
37
+ "unverified assumption, append it to artifacts/ASSUMPTIONS.md (create it on first use) as one list " +
38
+ "item with these exact fields — statement, confidence (low/med/high), basis, invalidation test, " +
39
+ "decision impact — and a status marker (open/resolved); update the marker to resolved when you later " +
40
+ "confirm or refute it, keeping the entry. If a phase has no open assumptions the file may be absent " +
41
+ "or contain only `_No open assumptions._` — never fabricate entries."
42
+ );
43
+ }
44
+ return (
45
+ "Assumptions: do NOT silently assume. When a decision hinges on something you are not sure of, ask the " +
46
+ "user (a focused ask_user) rather than guessing. If you must proceed on an unverified assumption, state " +
47
+ "it plainly to the user so they can correct it."
48
+ );
49
+ }
50
+
29
51
  export function isReadOnlyPhase(phase: Phase): boolean {
30
- return phase === "brainstorm" || phase === "debug" || phase === "review" || phase === "plan";
52
+ return phase === "brainstorm" || phase === "review" || phase === "plan";
31
53
  }
32
54
 
33
55
  export function phaseConstraint(phase: Phase): string {
@@ -37,13 +59,12 @@ export function phaseConstraint(phase: Phase): string {
37
59
  return READONLY_CONSTRAINT;
38
60
  }
39
61
 
40
- // Guided phases that stop and hand back to the user (brainstorm, review, debug) end their turn
62
+ // Guided phases that stop and hand back to the user (brainstorm, review) end their turn
41
63
  // with prose rather than a tool call. To keep that handoff consistent, the model must close with
42
64
  // this exact block. NEXT_PHASE_LABEL supplies the phase the /pp menu advances into.
43
65
  const NEXT_PHASE_LABEL: Partial<Record<Phase, string>> = {
44
66
  brainstorm: "plan",
45
67
  review: "plan",
46
- debug: "plan",
47
68
  };
48
69
 
49
70
  // The structured summary a review must print when it finishes, on BOTH review finish paths
@@ -91,7 +112,7 @@ export function closingBlockInstruction(phase: Phase): string {
91
112
 
92
113
  export function completionLine(phase: Phase, mode: TaskMode): string {
93
114
  if (phase === "quick") {
94
- return "When the user's request is complete, call pp_phase_complete. Do NOT stop and wait for the user before then.";
115
+ return "Call pp_phase_complete ONLY after you have actually performed the requested work (an edit, a command, a produced artifact). If the turn merely answered a question or provided information with no action taken, do NOT call pp_phase_complete — end the turn with your answer, at most suggesting the user run /pp if they want to start a tracked task. When you did do the requested work, call pp_phase_complete and do NOT stop to wait for the user before then.";
95
116
  }
96
117
  if (mode === "autonomous") {
97
118
  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.";
@@ -107,7 +128,6 @@ export function completionLine(phase: Phase, mode: TaskMode): string {
107
128
 
108
129
  const PHASE_IDENTITY: Record<string, string> = {
109
130
  brainstorm: "You clarify the request and research the codebase to produce USER_REQUEST.md and RESEARCH.md.",
110
- debug: "You diagnose the problem and research the codebase to produce USER_REQUEST.md and RESEARCH.md — investigation only, no fixes.",
111
131
  plan: "You synthesize the planner outputs into one plan — you do not write a plan from scratch, and you do not implement.",
112
132
  implement: "You implement the approved plan.",
113
133
  review: "You review the code changes to produce USER_REQUEST.md and RESEARCH.md capturing the findings — you do not apply fixes.",
@@ -123,6 +143,7 @@ export function constraintsBlock(phase: Phase, mode: TaskMode): string {
123
143
  "These rules override your default helpfulness and any next step you infer. Strict compliance is required.",
124
144
  phaseConstraint(phase),
125
145
  UNIVERSAL_RULES,
146
+ assumptionsRule(mode),
126
147
  completionLine(phase, mode),
127
148
  "</constraints>",
128
149
  ].join("\n");
@@ -8,7 +8,7 @@ export function createDeepDebuggerAgent(entry: PoolEntry) {
8
8
  const info = getModelInfo(model);
9
9
  return {
10
10
  frontmatter: {
11
- description: "Deep root-cause analysis for HARD, persistent failures — not every error (pi-pi)",
11
+ description: "Diagnoses the root cause of a HARD, persistent failure and returns a diagnosis (it does NOT apply the fix) best when a bug resists the obvious fix; not every error, and not for trivial/obvious failures (pi-pi)",
12
12
  tools,
13
13
  model,
14
14
  thinking: entry.thinking,
@@ -29,11 +29,12 @@ export function createDeepDebuggerAgent(entry: PoolEntry) {
29
29
  toolsBlock(parseToolNames(tools)),
30
30
  "",
31
31
  "<task>",
32
- "- Reproduce/inspect first: run the failing command, read the actual error and stack trace, check recent changes (git diff, cbm_changes).",
33
- "- Form competing hypotheses. For each, gather evidence FOR and AGAINST with tool calls. Do not commit to the first plausible cause.",
34
- "- Trace the failure to its true root, not the surface symptom. Use lsp findReferences / cbm_trace to follow the chain.",
35
- "- Report: Symptom Hypotheses considered (with evidence) Root cause (with file:line proof) Minimal recommended fix.",
36
- "- If you cannot prove the root cause, say so: report the narrowed-down suspects and the single most useful next probe.",
32
+ "Work the phases in order no fix recommendation until root cause is established:",
33
+ "1. INVESTIGATE: reproduce/inspect first run the failing command, read the actual error and stack trace, check recent changes (git diff, cbm_changes). Trace the failure to its true root, not the surface symptom, using lsp findReferences / cbm_trace to follow the chain to its source.",
34
+ "2. PATTERN ANALYSIS: form competing hypotheses; for each, gather evidence FOR and AGAINST with tool calls. Do not commit to the first plausible cause.",
35
+ "3. HYPOTHESIS: commit to a SINGLE hypothesis stated as 'X is the root cause because Y', then test it by changing ONE variable at a time — don't change several things at once.",
36
+ "4. RECOMMEND: report Symptom Hypotheses considered (with evidence) Root cause (with file:line proof) Minimal recommended fix.",
37
+ "Apply this discipline ESPECIALLY under time pressure or when the issue looks simple enough to 'just fix' — that is when skipping investigation causes the most wasted work. If you cannot prove the root cause, say so plainly (do NOT pretend to know): report the narrowed-down suspects and the single most useful next probe.",
37
38
  "</task>",
38
39
  ].join("\n"),
39
40
  };
@@ -9,7 +9,7 @@ export function createExploreAgent(config: PiPiConfig) {
9
9
  const info = getModelInfo(model);
10
10
  return {
11
11
  frontmatter: {
12
- description: "Codebase explorer (pi-pi)",
12
+ description: "Finds where code lives and how a flow connects across files — best for locating symbols, tracing usages, and mapping a subsystem; not for design judgment or applying edits (pi-pi)",
13
13
  tools,
14
14
  model,
15
15
  thinking,
@@ -9,7 +9,7 @@ export function createLibrarianAgent(config: PiPiConfig) {
9
9
  const info = getModelInfo(model);
10
10
  return {
11
11
  frontmatter: {
12
- description: "External docs researcher (pi-pi)",
12
+ description: "Researches external libraries, APIs, and framework docs from the web and installed packages — best when the answer lives outside this repo; not for searching this codebase (use explore) or applying edits (pi-pi)",
13
13
  tools,
14
14
  model,
15
15
  thinking,
@@ -54,7 +54,7 @@ export function createPlannerAgent(
54
54
  "- Start with # Plan",
55
55
  "- ## Scope: 2-4 lines — what changes, what doesn't, critical constraints",
56
56
  "- ## Checklist: each item is - [ ] <outcome> — Done when: <observable condition>",
57
- " Each item = one independently verifiable outcome. No code snippets or file-by-file instructions.",
57
+ " Each item = one independently verifiable outcome, right-sized to the smallest chunk worth verifying on its own. No code snippets or file-by-file instructions. Nest these INSIDE the Done-when condition (do not add sections or code): (a) a verification method — how the outcome is proven (test passes, diagnostics clean, command output); (b) for items meant for parallel delegation, the interface it consumes/produces (what earlier outcomes it depends on, what later ones depend on it); (c) NO unresolved items — an item that defers a decision instead of stating a concrete outcome is a plan failure; resolve it now (in any language: a placeholder such as 'TBD'/'TODO'/'decide later' is only an illustrative example of the deferral to avoid, not the literal thing being matched).",
58
58
  "- ## 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.",
59
59
  "- ## Blockers: unresolved issues blocking implementation (omit if none)",
60
60
  "- No other top-level sections allowed",
@@ -1,10 +1,12 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { getDefaultConfig, resolvePreset } from "../config.js";
3
- import { delegationBlock, toolsBlock, parseToolNames } from "./tool-routing.js";
3
+ import { delegationBlock, toolsBlock, parseToolNames, PRINCIPLES_BLOCK, IMPLEMENTATION_PRINCIPLES_BLOCK } from "./tool-routing.js";
4
4
  import { createAdvisorAgent } from "./advisor.js";
5
5
  import { createDeepDebuggerAgent } from "./deep-debugger.js";
6
6
  import { createReviewerAgent } from "./reviewer.js";
7
7
  import { createTaskAgent } from "./task.js";
8
+ import { createExploreAgent } from "./explore.js";
9
+ import { createLibrarianAgent } from "./librarian.js";
8
10
  import { createPlannerAgent } from "./planner.js";
9
11
  import { createPlanReviewerAgent } from "./plan-reviewer.js";
10
12
  import { createCodeReviewerAgent } from "./code-reviewer.js";
@@ -61,6 +63,66 @@ describe("toolsBlock only advertises granted tools", () => {
61
63
  });
62
64
  });
63
65
 
66
+ describe("PRINCIPLES_BLOCK degrees-of-freedom split", () => {
67
+ it("implementation-only code-style rules live in the implementation block, not the shared block", () => {
68
+ for (const phrase of [
69
+ "NEVER comment a private (non-exported) symbol",
70
+ "volatile detail",
71
+ "Prefer fewer, larger functions",
72
+ "inline it",
73
+ "Keep everything as private as possible",
74
+ "DO NOT WRITE COMMENTS",
75
+ "No temporary artifacts",
76
+ "Smallest viable change",
77
+ "Understand before modifying",
78
+ ]) {
79
+ expect(IMPLEMENTATION_PRINCIPLES_BLOCK).toContain(phrase);
80
+ expect(PRINCIPLES_BLOCK).not.toContain(phrase);
81
+ }
82
+ });
83
+
84
+ it("shared reasoning/evidence rules stay in the shared block", () => {
85
+ for (const phrase of ["Verify, don't assume", "Evidence over claims", "Match existing patterns", "Think critically"]) {
86
+ expect(PRINCIPLES_BLOCK).toContain(phrase);
87
+ }
88
+ });
89
+
90
+ const planners = resolvePreset(config, "planners");
91
+ const artifacts = { userRequest: "u", research: "r", manifest: [] as { title: string; path: string }[] };
92
+ const readOnlyFactories: [string, { prompt: string }][] = [
93
+ ["explore", createExploreAgent(config)],
94
+ ["librarian", createLibrarianAgent(config)],
95
+ ["advisor", createAdvisorAgent({ model: "anthropic/claude-fable-latest", thinking: "high" })],
96
+ ["deep-debugger", createDeepDebuggerAgent({ model: "openai/gpt-latest", thinking: "high" })],
97
+ ["reviewer", createReviewerAgent({ model: "openai/gpt-latest", thinking: "high" })],
98
+ ["planner", createPlannerAgent("opus", planners, artifacts, "/out.md", [])],
99
+ ["plan-reviewer", createPlanReviewerAgent("opus", resolvePreset(config, "planReviewers"), { userRequest: "u", research: "r", synthesizedPlan: "p", manifest: [] }, "/out.md", [])],
100
+ ["code-reviewer", createCodeReviewerAgent("opus", resolvePreset(config, "codeReviewers"), { userRequest: "u", research: "r", synthesizedPlan: "p", manifest: [] }, "/out.md", [])],
101
+ ["brainstorm-reviewer", createBrainstormReviewerAgent("opus", resolvePreset(config, "brainstormReviewers"), artifacts, "/out.md", [])],
102
+ ];
103
+
104
+ it("the shared block is embedded in ALL ten agent factory prompts", () => {
105
+ const all = [...readOnlyFactories.map(([, f]) => f), createTaskAgent(config)];
106
+ for (const f of all) {
107
+ expect(f.prompt).toContain("Evidence over claims");
108
+ expect(f.prompt).toContain("Verify, don't assume");
109
+ }
110
+ });
111
+
112
+ it("read-only factories do NOT carry implementation-only code-style rules", () => {
113
+ for (const [, f] of readOnlyFactories) {
114
+ expect(f.prompt).not.toContain("Keep everything as private as possible");
115
+ expect(f.prompt).not.toContain("NEVER comment a private (non-exported) symbol");
116
+ }
117
+ });
118
+
119
+ it("the edit-capable task factory DOES carry implementation-only code-style rules", () => {
120
+ const t = createTaskAgent(config);
121
+ expect(t.prompt).toContain("Keep everything as private as possible");
122
+ expect(t.prompt).toContain("NEVER comment a private (non-exported) symbol");
123
+ });
124
+ });
125
+
64
126
  describe("new free-form agent factories", () => {
65
127
  it("advisor is read-only (no write/edit) and reasons in Diagnosis/Options/Recommendation", () => {
66
128
  const a = createAdvisorAgent({ model: "anthropic/claude-fable-latest", thinking: "high" });
@@ -160,3 +222,75 @@ describe("phased factory prompts: manifest guidance replaces the do-not-re-read
160
222
  expect(b.prompt).toContain("read them from disk with the read tool");
161
223
  });
162
224
  });
225
+
226
+ describe("routing-contract descriptions (what / when / exclusion)", () => {
227
+ it("explore, librarian, and task descriptions state a fit and an exclusion, third person, with the (pi-pi) suffix", () => {
228
+ const descs = {
229
+ explore: createExploreAgent(config).frontmatter.description,
230
+ librarian: createLibrarianAgent(config).frontmatter.description,
231
+ task: createTaskAgent(config).frontmatter.description,
232
+ };
233
+ for (const d of Object.values(descs)) {
234
+ expect(d).toContain("best");
235
+ expect(d).toMatch(/not for|not when/i);
236
+ expect(d.endsWith("(pi-pi)")).toBe(true);
237
+ }
238
+ expect(descs.explore).toMatch(/locat|find|trac/i);
239
+ expect(descs.librarian).toMatch(/external|docs|librar/i);
240
+ expect(descs.task).toMatch(/implementation|slice/i);
241
+ });
242
+
243
+ it("extends the advisor / deep-debugger / reviewer descriptions with what+when+exclusion, preserving protected exclusions", () => {
244
+ const advisor = createAdvisorAgent({ model: "anthropic/claude-fable-latest", thinking: "high" }).frontmatter.description;
245
+ const debugger_ = createDeepDebuggerAgent({ model: "openai/gpt-latest", thinking: "high" }).frontmatter.description;
246
+ const reviewer = createReviewerAgent({ model: "openai/gpt-latest", thinking: "high" }).frontmatter.description;
247
+ for (const d of [advisor, debugger_, reviewer]) {
248
+ expect(d).toMatch(/not for|not every|not as|never/i);
249
+ expect(d.endsWith("(pi-pi)")).toBe(true);
250
+ }
251
+ expect(debugger_).toContain("not every error");
252
+ expect(reviewer).toContain("only when the user asks");
253
+ expect(advisor).toMatch(/judgment|tradeoff|why is this broken/i);
254
+ });
255
+
256
+ it("delegationBlock remains the sole owner of numeric routing thresholds", () => {
257
+ const pools = {
258
+ advisors: [{ name: "advisor_x_high", model: "anthropic/claude-fable-latest", family: "fable", tier: "xsmart", thinking: "high" }],
259
+ reviewers: [{ name: "reviewer_y_high", model: "openai/gpt-latest", family: "gpt", tier: "smart", thinking: "high" }],
260
+ deepDebuggers: [{ name: "deep-debugger_z_high", model: "openai/gpt-latest", family: "gpt", tier: "smart", thinking: "high" }],
261
+ };
262
+ expect(delegationBlock("opus", pools)).toContain("2–3 parallel");
263
+ expect(delegationBlock("opus", pools)).toContain("4+ only");
264
+ for (const d of [createExploreAgent(config).frontmatter.description, createTaskAgent(config).frontmatter.description]) {
265
+ expect(d).not.toMatch(/2–3|4\+/);
266
+ }
267
+ });
268
+ });
269
+
270
+ describe("affordance-aligned evidence gates", () => {
271
+ it("the edit-capable task delegate carries the evidence gate + N/A path", () => {
272
+ const t = createTaskAgent(config);
273
+ expect(t.prompt).toContain("Verification gate");
274
+ expect(t.prompt).toContain("not applicable");
275
+ expect(t.prompt).toMatch(/in any language/i);
276
+ });
277
+
278
+ it("read-only reviewer / code-reviewer restrict evidence to what their tools can produce", () => {
279
+ const r = createReviewerAgent({ model: "openai/gpt-latest", thinking: "high" });
280
+ const c = createCodeReviewerAgent("opus", resolvePreset(config, "codeReviewers"), { userRequest: "u", research: "r", synthesizedPlan: "p", manifest: [] }, "/out.md", []);
281
+ for (const p of [r.prompt, c.prompt]) {
282
+ expect(p).toMatch(/MUST NOT run tests|not run test suites/i);
283
+ expect(p).toMatch(/OPEN QUESTIONS|Open Questions/);
284
+ }
285
+ });
286
+ });
287
+
288
+ describe("advisor anti-sycophancy", () => {
289
+ it("requires taking a position + naming what would change it, behavior-framed with any quoted phrase marked as an example", () => {
290
+ const a = createAdvisorAgent({ model: "anthropic/claude-fable-latest", thinking: "high" }).prompt;
291
+ expect(a).toContain("Take a position");
292
+ expect(a).toMatch(/what evidence would change|what would change/i);
293
+ expect(a).toMatch(/in any language|targets the behavior/i);
294
+ expect(a).toMatch(/illustrative anti-patterns[^\n]{0,40}'that could work'/);
295
+ });
296
+ });
@@ -8,7 +8,7 @@ export function createReviewerAgent(entry: PoolEntry) {
8
8
  const info = getModelInfo(model);
9
9
  return {
10
10
  frontmatter: {
11
- description: "Code reviewer for changes/diffs with severity-rated findings — spawn only when the user asks for a review (pi-pi)",
11
+ description: "Read-only code reviewer that inspects a change/diff and returns severity-rated findings with file:line anchors (never edits) best for a focused review of completed work; spawn only when the user asks for a review, not as a routine step (pi-pi)",
12
12
  tools,
13
13
  model,
14
14
  thinking: entry.thinking,
@@ -38,7 +38,7 @@ export function createReviewerAgent(entry: PoolEntry) {
38
38
  "",
39
39
  "Review criteria: logic errors, off-by-ones, null/edge handling, race conditions; correctness vs intent; error handling and type safety; missing or untested paths.",
40
40
  "",
41
- "Evidence: every CRITICAL or MAJOR finding MUST cite file:line or quoted code. Never assert a problem without reading the code. Low-confidence concerns go under OPEN QUESTIONS.",
41
+ "Evidence: every CRITICAL or MAJOR finding MUST cite file:line or quoted code. Never assert a problem without reading the code. You are read-only and MUST NOT run tests/builds, so support each finding ONLY with what your granted tools can produce — the diff, the code you read, and lsp diagnostics. If a concern cannot be proven with those (it would need a test run, build, or runtime output you cannot obtain), do NOT assert it as a finding: move it to OPEN QUESTIONS and state what evidence would settle it.",
42
42
  "",
43
43
  "Format — verdict on the FIRST LINE, then:",
44
44
  "VERDICT: APPROVE | NEEDS_CHANGES",
@@ -1,6 +1,6 @@
1
1
  import type { PiPiConfig } from "../config.js";
2
2
  import { getModelInfo, resolveModel } from "../model-registry.js";
3
- import { toolsBlock, parseToolNames, identityBlock, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK, FAILURE_RECOVERY } from "./tool-routing.js";
3
+ import { toolsBlock, parseToolNames, identityBlock, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK, IMPLEMENTATION_PRINCIPLES_BLOCK, FAILURE_RECOVERY } from "./tool-routing.js";
4
4
 
5
5
  export function createTaskAgent(config: PiPiConfig) {
6
6
  const model = resolveModel(config.agents.subagents.simple.task.model);
@@ -9,7 +9,7 @@ export function createTaskAgent(config: PiPiConfig) {
9
9
  const info = getModelInfo(model);
10
10
  return {
11
11
  frontmatter: {
12
- description: "Implementation subtask (pi-pi)",
12
+ description: "Carries out a self-contained, parallelizable implementation slice end to end — best for a well-scoped chunk you could hand off; not for open-ended design, whole-task ownership, or work you're mid-edit on yourself (pi-pi)",
13
13
  tools,
14
14
  model,
15
15
  thinking,
@@ -29,6 +29,8 @@ export function createTaskAgent(config: PiPiConfig) {
29
29
  "",
30
30
  PRINCIPLES_BLOCK,
31
31
  "",
32
+ IMPLEMENTATION_PRINCIPLES_BLOCK,
33
+ "",
32
34
  toolsBlock(parseToolNames(tools)),
33
35
  "",
34
36
  FAILURE_RECOVERY,
@@ -40,6 +42,8 @@ export function createTaskAgent(config: PiPiConfig) {
40
42
  " Do NOT spawn task, advisor, deep-debugger, or reviewer subagents.",
41
43
  "- Before modifying a function, use lsp findReferences to understand all callers",
42
44
  "- After editing files, run lsp diagnostics and fix errors before moving on",
45
+ "- Verification gate: before reporting your subtask done, produce fresh tool output that proves it (lsp diagnostics clean, a passing test, expected cbm_changes) and cite it. The gate is on whether that proving evidence EXISTS, not on wording, so it holds in any language. If a claim cannot be proven with your granted tools, say so and state why (\"not applicable — <reason>\") rather than implying verification.",
46
+ "- Test-first policy (conditional): for a behavior change or bug fix where an automated test is feasible, write/reproduce the FAILING test first, then make it pass; otherwise state the verification method before editing. No universal test-first mandate and no delete-untested-code rule — choose the path that produces real evidence.",
43
47
  "- Your subtask and task context (USER_REQUEST, RESEARCH, and a manifest of additional documents) are provided in the spawn message. Read the manifested files from disk if relevant.",
44
48
  "</task>",
45
49
  ].join("\n"),
@@ -4,10 +4,6 @@ export const EXA_TOOLS = "exa_search, exa_fetch";
4
4
  export const PRINCIPLES_BLOCK = [
5
5
  "<principles>",
6
6
  "- Verify, don't assume. Check actual state with tools before making changes. Never guess paths, types, or APIs.",
7
- "- Understand before modifying. Read the code, trace callers, check types BEFORE editing. Compiling ≠ correct.",
8
- "- Smallest viable change. Do what was asked, nothing more. Don't broaden scope, don't refactor adjacent code.",
9
- "- No temporary artifacts. No console.log, TODO, HACK, debugger, or commented-out code left behind.",
10
- "- DO NOT WRITE COMMENTS. This is a hard rule, not a preference. Almost every comment an LLM writes is noise: it restates the code, repeats the function/variable name, narrates the obvious, or labels sections. NEVER write any of those. The ONLY allowed comments are (1) a genuine WHY that the code cannot express — a non-obvious constraint, workaround, or gotcha a reader would otherwise get wrong, or (2) required public-API/doc-comment syntax. If a comment restates WHAT the code does, delete it. When unsure, do not comment. Match the existing comment density of the surrounding code — if neighbors have none, add none.",
11
7
  "- Evidence over claims. 'It should work' is not proof. Show fresh tool output (lsp diagnostics, test results, build output).",
12
8
  "- Match existing patterns. Before adding a type, function, or user-facing value, find how the codebase already solves the most similar problem — search by behavior, not by filename — and mirror its shape, naming, error handling, and conventions. Reading one neighboring file is not enough.",
13
9
  "- Be concise and dense: minimum words, no preamble/filler/restatement. Don't narrate what you're about to do or just did.",
@@ -15,6 +11,24 @@ export const PRINCIPLES_BLOCK = [
15
11
  "</principles>",
16
12
  ].join("\n");
17
13
 
14
+ // Implementation-only code-style rules. Injected ONLY into agents that edit project
15
+ // source (the task subagent, and the main agent in edit-capable phases). Read-only
16
+ // judges (explore, librarian, advisor, reviewer, code-reviewer, plan-reviewer,
17
+ // brainstorm-reviewer, planner, deep-debugger) do NOT carry these — the deep-debugger
18
+ // has write/edit for throwaway diagnostics only, so it is intentionally read-only for
19
+ // PROJECT-SOURCE purposes and receives only the shared block above.
20
+ export const IMPLEMENTATION_PRINCIPLES_BLOCK = [
21
+ "<implementation_principles>",
22
+ "- Understand before modifying. Read the code, trace callers, check types BEFORE editing. Compiling ≠ correct.",
23
+ "- Smallest viable change. Do what was asked, nothing more. Don't broaden scope, don't refactor adjacent code.",
24
+ "- No temporary artifacts. No console.log, TODO, HACK, debugger, or commented-out code left behind.",
25
+ "- DO NOT WRITE COMMENTS. This is a hard rule, not a preference. Almost every comment an LLM writes is noise: it restates the code, repeats the function/variable name, narrates the obvious, or labels sections. NEVER write any of those. The ONLY allowed comments are (1) a genuine WHY that the code cannot express — a non-obvious constraint, workaround, or gotcha a reader would otherwise get wrong, or (2) required public-API/doc-comment syntax. If a comment restates WHAT the code does, delete it. When unsure, do not comment. Match the existing comment density of the surrounding code — if neighbors have none, add none.",
26
+ "- NEVER comment a private (non-exported) symbol. Also NEVER write a comment that embeds volatile detail that drifts out of date — flag/option names, constant values, or a restatement of what a flag/function does. A WHY comment survives refactors; a WHAT comment rots. If the only thing a comment adds is a name or a value already in the code, delete it.",
27
+ "- Prefer fewer, larger functions over many tiny ones. Do NOT extract a helper used in only one place just to name a step — inline it. Extract only when it removes real duplication or the extracted unit is independently meaningful and reused. A one-line or single-caller helper is usually noise.",
28
+ "- Keep everything as private as possible. Default to non-exported/file-local (or the language's most restricted visibility). Export or widen visibility ONLY when a symbol genuinely needs cross-module use — never 'just in case' or to make a test reach an internal.",
29
+ "</implementation_principles>",
30
+ ].join("\n");
31
+
18
32
  export const FAILURE_RECOVERY = [
19
33
  "# Failure recovery",
20
34
  "",
@@ -54,8 +68,10 @@ const TOOL_SEGMENTS: ToolSegment[] = [
54
68
  text:
55
69
  "**pp_checkout_pr_head** (review phase, PR-scoped only): after resolving a repo's PR (e.g. `gh pr view " +
56
70
  "--json headRefName,headRefOid`), call this once per repo to land it on its PR head before reviewing. " +
57
- "The extension fast-forwards a clean branch to the head; if the tree is dirty, on a different branch, or " +
58
- "diverged it HALTS and returns a message to relay to the user. Do NOT call it for a " +
71
+ "On a clean tree the extension fast-forwards the PR head's branch when behind, or switches to it from a " +
72
+ "different branch after fetching origin and verifying the fetched tip matches the PR head commit; if the " +
73
+ "tree is dirty, HEAD is detached, the branch has diverged, or the head is on a fork/unfetchable ref it " +
74
+ "HALTS and returns a message to relay to the user. Do NOT call it for a " +
59
75
  "branch/commit-range/uncommitted-changes review, and never run `git checkout` yourself.",
60
76
  },
61
77
  ],