@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.
- package/3p/pi-ask-user/index.ts +65 -49
- package/3p/pi-subagents/src/agent-manager.ts +8 -0
- package/3p/pi-subagents/src/agent-runner.ts +112 -19
- package/3p/pi-subagents/src/index.ts +3 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
- package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
- package/extensions/orchestrator/agents/constraints.ts +55 -0
- package/extensions/orchestrator/agents/explore.ts +13 -13
- package/extensions/orchestrator/agents/librarian.ts +12 -9
- package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
- package/extensions/orchestrator/agents/planner.ts +28 -23
- package/extensions/orchestrator/agents/registry.ts +2 -1
- package/extensions/orchestrator/agents/repo-context.ts +11 -0
- package/extensions/orchestrator/agents/task.ts +14 -11
- package/extensions/orchestrator/agents/tool-routing.ts +17 -32
- package/extensions/orchestrator/ast-search.ts +2 -1
- package/extensions/orchestrator/cbm.test.ts +35 -0
- package/extensions/orchestrator/cbm.ts +43 -13
- package/extensions/orchestrator/command-handlers.test.ts +390 -19
- package/extensions/orchestrator/command-handlers.ts +68 -28
- package/extensions/orchestrator/commands.test.ts +255 -2
- package/extensions/orchestrator/commands.ts +108 -10
- package/extensions/orchestrator/config.test.ts +289 -68
- package/extensions/orchestrator/config.ts +630 -121
- package/extensions/orchestrator/context.test.ts +177 -10
- package/extensions/orchestrator/context.ts +115 -14
- package/extensions/orchestrator/custom-footer.ts +2 -1
- package/extensions/orchestrator/doctor.test.ts +559 -0
- package/extensions/orchestrator/doctor.ts +664 -0
- package/extensions/orchestrator/event-handlers.test.ts +84 -22
- package/extensions/orchestrator/event-handlers.ts +1177 -341
- package/extensions/orchestrator/exa.test.ts +46 -0
- package/extensions/orchestrator/exa.ts +16 -10
- package/extensions/orchestrator/flant-infra.test.ts +224 -0
- package/extensions/orchestrator/flant-infra.ts +110 -41
- package/extensions/orchestrator/index.ts +13 -2
- package/extensions/orchestrator/integration.test.ts +2866 -118
- package/extensions/orchestrator/log.test.ts +219 -0
- package/extensions/orchestrator/log.ts +153 -0
- package/extensions/orchestrator/model-registry.test.ts +238 -0
- package/extensions/orchestrator/model-registry.ts +282 -0
- package/extensions/orchestrator/model-version.test.ts +27 -0
- package/extensions/orchestrator/model-version.ts +19 -0
- package/extensions/orchestrator/orchestrator.test.ts +206 -56
- package/extensions/orchestrator/orchestrator.ts +287 -140
- package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
- package/extensions/orchestrator/phases/brainstorm.ts +41 -36
- package/extensions/orchestrator/phases/implementation.ts +7 -11
- package/extensions/orchestrator/phases/machine.test.ts +27 -8
- package/extensions/orchestrator/phases/machine.ts +13 -0
- package/extensions/orchestrator/phases/planning.ts +57 -31
- package/extensions/orchestrator/phases/review-task.ts +3 -7
- package/extensions/orchestrator/phases/review.ts +38 -39
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
- package/extensions/orchestrator/phases/verdict.test.ts +139 -0
- package/extensions/orchestrator/phases/verdict.ts +82 -0
- package/extensions/orchestrator/plannotator.test.ts +85 -0
- package/extensions/orchestrator/plannotator.ts +24 -6
- package/extensions/orchestrator/pp-menu.test.ts +134 -0
- package/extensions/orchestrator/pp-menu.ts +2557 -347
- package/extensions/orchestrator/repo-utils.test.ts +151 -0
- package/extensions/orchestrator/repo-utils.ts +67 -0
- package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
- package/extensions/orchestrator/spawn-cleanup.ts +35 -0
- package/extensions/orchestrator/state.test.ts +76 -6
- package/extensions/orchestrator/state.ts +89 -26
- package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
- package/extensions/orchestrator/test-helpers.ts +217 -0
- package/extensions/orchestrator/tracer.test.ts +132 -0
- package/extensions/orchestrator/tracer.ts +127 -0
- package/extensions/orchestrator/transition-controller.test.ts +207 -0
- package/extensions/orchestrator/transition-controller.ts +259 -0
- package/extensions/orchestrator/usage-tracker.test.ts +435 -0
- package/extensions/orchestrator/usage-tracker.ts +23 -2
- package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
- 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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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("
|
|
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, "
|
|
286
|
-
writeFileSync(join(planReviewsDir, "
|
|
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
|
-
"
|
|
290
|
-
|
|
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: {
|
|
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 {
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 (
|
|
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(" ");
|