@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,85 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
|
|
7
|
+
const SG_AVAILABLE_KEY = Symbol.for("pi-pi:sg-available");
|
|
8
|
+
|
|
9
|
+
function isSgAvailable(): boolean {
|
|
10
|
+
const cached = (globalThis as any)[SG_AVAILABLE_KEY];
|
|
11
|
+
if (cached !== undefined) return cached;
|
|
12
|
+
try {
|
|
13
|
+
const { execFileSync } = require("child_process");
|
|
14
|
+
execFileSync("sg", ["--version"], { encoding: "utf-8", stdio: "pipe" });
|
|
15
|
+
(globalThis as any)[SG_AVAILABLE_KEY] = true;
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
(globalThis as any)[SG_AVAILABLE_KEY] = false;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ok(text: string) {
|
|
24
|
+
return { content: [{ type: "text" as const, text }], details: {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fail(text: string) {
|
|
28
|
+
return { content: [{ type: "text" as const, text }], isError: true as const, details: {} };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function registerAstSearchTool(pi: ExtensionAPI, cwd: string): boolean {
|
|
32
|
+
if (!isSgAvailable()) return false;
|
|
33
|
+
|
|
34
|
+
pi.registerTool({
|
|
35
|
+
name: "ast_search",
|
|
36
|
+
label: "ast-grep",
|
|
37
|
+
description:
|
|
38
|
+
"Search code using AST-aware structural patterns. Uses ast-grep (sg). " +
|
|
39
|
+
"Patterns use meta-variables: $NAME matches a single node, $$$ matches multiple nodes. " +
|
|
40
|
+
"Examples: 'if err != nil { $$$ }', 'func $NAME($$$) { $$$ }', '$X.($TYPE)'",
|
|
41
|
+
parameters: Type.Object({
|
|
42
|
+
pattern: Type.String({ description: "AST pattern to search for" }),
|
|
43
|
+
lang: Type.Optional(Type.String({ description: "Language hint (e.g. 'go', 'typescript', 'python')" })),
|
|
44
|
+
path: Type.Optional(Type.String({ description: "Directory or file to search (default: project root)" })),
|
|
45
|
+
}),
|
|
46
|
+
async execute(_toolCallId: string, params: any) {
|
|
47
|
+
const searchPath = resolve(cwd, params.path ?? ".");
|
|
48
|
+
if (!existsSync(searchPath)) {
|
|
49
|
+
return fail(`Path '${params.path}' does not exist`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const args = ["run", "--json", "-p", params.pattern];
|
|
53
|
+
if (params.lang) args.push("-l", params.lang);
|
|
54
|
+
args.push(searchPath);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const stdout = await new Promise<string>((res, rej) => {
|
|
58
|
+
execFile("sg", args, { timeout: 30000, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }, (err, out, stderr) => {
|
|
59
|
+
if (err && !out) return rej(new Error(stderr?.trim() || err.message));
|
|
60
|
+
res(out);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const results = JSON.parse(stdout || "[]");
|
|
65
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
66
|
+
return ok("No matches found.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const lines: string[] = [];
|
|
70
|
+
for (const match of results) {
|
|
71
|
+
const file = match.file ?? "?";
|
|
72
|
+
const startLine = match.range?.start?.line ?? match.start?.line ?? "?";
|
|
73
|
+
const text = match.text ?? match.matchedCode ?? "";
|
|
74
|
+
lines.push(`${file}:${startLine}: ${text.trim()}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return ok(`${results.length} match(es):\n\n${lines.join("\n")}`);
|
|
78
|
+
} catch (e: any) {
|
|
79
|
+
return fail(`ast_search error: ${e.message}`);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { spawn, execFileSync, type ChildProcess } from "child_process";
|
|
2
|
+
import { createInterface, type Interface as ReadlineInterface } from "readline";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
|
|
6
|
+
function findCbmBin(): string | null {
|
|
7
|
+
try {
|
|
8
|
+
return execFileSync("which", ["codebase-memory-mcp"], { encoding: "utf-8", stdio: "pipe" }).trim() || null;
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CBM_BIN_KEY = Symbol.for("pi-pi:cbm-bin");
|
|
15
|
+
|
|
16
|
+
function getCbmBin(): string | null {
|
|
17
|
+
if ((globalThis as any)[CBM_BIN_KEY] !== undefined) return (globalThis as any)[CBM_BIN_KEY];
|
|
18
|
+
const bin = findCbmBin();
|
|
19
|
+
(globalThis as any)[CBM_BIN_KEY] = bin;
|
|
20
|
+
return bin;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isCbmAvailable(): boolean {
|
|
24
|
+
return getCbmBin() !== null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CBM_DAEMON_KEY = Symbol.for("pi-pi:cbm-daemon");
|
|
28
|
+
|
|
29
|
+
function getGlobalDaemon(): CbmDaemon | null {
|
|
30
|
+
return (globalThis as any)[CBM_DAEMON_KEY] ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setGlobalDaemon(daemon: CbmDaemon): void {
|
|
34
|
+
(globalThis as any)[CBM_DAEMON_KEY] = daemon;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class CbmDaemon {
|
|
38
|
+
private proc: ChildProcess | null = null;
|
|
39
|
+
private rl: ReadlineInterface | null = null;
|
|
40
|
+
private nextId = 1;
|
|
41
|
+
private pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>();
|
|
42
|
+
private initialized = false;
|
|
43
|
+
private initPromise: Promise<void> | null = null;
|
|
44
|
+
private indexedProjects = new Set<string>();
|
|
45
|
+
|
|
46
|
+
start(): void {
|
|
47
|
+
if (this.proc) return;
|
|
48
|
+
|
|
49
|
+
this.proc = spawn(getCbmBin()!, [], { stdio: ["pipe", "pipe", "ignore"] });
|
|
50
|
+
this.proc.unref();
|
|
51
|
+
(this.proc.stdout as any)?.unref?.();
|
|
52
|
+
(this.proc.stdin as any)?.unref?.();
|
|
53
|
+
|
|
54
|
+
this.rl = createInterface({ input: this.proc.stdout! });
|
|
55
|
+
this.rl.on("line", (line) => this.handleLine(line));
|
|
56
|
+
|
|
57
|
+
this.proc.on("exit", () => this.cleanup());
|
|
58
|
+
this.proc.on("error", (err) => {
|
|
59
|
+
console.error(`[pi-pi] CBM daemon error: ${err.message}`);
|
|
60
|
+
this.cleanup();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.initPromise = this.initialize();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private cleanup(): void {
|
|
67
|
+
for (const [, entry] of this.pending) {
|
|
68
|
+
clearTimeout(entry.timer);
|
|
69
|
+
entry.reject(new Error("CBM daemon exited"));
|
|
70
|
+
}
|
|
71
|
+
this.pending.clear();
|
|
72
|
+
this.rl?.close();
|
|
73
|
+
this.rl = null;
|
|
74
|
+
this.proc = null;
|
|
75
|
+
this.initialized = false;
|
|
76
|
+
this.initPromise = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stop(): void {
|
|
80
|
+
if (!this.proc) return;
|
|
81
|
+
try { this.proc.kill(); } catch {}
|
|
82
|
+
this.cleanup();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async initialize(): Promise<void> {
|
|
86
|
+
await this.rpc("initialize", {
|
|
87
|
+
protocolVersion: "2024-11-05",
|
|
88
|
+
capabilities: {},
|
|
89
|
+
clientInfo: { name: "pi-pi", version: "1.0" },
|
|
90
|
+
});
|
|
91
|
+
this.initialized = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private handleLine(line: string): void {
|
|
95
|
+
if (!line.trim()) return;
|
|
96
|
+
try {
|
|
97
|
+
const msg = JSON.parse(line);
|
|
98
|
+
if (msg.id != null && this.pending.has(msg.id)) {
|
|
99
|
+
const entry = this.pending.get(msg.id)!;
|
|
100
|
+
this.pending.delete(msg.id);
|
|
101
|
+
clearTimeout(entry.timer);
|
|
102
|
+
if (msg.error) {
|
|
103
|
+
entry.reject(new Error(msg.error.message ?? JSON.stringify(msg.error)));
|
|
104
|
+
} else {
|
|
105
|
+
entry.resolve(msg.result);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// ignore unparseable lines (e.g. log output on stderr)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private rpc(method: string, params: Record<string, unknown>, timeoutMs = 60000): Promise<any> {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
if (!this.proc?.stdin?.writable) {
|
|
116
|
+
return reject(new Error("CBM daemon not running"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const id = this.nextId++;
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
this.pending.delete(id);
|
|
122
|
+
reject(new Error(`CBM call timed out: ${method}`));
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
|
|
125
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
126
|
+
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async ensureReady(): Promise<void> {
|
|
131
|
+
if (!this.proc) this.start();
|
|
132
|
+
if (this.initPromise) await this.initPromise;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async callTool(name: string, args: Record<string, unknown>, timeoutMs = 60000): Promise<unknown> {
|
|
136
|
+
await this.ensureReady();
|
|
137
|
+
const result = await this.rpc("tools/call", { name, arguments: args }, timeoutMs);
|
|
138
|
+
if (result?.isError) {
|
|
139
|
+
throw new Error(result.content?.[0]?.text ?? "unknown CBM error");
|
|
140
|
+
}
|
|
141
|
+
const text = result?.content?.[0]?.text;
|
|
142
|
+
if (!text) return result;
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(text);
|
|
145
|
+
} catch {
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async ensureIndexed(cwd: string): Promise<string> {
|
|
151
|
+
const name = projectName(cwd);
|
|
152
|
+
if (this.indexedProjects.has(name)) return name;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const projects = (await this.callTool("list_projects", {})) as { projects: Array<{ name: string }> };
|
|
156
|
+
if (projects.projects.some((p) => p.name === name)) {
|
|
157
|
+
this.indexedProjects.add(name);
|
|
158
|
+
return name;
|
|
159
|
+
}
|
|
160
|
+
} catch { /* fall through to index */ }
|
|
161
|
+
|
|
162
|
+
await this.callTool("index_repository", { repo_path: cwd }, 300000);
|
|
163
|
+
this.indexedProjects.add(name);
|
|
164
|
+
return name;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function projectName(cwd: string): string {
|
|
169
|
+
return cwd.replace(/^\//, "").replace(/\//g, "-");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ok(text: string) {
|
|
173
|
+
return { content: [{ type: "text" as const, text }], details: {} };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function fail(text: string) {
|
|
177
|
+
return { content: [{ type: "text" as const, text }], isError: true as const, details: {} };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function registerCbmTools(pi: ExtensionAPI, cwd: string): boolean {
|
|
181
|
+
if (!isCbmAvailable()) return false;
|
|
182
|
+
|
|
183
|
+
let daemon = getGlobalDaemon();
|
|
184
|
+
if (!daemon) {
|
|
185
|
+
daemon = new CbmDaemon();
|
|
186
|
+
daemon.start();
|
|
187
|
+
setGlobalDaemon(daemon);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pi.registerTool({
|
|
191
|
+
name: "cbm_search",
|
|
192
|
+
label: "CBM",
|
|
193
|
+
description:
|
|
194
|
+
"Search the codebase knowledge graph. Use `query` for natural-language BM25 search, " +
|
|
195
|
+
"`name_pattern` for regex on symbol names, or `semantic_query` (array of keywords) for " +
|
|
196
|
+
"vector similarity. Returns symbols with name, file_path, label, in_degree, out_degree.",
|
|
197
|
+
parameters: Type.Object({
|
|
198
|
+
query: Type.Optional(Type.String({ description: "Natural-language BM25 search (e.g. 'deploy release chart')" })),
|
|
199
|
+
name_pattern: Type.Optional(Type.String({ description: "Regex pattern on symbol names" })),
|
|
200
|
+
semantic_query: Type.Optional(Type.Array(Type.String(), { description: "Array of keywords for vector similarity search" })),
|
|
201
|
+
label: Type.Optional(Type.String({ description: "Filter by node type: Function, Method, Interface, Class, Type, Route" })),
|
|
202
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default: 20)" })),
|
|
203
|
+
}),
|
|
204
|
+
async execute(_toolCallId, params: any) {
|
|
205
|
+
try {
|
|
206
|
+
const project = await daemon.ensureIndexed(cwd);
|
|
207
|
+
const p: Record<string, unknown> = { project, limit: params.limit ?? 20 };
|
|
208
|
+
if (params.query) p.query = params.query;
|
|
209
|
+
if (params.name_pattern) p.name_pattern = params.name_pattern;
|
|
210
|
+
if (params.semantic_query) p.semantic_query = params.semantic_query;
|
|
211
|
+
if (params.label) p.label = params.label;
|
|
212
|
+
return ok(JSON.stringify(await daemon.callTool("search_graph", p), null, 2));
|
|
213
|
+
} catch (e: any) {
|
|
214
|
+
return fail(`cbm_search error: ${e.message}`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
pi.registerTool({
|
|
220
|
+
name: "cbm_search_code",
|
|
221
|
+
label: "CBM",
|
|
222
|
+
description:
|
|
223
|
+
"Graph-augmented grep: searches code text then deduplicates matches into containing " +
|
|
224
|
+
"functions with structural metadata (in_degree, out_degree, label). Better than raw grep " +
|
|
225
|
+
"for understanding which functions contain a pattern.",
|
|
226
|
+
parameters: Type.Object({
|
|
227
|
+
pattern: Type.String({ description: "Text pattern to search for" }),
|
|
228
|
+
file_pattern: Type.Optional(Type.String({ description: "Glob filter (e.g. '*.go', '*.ts')" })),
|
|
229
|
+
path_filter: Type.Optional(Type.String({ description: "Regex filter on file paths (e.g. '^pkg/')" })),
|
|
230
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default: 20)" })),
|
|
231
|
+
}),
|
|
232
|
+
async execute(_toolCallId, params: any) {
|
|
233
|
+
try {
|
|
234
|
+
const project = await daemon.ensureIndexed(cwd);
|
|
235
|
+
const p: Record<string, unknown> = { project, pattern: params.pattern, limit: params.limit ?? 20 };
|
|
236
|
+
if (params.file_pattern) p.file_pattern = params.file_pattern;
|
|
237
|
+
if (params.path_filter) p.path_filter = params.path_filter;
|
|
238
|
+
return ok(JSON.stringify(await daemon.callTool("search_code", p), null, 2));
|
|
239
|
+
} catch (e: any) {
|
|
240
|
+
return fail(`cbm_search_code error: ${e.message}`);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
pi.registerTool({
|
|
246
|
+
name: "cbm_trace",
|
|
247
|
+
label: "CBM",
|
|
248
|
+
description:
|
|
249
|
+
"Trace call chains through the code graph. Find who calls a function (inbound) or " +
|
|
250
|
+
"what a function calls (outbound). Useful for understanding dependencies and impact.",
|
|
251
|
+
parameters: Type.Object({
|
|
252
|
+
function_name: Type.String({ description: "Function name to trace" }),
|
|
253
|
+
direction: Type.Optional(Type.String({ description: "inbound, outbound, or both (default: both)" })),
|
|
254
|
+
depth: Type.Optional(Type.Number({ description: "Max traversal depth (default: 3)" })),
|
|
255
|
+
}),
|
|
256
|
+
async execute(_toolCallId, params: any) {
|
|
257
|
+
try {
|
|
258
|
+
const project = await daemon.ensureIndexed(cwd);
|
|
259
|
+
const p: Record<string, unknown> = {
|
|
260
|
+
project,
|
|
261
|
+
function_name: params.function_name,
|
|
262
|
+
direction: params.direction ?? "both",
|
|
263
|
+
depth: params.depth ?? 3,
|
|
264
|
+
};
|
|
265
|
+
return ok(JSON.stringify(await daemon.callTool("trace_path", p), null, 2));
|
|
266
|
+
} catch (e: any) {
|
|
267
|
+
return fail(`cbm_trace error: ${e.message}`);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
pi.registerTool({
|
|
273
|
+
name: "cbm_changes",
|
|
274
|
+
label: "CBM",
|
|
275
|
+
description:
|
|
276
|
+
"Detect code changes from git diff and map to affected symbols with blast radius. " +
|
|
277
|
+
"Shows which functions/types are impacted by uncommitted or branch changes.",
|
|
278
|
+
parameters: Type.Object({
|
|
279
|
+
base_branch: Type.Optional(Type.String({ description: "Compare against this branch (default: main)" })),
|
|
280
|
+
since: Type.Optional(Type.String({ description: "Git ref or date (e.g. HEAD~5, v0.5.0)" })),
|
|
281
|
+
}),
|
|
282
|
+
async execute(_toolCallId, params: any) {
|
|
283
|
+
try {
|
|
284
|
+
const project = await daemon.ensureIndexed(cwd);
|
|
285
|
+
const p: Record<string, unknown> = { project };
|
|
286
|
+
if (params.base_branch) p.base_branch = params.base_branch;
|
|
287
|
+
if (params.since) p.since = params.since;
|
|
288
|
+
return ok(JSON.stringify(await daemon.callTool("detect_changes", p), null, 2));
|
|
289
|
+
} catch (e: any) {
|
|
290
|
+
return fail(`cbm_changes error: ${e.message}`);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
pi.registerTool({
|
|
296
|
+
name: "cbm_query",
|
|
297
|
+
label: "CBM",
|
|
298
|
+
description:
|
|
299
|
+
"Execute a Cypher-like graph query for complex multi-hop patterns. " +
|
|
300
|
+
"Example: MATCH (f:Function)-[:CALLS]->(g:Function) WHERE f.name = 'main' RETURN g.name, g.file_path LIMIT 10",
|
|
301
|
+
parameters: Type.Object({
|
|
302
|
+
query: Type.String({ description: "Cypher query string" }),
|
|
303
|
+
}),
|
|
304
|
+
async execute(_toolCallId, params: any) {
|
|
305
|
+
try {
|
|
306
|
+
const project = await daemon.ensureIndexed(cwd);
|
|
307
|
+
return ok(JSON.stringify(await daemon.callTool("query_graph", { project, query: params.query }), null, 2));
|
|
308
|
+
} catch (e: any) {
|
|
309
|
+
return fail(`cbm_query error: ${e.message}`);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
pi.registerTool({
|
|
315
|
+
name: "cbm_architecture",
|
|
316
|
+
label: "CBM",
|
|
317
|
+
description: "Get high-level architecture overview of the indexed codebase — node/edge counts, schema, structure.",
|
|
318
|
+
parameters: Type.Object({}),
|
|
319
|
+
async execute() {
|
|
320
|
+
try {
|
|
321
|
+
const project = await daemon.ensureIndexed(cwd);
|
|
322
|
+
return ok(JSON.stringify(await daemon.callTool("get_architecture", { project }), null, 2));
|
|
323
|
+
} catch (e: any) {
|
|
324
|
+
return fail(`cbm_architecture error: ${e.message}`);
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { registerCommandHandlers } from "./command-handlers.js";
|
|
6
|
+
import { Orchestrator, type ActiveTask } from "./orchestrator.js";
|
|
7
|
+
|
|
8
|
+
vi.mock("./event-handlers.js", () => ({
|
|
9
|
+
runUserGateDialog: vi.fn().mockResolvedValue("User wants to continue. Run /pp:next when ready to advance."),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { runUserGateDialog } from "./event-handlers.js";
|
|
13
|
+
|
|
14
|
+
type Handler = (args: string | undefined, ctx: any) => any;
|
|
15
|
+
|
|
16
|
+
const tempDirs: string[] = [];
|
|
17
|
+
|
|
18
|
+
function makeTempDir(): string {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-pi-cmd-"));
|
|
20
|
+
tempDirs.push(dir);
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.mocked(runUserGateDialog).mockClear();
|
|
26
|
+
while (tempDirs.length > 0) {
|
|
27
|
+
const dir = tempDirs.pop();
|
|
28
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function makePi() {
|
|
33
|
+
const commands = new Map<string, { handler: Handler }>();
|
|
34
|
+
return {
|
|
35
|
+
registerCommand: vi.fn((name: string, cmd: { handler: Handler }) => {
|
|
36
|
+
commands.set(name, cmd);
|
|
37
|
+
}),
|
|
38
|
+
on: vi.fn(),
|
|
39
|
+
events: { on: vi.fn(), emit: vi.fn() },
|
|
40
|
+
getAllTools: vi.fn().mockReturnValue([]),
|
|
41
|
+
sendMessage: vi.fn(),
|
|
42
|
+
sendUserMessage: vi.fn(),
|
|
43
|
+
setModel: vi.fn(),
|
|
44
|
+
setThinkingLevel: vi.fn(),
|
|
45
|
+
setSessionName: vi.fn(),
|
|
46
|
+
_commands: commands,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeConfig() {
|
|
51
|
+
return {
|
|
52
|
+
mainModel: {
|
|
53
|
+
implement: { model: "a/b", thinking: "high" },
|
|
54
|
+
debug: { model: "a/b", thinking: "high" },
|
|
55
|
+
brainstorm: { model: "a/b", thinking: "high" },
|
|
56
|
+
},
|
|
57
|
+
planners: {},
|
|
58
|
+
planReviewers: {},
|
|
59
|
+
codeReviewers: { variant1: { enabled: true, model: "x/1", thinking: "low" } },
|
|
60
|
+
brainstormReviewers: { variant1: { enabled: true, model: "x/1", thinking: "low" } },
|
|
61
|
+
agents: {
|
|
62
|
+
explore: { model: "x/e", thinking: "low" },
|
|
63
|
+
librarian: { model: "x/l", thinking: "medium" },
|
|
64
|
+
task: { model: "x/t", thinking: "medium" },
|
|
65
|
+
},
|
|
66
|
+
commands: { afterEdit: [], afterImplement: [] },
|
|
67
|
+
timeouts: { afterEdit: 1, afterImplement: 1, agentSpawn: 1, agentReadyPing: 1, lockStale: 1, lockUpdate: 1 },
|
|
68
|
+
autoCommit: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function makeActiveTask(taskDir: string): ActiveTask {
|
|
73
|
+
const stateDir = join(taskDir, "plans");
|
|
74
|
+
mkdirSync(stateDir, { recursive: true });
|
|
75
|
+
writeFileSync(join(stateDir, "1_synthesized.md"), "- [x] done\n", "utf-8");
|
|
76
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), "request", "utf-8");
|
|
77
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), "research", "utf-8");
|
|
78
|
+
return {
|
|
79
|
+
dir: taskDir,
|
|
80
|
+
type: "implement",
|
|
81
|
+
state: {
|
|
82
|
+
phase: "implement",
|
|
83
|
+
step: "user_gate",
|
|
84
|
+
reviewCycle: null,
|
|
85
|
+
reviewPass: 1,
|
|
86
|
+
from: null,
|
|
87
|
+
description: "Test",
|
|
88
|
+
startedAt: new Date().toISOString(),
|
|
89
|
+
},
|
|
90
|
+
release: null,
|
|
91
|
+
taskId: "123",
|
|
92
|
+
modifiedFiles: new Set(),
|
|
93
|
+
reviewPass: 1,
|
|
94
|
+
description: "Test",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function makeCtx() {
|
|
99
|
+
return {
|
|
100
|
+
ui: {
|
|
101
|
+
confirm: vi.fn(),
|
|
102
|
+
select: vi.fn(),
|
|
103
|
+
notify: vi.fn(),
|
|
104
|
+
setStatus: vi.fn(),
|
|
105
|
+
},
|
|
106
|
+
abort: vi.fn(),
|
|
107
|
+
waitForIdle: vi.fn().mockResolvedValue(undefined),
|
|
108
|
+
compact: vi.fn(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("pp:next user gate", () => {
|
|
113
|
+
it("routes pp:next to shared user gate dialog", async () => {
|
|
114
|
+
const pi = makePi();
|
|
115
|
+
const orchestrator = new Orchestrator(pi as any);
|
|
116
|
+
const taskDir = makeTempDir();
|
|
117
|
+
orchestrator.cwd = taskDir;
|
|
118
|
+
orchestrator.config = makeConfig() as any;
|
|
119
|
+
orchestrator.active = makeActiveTask(taskDir);
|
|
120
|
+
registerCommandHandlers(orchestrator);
|
|
121
|
+
|
|
122
|
+
const ctx = makeCtx();
|
|
123
|
+
|
|
124
|
+
const ppNext = pi._commands.get("pp:next");
|
|
125
|
+
await ppNext!.handler(undefined, ctx);
|
|
126
|
+
|
|
127
|
+
expect(runUserGateDialog).toHaveBeenCalledOnce();
|
|
128
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith("[PI-PI] User wants to continue. Run /pp:next when ready to advance.");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns error when no active task", async () => {
|
|
132
|
+
const pi = makePi();
|
|
133
|
+
const orchestrator = new Orchestrator(pi as any);
|
|
134
|
+
orchestrator.cwd = makeTempDir();
|
|
135
|
+
orchestrator.config = makeConfig() as any;
|
|
136
|
+
registerCommandHandlers(orchestrator);
|
|
137
|
+
|
|
138
|
+
const ctx = makeCtx();
|
|
139
|
+
|
|
140
|
+
const ppNext = pi._commands.get("pp:next");
|
|
141
|
+
await ppNext!.handler(undefined, ctx);
|
|
142
|
+
|
|
143
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("No active task.", "error");
|
|
144
|
+
expect(runUserGateDialog).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("passes standard summary text to user gate dialog", async () => {
|
|
148
|
+
const pi = makePi();
|
|
149
|
+
const orchestrator = new Orchestrator(pi as any);
|
|
150
|
+
const taskDir = makeTempDir();
|
|
151
|
+
orchestrator.cwd = taskDir;
|
|
152
|
+
orchestrator.config = makeConfig() as any;
|
|
153
|
+
orchestrator.active = makeActiveTask(taskDir);
|
|
154
|
+
registerCommandHandlers(orchestrator);
|
|
155
|
+
|
|
156
|
+
const ctx = makeCtx();
|
|
157
|
+
|
|
158
|
+
const ppNext = pi._commands.get("pp:next");
|
|
159
|
+
await ppNext!.handler(undefined, ctx);
|
|
160
|
+
|
|
161
|
+
expect(runUserGateDialog).toHaveBeenCalledWith(orchestrator, ctx, "Choose next action");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { unregisterAgentDefinitions } from "./agents/registry.js";
|
|
2
|
+
import { runAfterImplement } from "./commands.js";
|
|
3
|
+
import { nextPhase, validateExitCriteria } from "./phases/machine.js";
|
|
4
|
+
import { spawnPlanners } from "./phases/planning.js";
|
|
5
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
6
|
+
import { saveTask } from "./state.js";
|
|
7
|
+
|
|
8
|
+
export async function transitionToNextPhase(
|
|
9
|
+
orchestrator: Orchestrator,
|
|
10
|
+
ctx: any,
|
|
11
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
12
|
+
if (!orchestrator.active) return { ok: false, error: "No active task." };
|
|
13
|
+
|
|
14
|
+
const currentPhase = orchestrator.active.state.phase;
|
|
15
|
+
if (currentPhase === "done") return { ok: false, error: "Task is already done." };
|
|
16
|
+
|
|
17
|
+
const exitCheck = validateExitCriteria(orchestrator.active.dir, orchestrator.active.type, currentPhase);
|
|
18
|
+
if (!exitCheck.ok) {
|
|
19
|
+
return { ok: false, error: exitCheck.reason };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (orchestrator.phaseStartTime > 0) {
|
|
23
|
+
const elapsed = Math.round((Date.now() - orchestrator.phaseStartTime) / 1000);
|
|
24
|
+
const min = Math.floor(elapsed / 60);
|
|
25
|
+
const sec = elapsed % 60;
|
|
26
|
+
ctx.ui.notify(`Phase "${currentPhase}" completed in ${min > 0 ? `${min}m ${sec}s` : `${sec}s`}`, "info");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const next = nextPhase(orchestrator.active.type, currentPhase);
|
|
30
|
+
if (!next) return { ok: false, error: "No next phase available." };
|
|
31
|
+
|
|
32
|
+
if (currentPhase === "implement") {
|
|
33
|
+
const afterResults = runAfterImplement(orchestrator.config, orchestrator.cwd);
|
|
34
|
+
const failures = afterResults.filter((r) => !r.ok);
|
|
35
|
+
if (failures.length > 0) {
|
|
36
|
+
const failureText = failures.map((f) => `${f.command}: ${f.output}`).join("\n");
|
|
37
|
+
return { ok: false, error: `afterImplement commands failed:\n${failureText}\n\nFix these issues before advancing.` };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
orchestrator.active.state.phase = next;
|
|
42
|
+
if (next !== "done") {
|
|
43
|
+
orchestrator.active.state.reviewCycle = null;
|
|
44
|
+
orchestrator.active.state.reviewPass = 0;
|
|
45
|
+
orchestrator.active.state.reviewPassByKind = {};
|
|
46
|
+
orchestrator.active.reviewPass = 0;
|
|
47
|
+
}
|
|
48
|
+
if (next === "plan") {
|
|
49
|
+
orchestrator.active.state.step = "spawn_planners";
|
|
50
|
+
} else if (next === "implement") {
|
|
51
|
+
orchestrator.active.state.step = "llm_work";
|
|
52
|
+
} else if (next === "brainstorm" || next === "debug") {
|
|
53
|
+
orchestrator.active.state.step = "llm_work";
|
|
54
|
+
} else if (next === "done") {
|
|
55
|
+
orchestrator.active.state.step = null;
|
|
56
|
+
}
|
|
57
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
58
|
+
|
|
59
|
+
if (next === "done") {
|
|
60
|
+
const name = orchestrator.active.description;
|
|
61
|
+
const type = orchestrator.active.type;
|
|
62
|
+
orchestrator.taskDoneCompactionPending = true;
|
|
63
|
+
orchestrator.taskDoneCompactionSummary = `Task "${name}" (${type}) completed.`;
|
|
64
|
+
|
|
65
|
+
orchestrator.abortAllSubagents();
|
|
66
|
+
unregisterAgentDefinitions(orchestrator.pi);
|
|
67
|
+
await orchestrator.cleanupActive();
|
|
68
|
+
orchestrator.updateStatus(ctx);
|
|
69
|
+
ctx.compact();
|
|
70
|
+
ctx.ui.notify("Task completed!", "info");
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
orchestrator.updateStatus(ctx);
|
|
75
|
+
|
|
76
|
+
if (next === "plan") {
|
|
77
|
+
orchestrator.active.state.step = "await_planners";
|
|
78
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
orchestrator.compactAndTransition(ctx, orchestrator.active.dir, orchestrator.active.state.phase);
|
|
82
|
+
|
|
83
|
+
if (next === "plan") {
|
|
84
|
+
orchestrator.pendingSubagentSpawns = Object.values(orchestrator.config.planners).filter((v) => v.enabled).length;
|
|
85
|
+
orchestrator.failedPlannerVariants = [];
|
|
86
|
+
spawnPlanners(orchestrator.pi, orchestrator.cwd, orchestrator.active.dir, orchestrator.active.taskId, orchestrator.config).then((result) => {
|
|
87
|
+
orchestrator.failedPlannerVariants = result.failedVariants;
|
|
88
|
+
if (result.spawned === 0) orchestrator.pendingSubagentSpawns = 0;
|
|
89
|
+
for (const id of result.agentIds ?? []) {
|
|
90
|
+
orchestrator.spawnedAgentIds.delete(id);
|
|
91
|
+
}
|
|
92
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
93
|
+
}).catch((err) => {
|
|
94
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
95
|
+
console.error(`[pi-pi] spawnPlanners failed: ${err.message}`);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function registerCommandHandlers(orchestrator: Orchestrator): void {
|
|
103
|
+
const pi = orchestrator.pi;
|
|
104
|
+
orchestrator.transitionToNextPhase = (ctx: any) => transitionToNextPhase(orchestrator, ctx);
|
|
105
|
+
|
|
106
|
+
pi.registerCommand("pp", {
|
|
107
|
+
description: "Open pi-pi task menu",
|
|
108
|
+
handler: async (_args, ctx) => {
|
|
109
|
+
const { showPpMenu } = await import("./pp-menu.js");
|
|
110
|
+
const text = await showPpMenu(orchestrator, ctx, "command");
|
|
111
|
+
if (text) {
|
|
112
|
+
pi.sendUserMessage(`[PI-PI] ${text}`);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|