@ilya-lesikov/pi-pi 0.11.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.
- package/3p/pi-subagents/src/agent-manager.ts +1 -0
- package/3p/pi-subagents/src/index.ts +1 -0
- package/3p/pi-subagents/src/types.ts +8 -0
- package/extensions/orchestrator/agents/advisor.ts +2 -1
- package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/constraints.test.ts +43 -10
- package/extensions/orchestrator/agents/constraints.ts +26 -5
- package/extensions/orchestrator/agents/deep-debugger.ts +7 -6
- package/extensions/orchestrator/agents/explore.ts +1 -1
- package/extensions/orchestrator/agents/librarian.ts +1 -1
- package/extensions/orchestrator/agents/planner.ts +1 -1
- package/extensions/orchestrator/agents/prompts.test.ts +135 -1
- package/extensions/orchestrator/agents/reviewer.ts +2 -2
- package/extensions/orchestrator/agents/task.ts +6 -2
- package/extensions/orchestrator/agents/tool-routing.ts +22 -6
- package/extensions/orchestrator/assumptions.test.ts +77 -0
- package/extensions/orchestrator/assumptions.ts +85 -0
- package/extensions/orchestrator/cbm.ts +4 -1
- package/extensions/orchestrator/cbm.which.test.ts +92 -0
- package/extensions/orchestrator/command-handlers.ts +9 -1
- package/extensions/orchestrator/config.test.ts +1 -3
- package/extensions/orchestrator/config.ts +3 -4
- package/extensions/orchestrator/context.test.ts +2 -7
- package/extensions/orchestrator/context.ts +3 -3
- package/extensions/orchestrator/doctor.test.ts +35 -0
- package/extensions/orchestrator/doctor.ts +4 -2
- package/extensions/orchestrator/event-handlers.more.test.ts +185 -0
- package/extensions/orchestrator/event-handlers.test.ts +22 -2
- package/extensions/orchestrator/event-handlers.ts +156 -18
- package/extensions/orchestrator/flant-infra.test.ts +0 -3
- package/extensions/orchestrator/flant-infra.ts +0 -1
- package/extensions/orchestrator/integration.test.ts +355 -173
- package/extensions/orchestrator/orchestrator.ts +3 -9
- package/extensions/orchestrator/phases/brainstorm.test.ts +25 -7
- package/extensions/orchestrator/phases/brainstorm.ts +35 -71
- package/extensions/orchestrator/phases/implementation.test.ts +22 -0
- package/extensions/orchestrator/phases/implementation.ts +6 -0
- package/extensions/orchestrator/phases/machine.test.ts +0 -28
- package/extensions/orchestrator/phases/machine.ts +0 -38
- package/extensions/orchestrator/phases/planning.test.ts +27 -1
- package/extensions/orchestrator/phases/planning.ts +1 -1
- package/extensions/orchestrator/phases/review-task.test.ts +7 -0
- package/extensions/orchestrator/phases/review-task.ts +1 -1
- package/extensions/orchestrator/phases/review.test.ts +47 -10
- package/extensions/orchestrator/phases/review.ts +36 -25
- package/extensions/orchestrator/pp-menu.from.test.ts +65 -0
- package/extensions/orchestrator/pp-menu.leaves.test.ts +17 -0
- package/extensions/orchestrator/pp-menu.test.ts +106 -49
- package/extensions/orchestrator/pp-menu.ts +279 -133
- package/extensions/orchestrator/pp-state-tools.ts +1 -1
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +2 -2
- package/extensions/orchestrator/rate-limit-fallback.ts +1 -1
- package/extensions/orchestrator/state.test.ts +17 -17
- package/extensions/orchestrator/state.ts +35 -12
- package/package.json +2 -2
- package/scripts/postinstall.mjs +93 -0
- package/scripts/postinstall.sh +0 -18
|
@@ -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
|
|
|
@@ -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
|
|
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
|
|
15
|
-
for (const phase of ["review"
|
|
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
|
|
27
|
-
for (const phase of ["brainstorm", "review"
|
|
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", "
|
|
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"
|
|
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
|
|
83
|
-
for (const phase of ["brainstorm"
|
|
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
|
|
102
|
-
for (const phase of ["brainstorm", "review"
|
|
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 === "
|
|
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
|
|
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 "
|
|
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: "
|
|
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
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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.
|
|
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: "
|
|
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
|
-
"
|
|
58
|
-
"
|
|
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
|
],
|