@ilya-lesikov/pi-pi 0.1.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 +1835 -0
- package/3p/pi-ask-user/package.json +50 -0
- package/3p/pi-ask-user/single-select-layout.ts +203 -0
- package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
- package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
- package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
- package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
- package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
- package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
- package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
- package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
- package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
- package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
- package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
- package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
- package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
- package/3p/pi-lsp/package.json +54 -0
- package/3p/pi-plannotator/AGENTS.md +583 -0
- package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
- package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
- package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
- package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
- package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
- package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
- package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
- package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
- package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
- package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
- package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
- package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
- package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
- package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
- package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
- package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
- package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
- package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
- package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
- package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
- package/3p/pi-plannotator/package.json +53 -0
- package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
- package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
- package/3p/pi-plannotator/packages/ai/context.ts +250 -0
- package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
- package/3p/pi-plannotator/packages/ai/index.ts +105 -0
- package/3p/pi-plannotator/packages/ai/package.json +21 -0
- package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
- package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
- package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
- package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
- package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
- package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
- package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/ai/types.ts +379 -0
- package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
- package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
- package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
- package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
- package/3p/pi-plannotator/packages/server/browser.ts +245 -0
- package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
- package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
- package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
- package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
- package/3p/pi-plannotator/packages/server/config.ts +8 -0
- package/3p/pi-plannotator/packages/server/draft.ts +1 -0
- package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
- package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
- package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
- package/3p/pi-plannotator/packages/server/git.ts +141 -0
- package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
- package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
- package/3p/pi-plannotator/packages/server/ide.ts +43 -0
- package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
- package/3p/pi-plannotator/packages/server/image.ts +66 -0
- package/3p/pi-plannotator/packages/server/index.ts +648 -0
- package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
- package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
- package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
- package/3p/pi-plannotator/packages/server/jj.ts +86 -0
- package/3p/pi-plannotator/packages/server/p4.ts +417 -0
- package/3p/pi-plannotator/packages/server/package.json +40 -0
- package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
- package/3p/pi-plannotator/packages/server/pr.ts +146 -0
- package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
- package/3p/pi-plannotator/packages/server/project.ts +45 -0
- package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
- package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
- package/3p/pi-plannotator/packages/server/remote.ts +74 -0
- package/3p/pi-plannotator/packages/server/repo.ts +80 -0
- package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
- package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
- package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
- package/3p/pi-plannotator/packages/server/review.ts +1318 -0
- package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
- package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
- package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
- package/3p/pi-plannotator/packages/server/storage.ts +17 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
- package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
- package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
- package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
- package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
- package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
- package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
- package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
- package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
- package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
- package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
- package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
- package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
- package/3p/pi-plannotator/packages/shared/config.ts +262 -0
- package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
- package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
- package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
- package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
- package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
- package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
- package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
- package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
- package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
- package/3p/pi-plannotator/packages/shared/package.json +55 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
- package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
- package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
- package/3p/pi-plannotator/packages/shared/project.ts +71 -0
- package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
- package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
- package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
- package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
- package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
- package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
- package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
- package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
- package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
- package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
- package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
- package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
- package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
- package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/shared/types.ts +29 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
- package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
- package/3p/pi-subagents/package.json +50 -0
- package/3p/pi-subagents/src/agent-manager.ts +413 -0
- package/3p/pi-subagents/src/agent-runner.ts +502 -0
- package/3p/pi-subagents/src/agent-types.ts +248 -0
- package/3p/pi-subagents/src/context.ts +58 -0
- package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
- package/3p/pi-subagents/src/custom-agents.ts +137 -0
- package/3p/pi-subagents/src/default-agents.ts +144 -0
- package/3p/pi-subagents/src/env.ts +33 -0
- package/3p/pi-subagents/src/group-join.ts +141 -0
- package/3p/pi-subagents/src/index.ts +1811 -0
- package/3p/pi-subagents/src/invocation-config.ts +40 -0
- package/3p/pi-subagents/src/memory.ts +165 -0
- package/3p/pi-subagents/src/model-resolver.ts +81 -0
- package/3p/pi-subagents/src/output-file.ts +77 -0
- package/3p/pi-subagents/src/prompts.ts +85 -0
- package/3p/pi-subagents/src/skill-loader.ts +79 -0
- package/3p/pi-subagents/src/types.ts +111 -0
- package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
- package/3p/pi-subagents/src/worktree.ts +162 -0
- package/3p/pi-tasks/package.json +52 -0
- package/3p/pi-tasks/src/auto-clear.ts +91 -0
- package/3p/pi-tasks/src/index.ts +1150 -0
- package/3p/pi-tasks/src/process-tracker.ts +140 -0
- package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
- package/3p/pi-tasks/src/task-store.ts +324 -0
- package/3p/pi-tasks/src/tasks-config.ts +27 -0
- package/3p/pi-tasks/src/types.ts +40 -0
- package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
- package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
- package/AGENTS.md +28 -0
- package/LICENSE +201 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
- package/extensions/orchestrator/agents/explore.ts +33 -0
- package/extensions/orchestrator/agents/librarian.ts +43 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
- package/extensions/orchestrator/agents/planner.ts +61 -0
- package/extensions/orchestrator/agents/registry.ts +156 -0
- package/extensions/orchestrator/agents/task.ts +50 -0
- package/extensions/orchestrator/agents/tool-routing.ts +84 -0
- package/extensions/orchestrator/ast-search.ts +85 -0
- package/extensions/orchestrator/cbm.ts +330 -0
- package/extensions/orchestrator/command-handlers.test.ts +163 -0
- package/extensions/orchestrator/command-handlers.ts +116 -0
- package/extensions/orchestrator/commands.test.ts +81 -0
- package/extensions/orchestrator/commands.ts +75 -0
- package/extensions/orchestrator/config.test.ts +221 -0
- package/extensions/orchestrator/config.ts +230 -0
- package/extensions/orchestrator/context.test.ts +293 -0
- package/extensions/orchestrator/context.ts +206 -0
- package/extensions/orchestrator/event-handlers.test.ts +190 -0
- package/extensions/orchestrator/event-handlers.ts +1416 -0
- package/extensions/orchestrator/exa.ts +104 -0
- package/extensions/orchestrator/flant-infra.ts +486 -0
- package/extensions/orchestrator/index.ts +80 -0
- package/extensions/orchestrator/integration.test.ts +1214 -0
- package/extensions/orchestrator/orchestrator.test.ts +252 -0
- package/extensions/orchestrator/orchestrator.ts +538 -0
- package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
- package/extensions/orchestrator/phases/brainstorm.ts +273 -0
- package/extensions/orchestrator/phases/implementation.ts +40 -0
- package/extensions/orchestrator/phases/machine.test.ts +293 -0
- package/extensions/orchestrator/phases/machine.ts +209 -0
- package/extensions/orchestrator/phases/planning.ts +255 -0
- package/extensions/orchestrator/phases/review.ts +193 -0
- package/extensions/orchestrator/plannotator.ts +56 -0
- package/extensions/orchestrator/pp-menu.ts +866 -0
- package/extensions/orchestrator/state.test.ts +343 -0
- package/extensions/orchestrator/state.ts +237 -0
- package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
- package/extensions/orchestrator/validate-artifacts.ts +272 -0
- package/extensions/orchestrator/vendor.d.ts +26 -0
- package/package.json +73 -0
- package/scripts/postinstall.sh +18 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import {
|
|
6
|
+
getLatestSynthesizedPlan,
|
|
7
|
+
getPhaseArtifacts,
|
|
8
|
+
loadContextFiles,
|
|
9
|
+
loadBrainstormReviewOutputs,
|
|
10
|
+
loadCodeReviewOutputs,
|
|
11
|
+
loadPlanReviewOutputs,
|
|
12
|
+
} from "./context.js";
|
|
13
|
+
|
|
14
|
+
const tempDirs: string[] = [];
|
|
15
|
+
|
|
16
|
+
function makeTempDir(): string {
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-pi-context-test-"));
|
|
18
|
+
tempDirs.push(dir);
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
for (const dir of tempDirs.splice(0)) {
|
|
25
|
+
rmSync(dir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("loadContextFiles", () => {
|
|
30
|
+
it("returns empty array when context directory is missing", () => {
|
|
31
|
+
const cwd = makeTempDir();
|
|
32
|
+
expect(loadContextFiles(cwd, "main")).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("defaults files without frontmatter to context mode and main agent", () => {
|
|
36
|
+
const cwd = makeTempDir();
|
|
37
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
38
|
+
mkdirSync(contextDir, { recursive: true });
|
|
39
|
+
writeFileSync(join(contextDir, "plain.md"), "hello world\n", "utf-8");
|
|
40
|
+
|
|
41
|
+
expect(loadContextFiles(cwd, "main")).toEqual([{ mode: "context", content: "hello world" }]);
|
|
42
|
+
expect(loadContextFiles(cwd, "explore")).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("filters by inject mode", () => {
|
|
46
|
+
const cwd = makeTempDir();
|
|
47
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
48
|
+
mkdirSync(contextDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(contextDir, "system.md"),
|
|
52
|
+
"---\ninject: system\nagents: main\n---\nSystem text\n",
|
|
53
|
+
"utf-8",
|
|
54
|
+
);
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(contextDir, "context.md"),
|
|
57
|
+
"---\ninject: context\nagents: main\n---\nContext text\n",
|
|
58
|
+
"utf-8",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(loadContextFiles(cwd, "main", "system")).toEqual([{ mode: "system", content: "System text" }]);
|
|
62
|
+
expect(loadContextFiles(cwd, "main", "context")).toEqual([{ mode: "context", content: "Context text" }]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("supports specific agents, agent groups, and main default", () => {
|
|
66
|
+
const cwd = makeTempDir();
|
|
67
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
68
|
+
mkdirSync(contextDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
writeFileSync(join(contextDir, "all.md"), "---\nagentGroups: all\n---\nall agents\n", "utf-8");
|
|
71
|
+
writeFileSync(join(contextDir, "sub.md"), "---\nagentGroups: subagents\n---\nsub only\n", "utf-8");
|
|
72
|
+
writeFileSync(join(contextDir, "explore.md"), "---\nagents: explore\n---\nexplore only\n", "utf-8");
|
|
73
|
+
writeFileSync(join(contextDir, "default.md"), "---\n\n---\ndefault main\n", "utf-8");
|
|
74
|
+
|
|
75
|
+
expect(loadContextFiles(cwd, "main").map((f) => f.content).sort()).toEqual(["all agents", "default main"]);
|
|
76
|
+
expect(loadContextFiles(cwd, "explore").map((f) => f.content).sort()).toEqual([
|
|
77
|
+
"all agents",
|
|
78
|
+
"explore only",
|
|
79
|
+
"sub only",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("handles quoted frontmatter values and bracket array syntax", () => {
|
|
84
|
+
const cwd = makeTempDir();
|
|
85
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
86
|
+
mkdirSync(contextDir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
writeFileSync(
|
|
89
|
+
join(contextDir, "quoted.md"),
|
|
90
|
+
"---\ninject: \"system\"\nagents: ['main', \"explore\"]\n---\nquoted\n",
|
|
91
|
+
"utf-8",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(loadContextFiles(cwd, "main", "system")).toEqual([{ mode: "system", content: "quoted" }]);
|
|
95
|
+
expect(loadContextFiles(cwd, "explore", "system")).toEqual([{ mode: "system", content: "quoted" }]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("ignores invalid inject mode and keeps default context mode", () => {
|
|
99
|
+
const cwd = makeTempDir();
|
|
100
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
101
|
+
mkdirSync(contextDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
writeFileSync(
|
|
104
|
+
join(contextDir, "bad-inject.md"),
|
|
105
|
+
"---\ninject: invalid\nagents: main\n---\nuses default inject\n",
|
|
106
|
+
"utf-8",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(loadContextFiles(cwd, "main", "system")).toEqual([]);
|
|
110
|
+
expect(loadContextFiles(cwd, "main", "context")).toEqual([{ mode: "context", content: "uses default inject" }]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("filters invalid agent names", () => {
|
|
114
|
+
const cwd = makeTempDir();
|
|
115
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
116
|
+
mkdirSync(contextDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
writeFileSync(
|
|
119
|
+
join(contextDir, "mixed-agents.md"),
|
|
120
|
+
"---\nagents: [main, unknown, fake]\n---\nonly main allowed\n",
|
|
121
|
+
"utf-8",
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(loadContextFiles(cwd, "main")).toEqual([{ mode: "context", content: "only main allowed" }]);
|
|
125
|
+
expect(loadContextFiles(cwd, "explore")).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("skips non-markdown files", () => {
|
|
129
|
+
const cwd = makeTempDir();
|
|
130
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
131
|
+
mkdirSync(contextDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
writeFileSync(join(contextDir, "notes.txt"), "---\nagents: main\n---\nshould skip\n", "utf-8");
|
|
134
|
+
writeFileSync(join(contextDir, "ok.md"), "---\nagents: main\n---\nshould include\n", "utf-8");
|
|
135
|
+
|
|
136
|
+
expect(loadContextFiles(cwd, "main")).toEqual([{ mode: "context", content: "should include" }]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("logs and skips files that fail to read", () => {
|
|
140
|
+
const cwd = makeTempDir();
|
|
141
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
142
|
+
mkdirSync(contextDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
const brokenPath = join(contextDir, "broken.md");
|
|
145
|
+
writeFileSync(brokenPath, "broken", "utf-8");
|
|
146
|
+
chmodSync(brokenPath, 0o000);
|
|
147
|
+
writeFileSync(join(contextDir, "good.md"), "---\nagents: main\n---\ngood file\n", "utf-8");
|
|
148
|
+
|
|
149
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
150
|
+
const result = loadContextFiles(cwd, "main");
|
|
151
|
+
|
|
152
|
+
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");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("getPhaseArtifacts", () => {
|
|
160
|
+
it("returns user request and research artifacts for plan", () => {
|
|
161
|
+
const taskDir = makeTempDir();
|
|
162
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), "user request", "utf-8");
|
|
163
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), "research notes", "utf-8");
|
|
164
|
+
|
|
165
|
+
expect(getPhaseArtifacts(taskDir, "plan")).toEqual([
|
|
166
|
+
{ name: "USER_REQUEST.md", content: "user request" },
|
|
167
|
+
{ name: "RESEARCH.md", content: "research notes" },
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("includes latest synthesized plan for plan and implement", () => {
|
|
172
|
+
const taskDir = makeTempDir();
|
|
173
|
+
const plansDir = join(taskDir, "plans");
|
|
174
|
+
mkdirSync(plansDir, { recursive: true });
|
|
175
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), "request", "utf-8");
|
|
176
|
+
writeFileSync(join(plansDir, "20260101_synthesized.md"), "old plan", "utf-8");
|
|
177
|
+
writeFileSync(join(plansDir, "20260102_synthesized.md"), "new plan", "utf-8");
|
|
178
|
+
|
|
179
|
+
expect(getPhaseArtifacts(taskDir, "implement")).toEqual([
|
|
180
|
+
{ name: "USER_REQUEST.md", content: "request" },
|
|
181
|
+
{ name: "Synthesized Plan", content: "new plan" },
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
expect(getPhaseArtifacts(taskDir, "plan")).toEqual([
|
|
185
|
+
{ name: "USER_REQUEST.md", content: "request" },
|
|
186
|
+
{ name: "Synthesized Plan", content: "new plan" },
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("getLatestSynthesizedPlan", () => {
|
|
192
|
+
it("returns null when plans directory is missing or has no synthesized files", () => {
|
|
193
|
+
const taskDir = makeTempDir();
|
|
194
|
+
expect(getLatestSynthesizedPlan(taskDir)).toBeNull();
|
|
195
|
+
|
|
196
|
+
const plansDir = join(taskDir, "plans");
|
|
197
|
+
mkdirSync(plansDir, { recursive: true });
|
|
198
|
+
writeFileSync(join(plansDir, "other.md"), "not synthesized", "utf-8");
|
|
199
|
+
|
|
200
|
+
expect(getLatestSynthesizedPlan(taskDir)).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("returns content of lexicographically latest synthesized file", () => {
|
|
204
|
+
const taskDir = makeTempDir();
|
|
205
|
+
const plansDir = join(taskDir, "plans");
|
|
206
|
+
mkdirSync(plansDir, { recursive: true });
|
|
207
|
+
|
|
208
|
+
writeFileSync(join(plansDir, "001_synthesized.md"), "first", "utf-8");
|
|
209
|
+
writeFileSync(join(plansDir, "010_synthesized.md"), "latest", "utf-8");
|
|
210
|
+
|
|
211
|
+
expect(getLatestSynthesizedPlan(taskDir)).toBe("latest");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("context regressions", () => {
|
|
216
|
+
it("parses bracket array values without truncation when closing bracket is missing", () => {
|
|
217
|
+
const cwd = makeTempDir();
|
|
218
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
219
|
+
mkdirSync(contextDir, { recursive: true });
|
|
220
|
+
|
|
221
|
+
writeFileSync(
|
|
222
|
+
join(contextDir, "broken-array.md"),
|
|
223
|
+
"---\nagents: [main, explore\n---\nregression body\n",
|
|
224
|
+
"utf-8",
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(loadContextFiles(cwd, "main")).toEqual([{ mode: "context", content: "regression body" }]);
|
|
228
|
+
expect(loadContextFiles(cwd, "explore")).toEqual([{ mode: "context", content: "regression body" }]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("selects numerically latest synthesized plan in getLatestSynthesizedPlan", () => {
|
|
232
|
+
const taskDir = makeTempDir();
|
|
233
|
+
const plansDir = join(taskDir, "plans");
|
|
234
|
+
mkdirSync(plansDir, { recursive: true });
|
|
235
|
+
|
|
236
|
+
writeFileSync(join(plansDir, "999_synthesized.md"), "old", "utf-8");
|
|
237
|
+
writeFileSync(join(plansDir, "1000_synthesized.md"), "new", "utf-8");
|
|
238
|
+
|
|
239
|
+
expect(getLatestSynthesizedPlan(taskDir)).toBe("new");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("selects numerically latest synthesized plan in getPhaseArtifacts implement phase", () => {
|
|
243
|
+
const taskDir = makeTempDir();
|
|
244
|
+
const plansDir = join(taskDir, "plans");
|
|
245
|
+
mkdirSync(plansDir, { recursive: true });
|
|
246
|
+
|
|
247
|
+
writeFileSync(join(plansDir, "999_synthesized.md"), "old", "utf-8");
|
|
248
|
+
writeFileSync(join(plansDir, "1000_synthesized.md"), "new", "utf-8");
|
|
249
|
+
|
|
250
|
+
expect(getPhaseArtifacts(taskDir, "implement")).toEqual([{ name: "Synthesized Plan", content: "new" }]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("filters brainstorm and code review outputs by pass", () => {
|
|
254
|
+
const taskDir = makeTempDir();
|
|
255
|
+
const brainstormReviewsDir = join(taskDir, "brainstorm-reviews");
|
|
256
|
+
const codeReviewsDir = join(taskDir, "code-reviews");
|
|
257
|
+
mkdirSync(brainstormReviewsDir, { recursive: true });
|
|
258
|
+
mkdirSync(codeReviewsDir, { recursive: true });
|
|
259
|
+
|
|
260
|
+
writeFileSync(join(brainstormReviewsDir, "001_alpha_round-1.md"), "brainstorm", "utf-8");
|
|
261
|
+
writeFileSync(join(brainstormReviewsDir, "003_alpha_round-2.md"), "round2", "utf-8");
|
|
262
|
+
writeFileSync(join(codeReviewsDir, "002_alpha_round-1.md"), "implement", "utf-8");
|
|
263
|
+
writeFileSync(join(codeReviewsDir, "004_alpha_round-2.md"), "round2", "utf-8");
|
|
264
|
+
|
|
265
|
+
expect(loadBrainstormReviewOutputs(taskDir, 1).map((r) => r.name)).toEqual([
|
|
266
|
+
"001_alpha_round-1.md",
|
|
267
|
+
]);
|
|
268
|
+
expect(loadCodeReviewOutputs(taskDir, 1).map((r) => r.name)).toEqual([
|
|
269
|
+
"002_alpha_round-1.md",
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
expect(loadBrainstormReviewOutputs(taskDir, 2).map((r) => r.name)).toEqual([
|
|
273
|
+
"003_alpha_round-2.md",
|
|
274
|
+
]);
|
|
275
|
+
expect(loadCodeReviewOutputs(taskDir, 2).map((r) => r.name)).toEqual([
|
|
276
|
+
"004_alpha_round-2.md",
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("loads all plan review outputs regardless of pass", () => {
|
|
281
|
+
const taskDir = makeTempDir();
|
|
282
|
+
const planReviewsDir = join(taskDir, "plan-reviews");
|
|
283
|
+
mkdirSync(planReviewsDir, { recursive: true });
|
|
284
|
+
|
|
285
|
+
writeFileSync(join(planReviewsDir, "001_alpha.md"), "a", "utf-8");
|
|
286
|
+
writeFileSync(join(planReviewsDir, "002_beta.md"), "b", "utf-8");
|
|
287
|
+
|
|
288
|
+
expect(loadPlanReviewOutputs(taskDir).map((r) => r.name)).toEqual([
|
|
289
|
+
"001_alpha.md",
|
|
290
|
+
"002_beta.md",
|
|
291
|
+
]);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { Phase } from "./state.js";
|
|
4
|
+
|
|
5
|
+
type AgentType = "main" | "explore" | "librarian" | "planner" | "planReviewer" | "task" | "codeReviewer";
|
|
6
|
+
type AgentGroup = "all" | "subagents";
|
|
7
|
+
type InjectMode = "system" | "context";
|
|
8
|
+
|
|
9
|
+
interface ContextFile {
|
|
10
|
+
mode: InjectMode;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Frontmatter {
|
|
15
|
+
inject: InjectMode;
|
|
16
|
+
agents: AgentType[];
|
|
17
|
+
agentGroups: AgentGroup[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const VALID_INJECT_MODES: readonly string[] = ["system", "context"];
|
|
21
|
+
const VALID_AGENTS: readonly string[] = ["main", "explore", "librarian", "planner", "planReviewer", "task", "codeReviewer"];
|
|
22
|
+
const VALID_AGENT_GROUPS: readonly string[] = ["all", "subagents"];
|
|
23
|
+
|
|
24
|
+
function parseFrontmatter(raw: string): { frontmatter: Frontmatter; body: string } {
|
|
25
|
+
const match = raw.match(/^---[^\S\n]*\n([\s\S]*?)\n---[^\S\n]*\n([\s\S]*)$/);
|
|
26
|
+
if (!match) {
|
|
27
|
+
return {
|
|
28
|
+
frontmatter: { inject: "context", agents: ["main"], agentGroups: [] },
|
|
29
|
+
body: raw,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const yamlBlock = match[1];
|
|
34
|
+
const body = match[2];
|
|
35
|
+
|
|
36
|
+
let inject: InjectMode = "context";
|
|
37
|
+
let agents: AgentType[] = [];
|
|
38
|
+
let agentGroups: AgentGroup[] = [];
|
|
39
|
+
|
|
40
|
+
for (const line of yamlBlock.split("\n")) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
43
|
+
|
|
44
|
+
const colonIdx = trimmed.indexOf(":");
|
|
45
|
+
if (colonIdx === -1) continue;
|
|
46
|
+
|
|
47
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
48
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
49
|
+
|
|
50
|
+
if (key === "inject") {
|
|
51
|
+
const cleaned = stripQuotes(val);
|
|
52
|
+
if (VALID_INJECT_MODES.includes(cleaned)) {
|
|
53
|
+
inject = cleaned as InjectMode;
|
|
54
|
+
}
|
|
55
|
+
} else if (key === "agents") {
|
|
56
|
+
agents = parseArray(val).filter((v): v is AgentType => VALID_AGENTS.includes(v));
|
|
57
|
+
} else if (key === "agentGroups") {
|
|
58
|
+
agentGroups = parseArray(val).filter((v): v is AgentGroup => VALID_AGENT_GROUPS.includes(v));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (agents.length === 0 && agentGroups.length === 0) {
|
|
63
|
+
agents = ["main"];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { frontmatter: { inject, agents, agentGroups }, body };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stripQuotes(val: string): string {
|
|
70
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
71
|
+
return val.slice(1, -1);
|
|
72
|
+
}
|
|
73
|
+
return val;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseArray(val: string): string[] {
|
|
77
|
+
const trimmed = val.trim();
|
|
78
|
+
if (trimmed.startsWith("[")) {
|
|
79
|
+
const closingIdx = trimmed.lastIndexOf("]");
|
|
80
|
+
const inner = closingIdx > 0 ? trimmed.slice(1, closingIdx) : trimmed.slice(1);
|
|
81
|
+
return inner
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((s) => stripQuotes(s.trim()))
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
}
|
|
86
|
+
return [stripQuotes(trimmed)];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sortByTimestampPrefix(a: string, b: string): number {
|
|
90
|
+
const aNum = parseInt(a, 10);
|
|
91
|
+
const bNum = parseInt(b, 10);
|
|
92
|
+
if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;
|
|
93
|
+
return a.localeCompare(b);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function matchesAgent(fm: Frontmatter, agentType: AgentType): boolean {
|
|
97
|
+
if (fm.agentGroups.includes("all")) return true;
|
|
98
|
+
if (fm.agentGroups.includes("subagents") && agentType !== "main") return true;
|
|
99
|
+
return fm.agents.includes(agentType);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function loadContextFiles(cwd: string, agentType: AgentType, injectMode?: InjectMode): ContextFile[] {
|
|
103
|
+
const contextDir = join(cwd, ".pp", "context");
|
|
104
|
+
if (!existsSync(contextDir)) return [];
|
|
105
|
+
|
|
106
|
+
const results: ContextFile[] = [];
|
|
107
|
+
for (const file of readdirSync(contextDir)) {
|
|
108
|
+
if (!file.endsWith(".md")) continue;
|
|
109
|
+
const filePath = join(contextDir, file);
|
|
110
|
+
let raw: string;
|
|
111
|
+
try {
|
|
112
|
+
raw = readFileSync(filePath, "utf-8");
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
console.error(`[pi-pi] Failed to read context file ${filePath}: ${err.message}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
118
|
+
|
|
119
|
+
if (!matchesAgent(frontmatter, agentType)) continue;
|
|
120
|
+
if (injectMode && frontmatter.inject !== injectMode) continue;
|
|
121
|
+
|
|
122
|
+
results.push({ mode: frontmatter.inject, content: body.trim() });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
127
|
+
|
|
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");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getPhaseArtifacts(taskDir: string, phase: Phase): { name: string; content: string }[] {
|
|
135
|
+
const artifacts: { name: string; content: string }[] = [];
|
|
136
|
+
|
|
137
|
+
const tryAdd = (name: string, path: string) => {
|
|
138
|
+
if (existsSync(path)) {
|
|
139
|
+
artifacts.push({ name, content: readFileSync(path, "utf-8") });
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
tryAdd("USER_REQUEST.md", join(taskDir, "USER_REQUEST.md"));
|
|
144
|
+
tryAdd("RESEARCH.md", join(taskDir, "RESEARCH.md"));
|
|
145
|
+
|
|
146
|
+
const artifactsDir = join(taskDir, "artifacts");
|
|
147
|
+
if (existsSync(artifactsDir)) {
|
|
148
|
+
for (const file of readdirSync(artifactsDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
149
|
+
tryAdd(`artifacts/${file}`, join(artifactsDir, file));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (phase === "plan" || phase === "implement") {
|
|
154
|
+
const plansDir = join(taskDir, "plans");
|
|
155
|
+
if (existsSync(plansDir)) {
|
|
156
|
+
const synthFiles = readdirSync(plansDir)
|
|
157
|
+
.filter((f) => f.includes("synthesized"))
|
|
158
|
+
.sort(sortByTimestampPrefix);
|
|
159
|
+
if (synthFiles.length > 0) {
|
|
160
|
+
const synthPath = join(plansDir, synthFiles[synthFiles.length - 1]);
|
|
161
|
+
tryAdd("Synthesized Plan", synthPath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return artifacts;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getLatestSynthesizedPlan(taskDir: string): string | null {
|
|
170
|
+
const plansDir = join(taskDir, "plans");
|
|
171
|
+
if (!existsSync(plansDir)) return null;
|
|
172
|
+
|
|
173
|
+
const synthFiles = readdirSync(plansDir)
|
|
174
|
+
.filter((f) => f.includes("synthesized"))
|
|
175
|
+
.sort(sortByTimestampPrefix);
|
|
176
|
+
if (synthFiles.length === 0) return null;
|
|
177
|
+
|
|
178
|
+
return readFileSync(join(plansDir, synthFiles[synthFiles.length - 1]), "utf-8");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function loadBrainstormReviewOutputs(taskDir: string, pass: number): { name: string; content: string }[] {
|
|
182
|
+
const dir = join(taskDir, "brainstorm-reviews");
|
|
183
|
+
if (!existsSync(dir)) return [];
|
|
184
|
+
return readdirSync(dir)
|
|
185
|
+
.filter((f) => f.includes(`round-${pass}`) && f.endsWith(".md"))
|
|
186
|
+
.sort()
|
|
187
|
+
.map((name) => ({ name, content: readFileSync(join(dir, name), "utf-8") }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function loadCodeReviewOutputs(taskDir: string, pass: number): { name: string; content: string }[] {
|
|
191
|
+
const dir = join(taskDir, "code-reviews");
|
|
192
|
+
if (!existsSync(dir)) return [];
|
|
193
|
+
return readdirSync(dir)
|
|
194
|
+
.filter((f) => f.includes(`round-${pass}`) && f.endsWith(".md"))
|
|
195
|
+
.sort()
|
|
196
|
+
.map((name) => ({ name, content: readFileSync(join(dir, name), "utf-8") }));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function loadPlanReviewOutputs(taskDir: string): { name: string; content: string }[] {
|
|
200
|
+
const dir = join(taskDir, "plan-reviews");
|
|
201
|
+
if (!existsSync(dir)) return [];
|
|
202
|
+
return readdirSync(dir)
|
|
203
|
+
.filter((f) => f.endsWith(".md"))
|
|
204
|
+
.sort()
|
|
205
|
+
.map((name) => ({ name, content: readFileSync(join(dir, name), "utf-8") }));
|
|
206
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
import { registerEventHandlers } from "./event-handlers.js";
|
|
3
|
+
import { Orchestrator, type ActiveTask } from "./orchestrator.js";
|
|
4
|
+
|
|
5
|
+
type Handler = (event: any, ctx: any) => any;
|
|
6
|
+
|
|
7
|
+
function makePi() {
|
|
8
|
+
const handlers = new Map<string, Handler>();
|
|
9
|
+
const eventHandlers = new Map<string, Handler>();
|
|
10
|
+
return {
|
|
11
|
+
on: vi.fn((name: string, handler: Handler) => {
|
|
12
|
+
handlers.set(name, handler);
|
|
13
|
+
}),
|
|
14
|
+
events: {
|
|
15
|
+
on: vi.fn((name: string, handler: Handler) => {
|
|
16
|
+
eventHandlers.set(name, handler);
|
|
17
|
+
}),
|
|
18
|
+
emit: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
getAllTools: vi.fn().mockReturnValue([{ name: "lsp" }]),
|
|
21
|
+
sendMessage: vi.fn(),
|
|
22
|
+
sendUserMessage: vi.fn(),
|
|
23
|
+
setModel: vi.fn(),
|
|
24
|
+
setThinkingLevel: vi.fn(),
|
|
25
|
+
setSessionName: vi.fn(),
|
|
26
|
+
_handlers: handlers,
|
|
27
|
+
_eventHandlers: eventHandlers,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeConfig() {
|
|
32
|
+
return {
|
|
33
|
+
mainModel: {
|
|
34
|
+
implement: { model: "a/b", thinking: "high" },
|
|
35
|
+
debug: { model: "a/b", thinking: "high" },
|
|
36
|
+
brainstorm: { model: "a/b", thinking: "high" },
|
|
37
|
+
},
|
|
38
|
+
planners: {},
|
|
39
|
+
planReviewers: {},
|
|
40
|
+
codeReviewers: {},
|
|
41
|
+
brainstormReviewers: {},
|
|
42
|
+
agents: {
|
|
43
|
+
explore: { model: "x/e", thinking: "low" },
|
|
44
|
+
librarian: { model: "x/l", thinking: "medium" },
|
|
45
|
+
task: { model: "x/t", thinking: "medium" },
|
|
46
|
+
},
|
|
47
|
+
commands: { afterEdit: [], afterImplement: [] },
|
|
48
|
+
timeouts: { afterEdit: 1, afterImplement: 1, agentSpawn: 1, agentReadyPing: 1, lockStale: 1, lockUpdate: 1 },
|
|
49
|
+
autoCommit: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeActiveTask(): ActiveTask {
|
|
54
|
+
return {
|
|
55
|
+
dir: "/tmp/task",
|
|
56
|
+
type: "implement",
|
|
57
|
+
state: {
|
|
58
|
+
phase: "implement",
|
|
59
|
+
step: "llm_work",
|
|
60
|
+
reviewCycle: null,
|
|
61
|
+
reviewPass: 0,
|
|
62
|
+
from: null,
|
|
63
|
+
description: "Test",
|
|
64
|
+
startedAt: new Date().toISOString(),
|
|
65
|
+
},
|
|
66
|
+
release: null,
|
|
67
|
+
taskId: "123",
|
|
68
|
+
modifiedFiles: new Set(),
|
|
69
|
+
reviewPass: 0,
|
|
70
|
+
description: "Test",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let pi: ReturnType<typeof makePi>;
|
|
75
|
+
let orchestrator: Orchestrator;
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
pi = makePi();
|
|
79
|
+
orchestrator = new Orchestrator(pi as any);
|
|
80
|
+
orchestrator.cwd = "/project";
|
|
81
|
+
orchestrator.config = makeConfig() as any;
|
|
82
|
+
registerEventHandlers(orchestrator);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function getHandler(name: string): Handler {
|
|
86
|
+
const h = pi._handlers.get(name);
|
|
87
|
+
if (!h) throw new Error(`No handler for ${name}`);
|
|
88
|
+
return h;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe("tool_call write protection", () => {
|
|
92
|
+
it("blocks non-.md writes to .pp/state/ when input uses path field", async () => {
|
|
93
|
+
const handler = getHandler("tool_call");
|
|
94
|
+
const result = await handler(
|
|
95
|
+
{ toolName: "write", input: { path: ".pp/state/implement/123/state.json" } },
|
|
96
|
+
{},
|
|
97
|
+
);
|
|
98
|
+
expect(result).toEqual({ block: true, reason: "Cannot write non-.md files in .pp/state/" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("blocks state.json writes under .pp/ when input uses path field", async () => {
|
|
102
|
+
const handler = getHandler("tool_call");
|
|
103
|
+
const result = await handler(
|
|
104
|
+
{ toolName: "edit", input: { path: ".pp/state.json" } },
|
|
105
|
+
{},
|
|
106
|
+
);
|
|
107
|
+
expect(result).toEqual({ block: true, reason: "state.json is managed by the extension" });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("blocks config.json writes under .pp/ when input uses path field", async () => {
|
|
111
|
+
const handler = getHandler("tool_call");
|
|
112
|
+
const result = await handler(
|
|
113
|
+
{ toolName: "write", input: { path: ".pp/config.json" } },
|
|
114
|
+
{},
|
|
115
|
+
);
|
|
116
|
+
expect(result).toEqual({ block: true, reason: "config.json is managed by the user, not the LLM" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("allows .md writes to .pp/state/ when input uses path field", async () => {
|
|
120
|
+
const handler = getHandler("tool_call");
|
|
121
|
+
const result = await handler(
|
|
122
|
+
{ toolName: "write", input: { path: ".pp/state/implement/123/plans/synthesized.md" } },
|
|
123
|
+
{},
|
|
124
|
+
);
|
|
125
|
+
expect(result).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("backward compatibility: blocks using file_path field", async () => {
|
|
129
|
+
const handler = getHandler("tool_call");
|
|
130
|
+
const result = await handler(
|
|
131
|
+
{ toolName: "write", input: { file_path: ".pp/state/implement/123/state.json" } },
|
|
132
|
+
{},
|
|
133
|
+
);
|
|
134
|
+
expect(result).toEqual({ block: true, reason: "Cannot write non-.md files in .pp/state/" });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("backward compatibility: blocks using filePath field", async () => {
|
|
138
|
+
const handler = getHandler("tool_call");
|
|
139
|
+
const result = await handler(
|
|
140
|
+
{ toolName: "edit", input: { filePath: ".pp/config.json" } },
|
|
141
|
+
{},
|
|
142
|
+
);
|
|
143
|
+
expect(result).toEqual({ block: true, reason: "config.json is managed by the user, not the LLM" });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("tool_result implementation tracking", () => {
|
|
148
|
+
it("adds file to modifiedFiles when input uses path field", async () => {
|
|
149
|
+
orchestrator.active = makeActiveTask();
|
|
150
|
+
const handler = getHandler("tool_result");
|
|
151
|
+
await handler(
|
|
152
|
+
{ toolName: "write", input: { path: "src/index.ts" }, isError: false, content: [] },
|
|
153
|
+
{},
|
|
154
|
+
);
|
|
155
|
+
expect(orchestrator.active.modifiedFiles.has("src/index.ts")).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("skips .pp/ paths in modified-file tracking", async () => {
|
|
159
|
+
orchestrator.active = makeActiveTask();
|
|
160
|
+
const handler = getHandler("tool_result");
|
|
161
|
+
await handler(
|
|
162
|
+
{ toolName: "write", input: { path: ".pp/state/plans/plan.md" }, isError: false, content: [] },
|
|
163
|
+
{},
|
|
164
|
+
);
|
|
165
|
+
expect(orchestrator.active.modifiedFiles.size).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("appends LSP nudge when input uses path field", async () => {
|
|
169
|
+
orchestrator.active = makeActiveTask();
|
|
170
|
+
const handler = getHandler("tool_result");
|
|
171
|
+
const result = await handler(
|
|
172
|
+
{ toolName: "edit", input: { path: "src/foo.ts" }, isError: false, content: [{ type: "text", text: "ok" }] },
|
|
173
|
+
{},
|
|
174
|
+
);
|
|
175
|
+
expect(result?.content).toBeDefined();
|
|
176
|
+
const lastContent = result.content[result.content.length - 1];
|
|
177
|
+
expect(lastContent.text).toContain("lsp diagnostics");
|
|
178
|
+
expect(lastContent.text).toContain("src/foo.ts");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("backward compatibility: tracks file using file_path field", async () => {
|
|
182
|
+
orchestrator.active = makeActiveTask();
|
|
183
|
+
const handler = getHandler("tool_result");
|
|
184
|
+
await handler(
|
|
185
|
+
{ toolName: "edit", input: { file_path: "src/bar.ts" }, isError: false, content: [] },
|
|
186
|
+
{},
|
|
187
|
+
);
|
|
188
|
+
expect(orchestrator.active.modifiedFiles.has("src/bar.ts")).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
});
|