@ilya-lesikov/pi-pi 0.5.0 → 0.6.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 (76) hide show
  1. package/3p/pi-ask-user/index.ts +65 -49
  2. package/3p/pi-subagents/src/agent-manager.ts +8 -0
  3. package/3p/pi-subagents/src/agent-runner.ts +112 -19
  4. package/3p/pi-subagents/src/index.ts +3 -0
  5. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
  6. package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
  7. package/extensions/orchestrator/agents/constraints.ts +55 -0
  8. package/extensions/orchestrator/agents/explore.ts +13 -13
  9. package/extensions/orchestrator/agents/librarian.ts +12 -9
  10. package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
  11. package/extensions/orchestrator/agents/planner.ts +28 -23
  12. package/extensions/orchestrator/agents/registry.ts +2 -1
  13. package/extensions/orchestrator/agents/repo-context.ts +11 -0
  14. package/extensions/orchestrator/agents/task.ts +14 -11
  15. package/extensions/orchestrator/agents/tool-routing.ts +17 -32
  16. package/extensions/orchestrator/ast-search.ts +2 -1
  17. package/extensions/orchestrator/cbm.test.ts +35 -0
  18. package/extensions/orchestrator/cbm.ts +43 -13
  19. package/extensions/orchestrator/command-handlers.test.ts +390 -19
  20. package/extensions/orchestrator/command-handlers.ts +68 -28
  21. package/extensions/orchestrator/commands.test.ts +255 -2
  22. package/extensions/orchestrator/commands.ts +108 -10
  23. package/extensions/orchestrator/config.test.ts +289 -68
  24. package/extensions/orchestrator/config.ts +630 -121
  25. package/extensions/orchestrator/context.test.ts +177 -10
  26. package/extensions/orchestrator/context.ts +115 -14
  27. package/extensions/orchestrator/custom-footer.ts +2 -1
  28. package/extensions/orchestrator/doctor.test.ts +559 -0
  29. package/extensions/orchestrator/doctor.ts +664 -0
  30. package/extensions/orchestrator/event-handlers.test.ts +84 -22
  31. package/extensions/orchestrator/event-handlers.ts +1177 -341
  32. package/extensions/orchestrator/exa.test.ts +46 -0
  33. package/extensions/orchestrator/exa.ts +16 -10
  34. package/extensions/orchestrator/flant-infra.test.ts +224 -0
  35. package/extensions/orchestrator/flant-infra.ts +110 -41
  36. package/extensions/orchestrator/index.ts +13 -2
  37. package/extensions/orchestrator/integration.test.ts +2866 -118
  38. package/extensions/orchestrator/log.test.ts +219 -0
  39. package/extensions/orchestrator/log.ts +153 -0
  40. package/extensions/orchestrator/model-registry.test.ts +238 -0
  41. package/extensions/orchestrator/model-registry.ts +282 -0
  42. package/extensions/orchestrator/model-version.test.ts +27 -0
  43. package/extensions/orchestrator/model-version.ts +19 -0
  44. package/extensions/orchestrator/orchestrator.test.ts +206 -56
  45. package/extensions/orchestrator/orchestrator.ts +287 -140
  46. package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
  47. package/extensions/orchestrator/phases/brainstorm.ts +41 -36
  48. package/extensions/orchestrator/phases/implementation.ts +7 -11
  49. package/extensions/orchestrator/phases/machine.test.ts +27 -8
  50. package/extensions/orchestrator/phases/machine.ts +13 -0
  51. package/extensions/orchestrator/phases/planning.ts +57 -31
  52. package/extensions/orchestrator/phases/review-task.ts +3 -7
  53. package/extensions/orchestrator/phases/review.ts +38 -39
  54. package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
  55. package/extensions/orchestrator/phases/verdict.test.ts +139 -0
  56. package/extensions/orchestrator/phases/verdict.ts +82 -0
  57. package/extensions/orchestrator/plannotator.test.ts +85 -0
  58. package/extensions/orchestrator/plannotator.ts +24 -6
  59. package/extensions/orchestrator/pp-menu.test.ts +134 -0
  60. package/extensions/orchestrator/pp-menu.ts +2557 -347
  61. package/extensions/orchestrator/repo-utils.test.ts +151 -0
  62. package/extensions/orchestrator/repo-utils.ts +67 -0
  63. package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
  64. package/extensions/orchestrator/spawn-cleanup.ts +35 -0
  65. package/extensions/orchestrator/state.test.ts +76 -6
  66. package/extensions/orchestrator/state.ts +89 -26
  67. package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
  68. package/extensions/orchestrator/test-helpers.ts +217 -0
  69. package/extensions/orchestrator/tracer.test.ts +132 -0
  70. package/extensions/orchestrator/tracer.ts +127 -0
  71. package/extensions/orchestrator/transition-controller.test.ts +207 -0
  72. package/extensions/orchestrator/transition-controller.ts +259 -0
  73. package/extensions/orchestrator/usage-tracker.test.ts +435 -0
  74. package/extensions/orchestrator/usage-tracker.ts +23 -2
  75. package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
  76. package/package.json +2 -1
@@ -2,9 +2,20 @@ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { tmpdir } from "os";
5
+
6
+ const mockState = vi.hoisted(() => ({
7
+ agentDir: "",
8
+ }));
9
+
10
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
11
+ getAgentDir: () => mockState.agentDir,
12
+ }));
13
+
5
14
  import {
15
+ getContextDirs,
6
16
  getLatestSynthesizedPlan,
7
17
  getPhaseArtifacts,
18
+ loadAllContextFiles,
8
19
  loadContextFiles,
9
20
  loadBrainstormReviewOutputs,
10
21
  loadCodeReviewOutputs,
@@ -125,6 +136,95 @@ describe("loadContextFiles", () => {
125
136
  expect(loadContextFiles(cwd, "explore")).toEqual([]);
126
137
  });
127
138
 
139
+ it("supports brainstormReviewer agent type", () => {
140
+ const cwd = makeTempDir();
141
+ const contextDir = join(cwd, ".pp", "context");
142
+ mkdirSync(contextDir, { recursive: true });
143
+
144
+ writeFileSync(
145
+ join(contextDir, "brainstorm-reviewer.md"),
146
+ "---\nagents: brainstormReviewer\n---\nbrainstorm reviewer only\n",
147
+ "utf-8",
148
+ );
149
+
150
+ expect(loadContextFiles(cwd, "brainstormReviewer")).toEqual([{ mode: "context", content: "brainstorm reviewer only" }]);
151
+ expect(loadContextFiles(cwd, "codeReviewer")).toEqual([]);
152
+ });
153
+
154
+ it("parses phase/vendor/family/tier filters and matches when all filters pass", () => {
155
+ const cwd = makeTempDir();
156
+ const contextDir = join(cwd, ".pp", "context");
157
+ mkdirSync(contextDir, { recursive: true });
158
+
159
+ writeFileSync(
160
+ join(contextDir, "filtered.md"),
161
+ "---\ninject: system\nagents: ['planner']\nphases: ['plan']\nvendors: ['anthropic']\nfamilies: ['sonnet']\ntiers: ['regular']\n---\nfiltered\n",
162
+ "utf-8",
163
+ );
164
+
165
+ expect(
166
+ loadContextFiles(cwd, "planner", "system", "plan", { vendor: "anthropic", family: "sonnet", tier: "regular" }),
167
+ ).toEqual([{ mode: "system", content: "filtered" }]);
168
+ });
169
+
170
+ it("uses AND between filters and OR inside each filter", () => {
171
+ const cwd = makeTempDir();
172
+ const contextDir = join(cwd, ".pp", "context");
173
+ mkdirSync(contextDir, { recursive: true });
174
+
175
+ writeFileSync(
176
+ join(contextDir, "or-and.md"),
177
+ "---\nagents: [codeReviewer]\nphases: [implement, review]\nvendors: [openai, anthropic]\nfamilies: [gpt, gpt-mini]\ntiers: [regular, stupid]\n---\nmatched\n",
178
+ "utf-8",
179
+ );
180
+
181
+ expect(
182
+ loadContextFiles(cwd, "codeReviewer", "context", "review", { vendor: "openai", family: "gpt-mini", tier: "stupid" }),
183
+ ).toEqual([{ mode: "context", content: "matched" }]);
184
+
185
+ expect(
186
+ loadContextFiles(cwd, "codeReviewer", "context", "review", { vendor: "google", family: "gpt-mini", tier: "stupid" }),
187
+ ).toEqual([]);
188
+ expect(
189
+ loadContextFiles(cwd, "codeReviewer", "context", "debug", { vendor: "openai", family: "gpt-mini", tier: "stupid" }),
190
+ ).toEqual([]);
191
+ });
192
+
193
+ it("treats empty filter arrays as match-all", () => {
194
+ const cwd = makeTempDir();
195
+ const contextDir = join(cwd, ".pp", "context");
196
+ mkdirSync(contextDir, { recursive: true });
197
+
198
+ writeFileSync(
199
+ join(contextDir, "empty-filters.md"),
200
+ "---\nagents: [planner]\nphases: []\nvendors: []\nfamilies: []\ntiers: []\n---\nno restrictions\n",
201
+ "utf-8",
202
+ );
203
+
204
+ expect(
205
+ loadContextFiles(cwd, "planner", "context", "plan", { vendor: "unknown", family: "unknown", tier: "unknown" }),
206
+ ).toEqual([{ mode: "context", content: "no restrictions" }]);
207
+ expect(loadContextFiles(cwd, "planner", "context")).toEqual([{ mode: "context", content: "no restrictions" }]);
208
+ });
209
+
210
+ it("applies filter restrictions only when phase/model info are provided", () => {
211
+ const cwd = makeTempDir();
212
+ const contextDir = join(cwd, ".pp", "context");
213
+ mkdirSync(contextDir, { recursive: true });
214
+
215
+ writeFileSync(
216
+ join(contextDir, "requires-filters.md"),
217
+ "---\nagents: [planner]\nphases: [plan]\nvendors: [anthropic]\nfamilies: [sonnet]\ntiers: [regular]\n---\nrestricted\n",
218
+ "utf-8",
219
+ );
220
+
221
+ expect(loadContextFiles(cwd, "planner", "context")).toEqual([{ mode: "context", content: "restricted" }]);
222
+ expect(loadContextFiles(cwd, "planner", "context", "plan")).toEqual([{ mode: "context", content: "restricted" }]);
223
+ expect(
224
+ loadContextFiles(cwd, "planner", "context", "plan", { vendor: "openai", family: "gpt", tier: "regular" }),
225
+ ).toEqual([]);
226
+ });
227
+
128
228
  it("skips non-markdown files", () => {
129
229
  const cwd = makeTempDir();
130
230
  const contextDir = join(cwd, ".pp", "context");
@@ -146,13 +246,78 @@ describe("loadContextFiles", () => {
146
246
  chmodSync(brokenPath, 0o000);
147
247
  writeFileSync(join(contextDir, "good.md"), "---\nagents: main\n---\ngood file\n", "utf-8");
148
248
 
149
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
150
249
  const result = loadContextFiles(cwd, "main");
151
250
 
152
251
  expect(result).toEqual([{ mode: "context", content: "good file" }]);
153
- expect(errorSpy).toHaveBeenCalledTimes(1);
154
- expect(String(errorSpy.mock.calls[0][0])).toContain("Failed to read context file");
155
- expect(String(errorSpy.mock.calls[0][0])).toContain("broken.md");
252
+ });
253
+ });
254
+
255
+ describe("loadAllContextFiles", () => {
256
+ it("loads files from all provided directories preserving directory order", () => {
257
+ const root = makeTempDir();
258
+ const dirA = join(root, "ctx-a");
259
+ const dirB = join(root, "ctx-b");
260
+ mkdirSync(dirA, { recursive: true });
261
+ mkdirSync(dirB, { recursive: true });
262
+
263
+ writeFileSync(join(dirA, "a.md"), "---\nagents: main\n---\nfrom a\n", "utf-8");
264
+ writeFileSync(join(dirB, "b.md"), "---\nagents: main\n---\nfrom b\n", "utf-8");
265
+
266
+ expect(loadAllContextFiles([dirA, dirB], "main")).toEqual([
267
+ { mode: "context", content: "from a" },
268
+ { mode: "context", content: "from b" },
269
+ ]);
270
+ });
271
+ });
272
+
273
+ describe("getContextDirs", () => {
274
+ it("returns global, root, and extra repo context dirs in order", () => {
275
+ const root = makeTempDir();
276
+ const extra = makeTempDir();
277
+ const agentDir = makeTempDir();
278
+ mockState.agentDir = agentDir;
279
+ mkdirSync(join(root, ".pp", "context"), { recursive: true });
280
+ mkdirSync(join(extra, ".pp", "context"), { recursive: true });
281
+ mkdirSync(join(agentDir, "extensions", "pp", "context"), { recursive: true });
282
+
283
+ expect(
284
+ getContextDirs(
285
+ root,
286
+ [
287
+ { path: root, isRoot: true },
288
+ { path: extra, isRoot: false },
289
+ ],
290
+ true,
291
+ ),
292
+ ).toEqual([
293
+ join(agentDir, "extensions", "pp", "context"),
294
+ join(root, ".pp", "context"),
295
+ join(extra, ".pp", "context"),
296
+ ]);
297
+ });
298
+
299
+ it("omits extra repos when ignoreExtraRepoConfigs is true", () => {
300
+ const root = makeTempDir();
301
+ const extra = makeTempDir();
302
+ const agentDir = makeTempDir();
303
+ mockState.agentDir = agentDir;
304
+ mkdirSync(join(root, ".pp", "context"), { recursive: true });
305
+ mkdirSync(join(extra, ".pp", "context"), { recursive: true });
306
+ mkdirSync(join(agentDir, "extensions", "pp", "context"), { recursive: true });
307
+
308
+ expect(
309
+ getContextDirs(
310
+ root,
311
+ [
312
+ { path: root, isRoot: true },
313
+ { path: extra, isRoot: false },
314
+ ],
315
+ false,
316
+ ),
317
+ ).toEqual([
318
+ join(agentDir, "extensions", "pp", "context"),
319
+ join(root, ".pp", "context"),
320
+ ]);
156
321
  });
157
322
  });
158
323
 
@@ -277,17 +442,19 @@ describe("context regressions", () => {
277
442
  ]);
278
443
  });
279
444
 
280
- it("loads all plan review outputs regardless of pass", () => {
445
+ it("filters plan review outputs by pass", () => {
281
446
  const taskDir = makeTempDir();
282
447
  const planReviewsDir = join(taskDir, "plan-reviews");
283
448
  mkdirSync(planReviewsDir, { recursive: true });
284
449
 
285
- writeFileSync(join(planReviewsDir, "001_alpha.md"), "a", "utf-8");
286
- writeFileSync(join(planReviewsDir, "002_beta.md"), "b", "utf-8");
450
+ writeFileSync(join(planReviewsDir, "001_alpha_round-1.md"), "a", "utf-8");
451
+ writeFileSync(join(planReviewsDir, "002_beta_round-2.md"), "b", "utf-8");
287
452
 
288
- expect(loadPlanReviewOutputs(taskDir).map((r) => r.name)).toEqual([
289
- "001_alpha.md",
290
- "002_beta.md",
453
+ expect(loadPlanReviewOutputs(taskDir, 1).map((r) => r.name)).toEqual([
454
+ "001_alpha_round-1.md",
455
+ ]);
456
+ expect(loadPlanReviewOutputs(taskDir, 2).map((r) => r.name)).toEqual([
457
+ "002_beta_round-2.md",
291
458
  ]);
292
459
  });
293
460
  });
@@ -1,10 +1,18 @@
1
1
  import { readFileSync, existsSync, readdirSync } from "fs";
2
2
  import { join } from "path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import type { RepoInfo } from "./repo-utils.js";
3
5
  import type { Phase } from "./state.js";
6
+ import { getLogger } from "./log.js";
4
7
 
5
- type AgentType = "main" | "explore" | "librarian" | "planner" | "planReviewer" | "task" | "codeReviewer";
8
+ type AgentType = "main" | "explore" | "librarian" | "planner" | "planReviewer" | "task" | "codeReviewer" | "brainstormReviewer";
6
9
  type AgentGroup = "all" | "subagents";
7
10
  type InjectMode = "system" | "context";
11
+ type PhaseFilter = "brainstorm" | "debug" | "plan" | "implement" | "review";
12
+ type VendorFilter = "anthropic" | "openai" | "google" | "unknown";
13
+ type FamilyFilter = "opus" | "sonnet" | "haiku" | "gpt" | "gpt-mini" | "gemini-pro" | "gemini-flash" | "unknown";
14
+ type TierFilter = "stupid" | "regular" | "smart" | "xsmart" | "unknown";
15
+ type ModelInfo = { vendor: string; family: string; tier: string };
8
16
 
9
17
  interface ContextFile {
10
18
  mode: InjectMode;
@@ -15,17 +23,33 @@ interface Frontmatter {
15
23
  inject: InjectMode;
16
24
  agents: AgentType[];
17
25
  agentGroups: AgentGroup[];
26
+ phases: PhaseFilter[];
27
+ vendors: VendorFilter[];
28
+ families: FamilyFilter[];
29
+ tiers: TierFilter[];
18
30
  }
19
31
 
20
32
  const VALID_INJECT_MODES: readonly string[] = ["system", "context"];
21
- const VALID_AGENTS: readonly string[] = ["main", "explore", "librarian", "planner", "planReviewer", "task", "codeReviewer"];
33
+ const VALID_AGENTS: readonly string[] = ["main", "explore", "librarian", "planner", "planReviewer", "task", "codeReviewer", "brainstormReviewer"];
22
34
  const VALID_AGENT_GROUPS: readonly string[] = ["all", "subagents"];
35
+ const VALID_PHASES: readonly string[] = ["brainstorm", "debug", "plan", "implement", "review"];
36
+ const VALID_VENDORS: readonly string[] = ["anthropic", "openai", "google", "unknown"];
37
+ const VALID_FAMILIES: readonly string[] = ["opus", "sonnet", "haiku", "gpt", "gpt-mini", "gemini-pro", "gemini-flash", "unknown"];
38
+ const VALID_TIERS: readonly string[] = ["stupid", "regular", "smart", "xsmart", "unknown"];
23
39
 
24
40
  function parseFrontmatter(raw: string): { frontmatter: Frontmatter; body: string } {
25
41
  const match = raw.match(/^---[^\S\n]*\n([\s\S]*?)\n---[^\S\n]*\n([\s\S]*)$/);
26
42
  if (!match) {
27
43
  return {
28
- frontmatter: { inject: "context", agents: ["main"], agentGroups: [] },
44
+ frontmatter: {
45
+ inject: "context",
46
+ agents: ["main"],
47
+ agentGroups: [],
48
+ phases: [],
49
+ vendors: [],
50
+ families: [],
51
+ tiers: [],
52
+ },
29
53
  body: raw,
30
54
  };
31
55
  }
@@ -36,6 +60,10 @@ function parseFrontmatter(raw: string): { frontmatter: Frontmatter; body: string
36
60
  let inject: InjectMode = "context";
37
61
  let agents: AgentType[] = [];
38
62
  let agentGroups: AgentGroup[] = [];
63
+ let phases: PhaseFilter[] = [];
64
+ let vendors: VendorFilter[] = [];
65
+ let families: FamilyFilter[] = [];
66
+ let tiers: TierFilter[] = [];
39
67
 
40
68
  for (const line of yamlBlock.split("\n")) {
41
69
  const trimmed = line.trim();
@@ -56,6 +84,14 @@ function parseFrontmatter(raw: string): { frontmatter: Frontmatter; body: string
56
84
  agents = parseArray(val).filter((v): v is AgentType => VALID_AGENTS.includes(v));
57
85
  } else if (key === "agentGroups") {
58
86
  agentGroups = parseArray(val).filter((v): v is AgentGroup => VALID_AGENT_GROUPS.includes(v));
87
+ } else if (key === "phases") {
88
+ phases = parseArray(val).filter((v): v is PhaseFilter => VALID_PHASES.includes(v));
89
+ } else if (key === "vendors") {
90
+ vendors = parseArray(val).filter((v): v is VendorFilter => VALID_VENDORS.includes(v));
91
+ } else if (key === "families") {
92
+ families = parseArray(val).filter((v): v is FamilyFilter => VALID_FAMILIES.includes(v));
93
+ } else if (key === "tiers") {
94
+ tiers = parseArray(val).filter((v): v is TierFilter => VALID_TIERS.includes(v));
59
95
  }
60
96
  }
61
97
 
@@ -63,7 +99,10 @@ function parseFrontmatter(raw: string): { frontmatter: Frontmatter; body: string
63
99
  agents = ["main"];
64
100
  }
65
101
 
66
- return { frontmatter: { inject, agents, agentGroups }, body };
102
+ return {
103
+ frontmatter: { inject, agents, agentGroups, phases, vendors, families, tiers },
104
+ body,
105
+ };
67
106
  }
68
107
 
69
108
  function stripQuotes(val: string): string {
@@ -99,8 +138,39 @@ function matchesAgent(fm: Frontmatter, agentType: AgentType): boolean {
99
138
  return fm.agents.includes(agentType);
100
139
  }
101
140
 
102
- export function loadContextFiles(cwd: string, agentType: AgentType, injectMode?: InjectMode): ContextFile[] {
103
- const contextDir = join(cwd, ".pp", "context");
141
+ function matchesFilters(
142
+ fm: Frontmatter,
143
+ agentType: AgentType,
144
+ phase?: string,
145
+ modelInfo?: ModelInfo,
146
+ ): boolean {
147
+ if (!matchesAgent(fm, agentType)) return false;
148
+ if (fm.phases.length > 0 && phase && !fm.phases.includes(phase as PhaseFilter)) return false;
149
+ if (modelInfo) {
150
+ if (fm.vendors.length > 0 && !fm.vendors.includes(modelInfo.vendor as VendorFilter)) return false;
151
+ if (fm.families.length > 0 && !fm.families.includes(modelInfo.family as FamilyFilter)) return false;
152
+ if (fm.tiers.length > 0 && !fm.tiers.includes(modelInfo.tier as TierFilter)) return false;
153
+ }
154
+ return true;
155
+ }
156
+
157
+ export function loadContextFiles(
158
+ cwd: string,
159
+ agentType: AgentType,
160
+ injectMode?: InjectMode,
161
+ phase?: string,
162
+ modelInfo?: ModelInfo,
163
+ ): ContextFile[] {
164
+ return loadContextFilesFromDir(join(cwd, ".pp", "context"), agentType, injectMode, phase, modelInfo);
165
+ }
166
+
167
+ export function loadContextFilesFromDir(
168
+ contextDir: string,
169
+ agentType: AgentType,
170
+ injectMode?: InjectMode,
171
+ phase?: string,
172
+ modelInfo?: ModelInfo,
173
+ ): ContextFile[] {
104
174
  if (!existsSync(contextDir)) return [];
105
175
 
106
176
  const results: ContextFile[] = [];
@@ -111,12 +181,12 @@ export function loadContextFiles(cwd: string, agentType: AgentType, injectMode?:
111
181
  try {
112
182
  raw = readFileSync(filePath, "utf-8");
113
183
  } catch (err: any) {
114
- console.error(`[pi-pi] Failed to read context file ${filePath}: ${err.message}`);
184
+ getLogger().warn({ s: "context", filePath, err: err.message }, "failed to read context file");
115
185
  continue;
116
186
  }
117
187
  const { frontmatter, body } = parseFrontmatter(raw);
118
188
 
119
- if (!matchesAgent(frontmatter, agentType)) continue;
189
+ if (!matchesFilters(frontmatter, agentType, phase, modelInfo)) continue;
120
190
  if (injectMode && frontmatter.inject !== injectMode) continue;
121
191
 
122
192
  results.push({ mode: frontmatter.inject, content: body.trim() });
@@ -125,10 +195,41 @@ export function loadContextFiles(cwd: string, agentType: AgentType, injectMode?:
125
195
  return results;
126
196
  }
127
197
 
128
- export function loadAgentsMd(cwd: string): string | null {
129
- const agentsPath = join(cwd, "AGENTS.md");
130
- if (!existsSync(agentsPath)) return null;
131
- return readFileSync(agentsPath, "utf-8");
198
+ export function loadAllContextFiles(
199
+ contextDirs: string[],
200
+ agentType: AgentType,
201
+ injectMode?: InjectMode,
202
+ phase?: string,
203
+ modelInfo?: ModelInfo,
204
+ ): ContextFile[] {
205
+ const results: ContextFile[] = [];
206
+ for (const contextDir of contextDirs) {
207
+ results.push(...loadContextFilesFromDir(contextDir, agentType, injectMode, phase, modelInfo));
208
+ }
209
+ getLogger().debug({ s: "context", agentType, injectMode, phase, dirs: contextDirs.length, files: results.length }, "loaded context files");
210
+ return results;
211
+ }
212
+
213
+ export function getContextDirs(rootCwd: string, repos: RepoInfo[], loadExtraRepoConfigs: boolean): string[] {
214
+ const dirs: string[] = [];
215
+ const seen = new Set<string>();
216
+ const add = (dir: string) => {
217
+ if (!existsSync(dir) || seen.has(dir)) return;
218
+ seen.add(dir);
219
+ dirs.push(dir);
220
+ };
221
+
222
+ add(join(getAgentDir(), "extensions", "pp", "context"));
223
+ add(join(rootCwd, ".pp", "context"));
224
+
225
+ if (loadExtraRepoConfigs) {
226
+ for (const repo of repos) {
227
+ if (repo.isRoot) continue;
228
+ add(join(repo.path, ".pp", "context"));
229
+ }
230
+ }
231
+
232
+ return dirs;
132
233
  }
133
234
 
134
235
  export function getPhaseArtifacts(taskDir: string, phase: Phase): { name: string; content: string }[] {
@@ -196,11 +297,11 @@ export function loadCodeReviewOutputs(taskDir: string, pass: number): { name: st
196
297
  .map((name) => ({ name, content: readFileSync(join(dir, name), "utf-8") }));
197
298
  }
198
299
 
199
- export function loadPlanReviewOutputs(taskDir: string): { name: string; content: string }[] {
300
+ export function loadPlanReviewOutputs(taskDir: string, pass: number): { name: string; content: string }[] {
200
301
  const dir = join(taskDir, "plan-reviews");
201
302
  if (!existsSync(dir)) return [];
202
303
  return readdirSync(dir)
203
- .filter((f) => f.endsWith(".md"))
304
+ .filter((f) => f.includes(`round-${pass}`) && f.endsWith(".md"))
204
305
  .sort()
205
306
  .map((name) => ({ name, content: readFileSync(join(dir, name), "utf-8") }));
206
307
  }
@@ -63,8 +63,9 @@ function renderStatsLine(width: number, theme: Theme): string {
63
63
  const cacheRate = tracker?.getCacheHitRate() ?? 0;
64
64
  const totalCost = tracker?.getTotalCost() ?? 0;
65
65
 
66
+ const cacheSupported = tracker?.isCacheSupported() ?? false;
66
67
  const leftParts: string[] = [`↑${formatTokens(inputTokens)}`, `↓${formatTokens(outputTokens)}`];
67
- if (cacheRate > 0) leftParts.push(`⚡${Math.round(cacheRate * 100)}%`);
68
+ if (cacheSupported) leftParts.push(`⚡${Math.round(cacheRate * 100)}%`);
68
69
  if (totalCost > 0) leftParts.push(`$${totalCost.toFixed(2)}`);
69
70
  leftParts.push(toContextUsagePart(ctx, theme));
70
71
  let left = leftParts.join(" ");