@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,1416 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
2
|
+
import { resolve, basename, join } from "path";
|
|
3
|
+
import { validateUserRequest, validateResearch, validateArtifact } from "./validate-artifacts.js";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { runAfterEdit, autoCommit } from "./commands.js";
|
|
7
|
+
import { taskName, getActiveTask, saveTask, appendTaskLog } from "./state.js";
|
|
8
|
+
import {
|
|
9
|
+
loadContextFiles,
|
|
10
|
+
getPhaseArtifacts,
|
|
11
|
+
getLatestSynthesizedPlan,
|
|
12
|
+
loadBrainstormReviewOutputs,
|
|
13
|
+
loadCodeReviewOutputs,
|
|
14
|
+
loadPlanReviewOutputs,
|
|
15
|
+
} from "./context.js";
|
|
16
|
+
import { WORKING_PRINCIPLES, COMMUNICATION } from "./agents/tool-routing.js";
|
|
17
|
+
import { registerCbmTools } from "./cbm.js";
|
|
18
|
+
import { registerExaTools } from "./exa.js";
|
|
19
|
+
import { registerAstSearchTool } from "./ast-search.js";
|
|
20
|
+
import { SUBAGENT_SESSION_KEY } from "./index.js";
|
|
21
|
+
import { setExtensionOnlyMode, unregisterAgentDefinitions } from "./agents/registry.js";
|
|
22
|
+
import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
|
|
23
|
+
import { spawnCodeReviewers } from "./phases/review.js";
|
|
24
|
+
import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
25
|
+
import { openPlannotator, waitForPlannotatorResult, cancelPendingPlannotatorWait } from "./plannotator.js";
|
|
26
|
+
import { Orchestrator, deepReviewConfig, type ActiveTask } from "./orchestrator.js";
|
|
27
|
+
import { askUser } from "../../3p/pi-ask-user/index.js";
|
|
28
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
|
|
30
|
+
export async function detectDefaultBranch(pi: ExtensionAPI, cwd: string, config?: { diffBaseBranch?: string }): Promise<string> {
|
|
31
|
+
if (config?.diffBaseBranch) return config.diffBaseBranch;
|
|
32
|
+
try {
|
|
33
|
+
const result = await pi.exec("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd, timeout: 5000 });
|
|
34
|
+
if (result.code === 0 && result.stdout.trim()) {
|
|
35
|
+
return "origin/" + result.stdout.trim().replace("refs/remotes/origin/", "");
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
try {
|
|
39
|
+
const result = await pi.exec("git", ["show-ref", "--verify", "refs/remotes/origin/main"], { cwd, timeout: 5000 });
|
|
40
|
+
if (result.code === 0) return "origin/main";
|
|
41
|
+
} catch {}
|
|
42
|
+
try {
|
|
43
|
+
const result = await pi.exec("git", ["show-ref", "--verify", "refs/remotes/origin/master"], { cwd, timeout: 5000 });
|
|
44
|
+
if (result.code === 0) return "origin/master";
|
|
45
|
+
} catch {}
|
|
46
|
+
return "main";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function detectRepoCwd(pi: ExtensionAPI, modifiedFiles: Set<string>, fallbackCwd: string): Promise<string> {
|
|
50
|
+
if (modifiedFiles.size === 0) return fallbackCwd;
|
|
51
|
+
const firstFile = [...modifiedFiles][0];
|
|
52
|
+
const dir = resolve(firstFile, "..");
|
|
53
|
+
try {
|
|
54
|
+
const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: dir, timeout: 5000 });
|
|
55
|
+
if (result.code === 0 && result.stdout.trim()) return result.stdout.trim();
|
|
56
|
+
} catch {}
|
|
57
|
+
return fallbackCwd;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function selectOption(ctx: any, question: string, options: string[]): Promise<string | undefined> {
|
|
61
|
+
const result = await askUser(ctx, {
|
|
62
|
+
question,
|
|
63
|
+
options,
|
|
64
|
+
allowFreeform: false,
|
|
65
|
+
allowComment: false,
|
|
66
|
+
allowMultiple: false,
|
|
67
|
+
});
|
|
68
|
+
if (!result || result.kind !== "selection") return undefined;
|
|
69
|
+
return result.selections[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setStep(orchestrator: Orchestrator, step: string): void {
|
|
73
|
+
if (!orchestrator.active) return;
|
|
74
|
+
orchestrator.active.state.step = step;
|
|
75
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tryCompleteReviewCycle(orchestrator: Orchestrator): void {
|
|
79
|
+
if (
|
|
80
|
+
!orchestrator.active?.state.reviewCycle ||
|
|
81
|
+
orchestrator.active.state.reviewCycle.step !== "await_reviewers" ||
|
|
82
|
+
orchestrator.spawnedAgentIds.size > 0 ||
|
|
83
|
+
orchestrator.pendingSubagentSpawns > 0
|
|
84
|
+
) return;
|
|
85
|
+
|
|
86
|
+
if (orchestrator.reviewTransitionToken === orchestrator.activeTaskToken) return;
|
|
87
|
+
|
|
88
|
+
const cycle = orchestrator.active.state.reviewCycle;
|
|
89
|
+
const phase = orchestrator.active.state.phase;
|
|
90
|
+
const outputs = loadPhaseReviewOutputs(orchestrator.active.dir, phase, cycle.pass);
|
|
91
|
+
const pi = orchestrator.pi;
|
|
92
|
+
|
|
93
|
+
orchestrator.reviewTransitionToken = orchestrator.activeTaskToken;
|
|
94
|
+
cycle.step = "apply_feedback";
|
|
95
|
+
orchestrator.active.state.step = "apply_feedback";
|
|
96
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
97
|
+
|
|
98
|
+
const rendered = outputs.length
|
|
99
|
+
? outputs.map((o) => `=== ${o.name} ===\n${o.content}`).join("\n\n")
|
|
100
|
+
: "No reviewer outputs found. Review the implementation yourself and decide whether to approve or request changes.";
|
|
101
|
+
|
|
102
|
+
pi.sendMessage(
|
|
103
|
+
{
|
|
104
|
+
customType: "pp-review-ready",
|
|
105
|
+
content: `[PI-PI] Reviewer outputs are ready.\n\n${rendered}`,
|
|
106
|
+
display: false,
|
|
107
|
+
},
|
|
108
|
+
{ deliverAs: "followUp" },
|
|
109
|
+
);
|
|
110
|
+
pi.sendUserMessage("[PI-PI] Review cycle is ready for apply_feedback. Read reviewer outputs and proceed.", { deliverAs: "followUp" });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function enterReviewCycle(orchestrator: Orchestrator, ctx: any, kind: "auto" | "auto-deep" | "plannotator") {
|
|
114
|
+
if (!orchestrator.active) return "No active task.";
|
|
115
|
+
const pi = orchestrator.pi;
|
|
116
|
+
const pass = orchestrator.active.state.reviewPass + 1;
|
|
117
|
+
orchestrator.active.state.reviewCycle = { kind, step: "spawn_reviewers", pass };
|
|
118
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
119
|
+
|
|
120
|
+
if (kind === "plannotator") {
|
|
121
|
+
const phase = orchestrator.active.state.phase;
|
|
122
|
+
if (phase === "brainstorm") {
|
|
123
|
+
orchestrator.active.state.reviewCycle = null;
|
|
124
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
125
|
+
return "Plannotator is only available for plan and implement phases. Choose another option.";
|
|
126
|
+
}
|
|
127
|
+
if (phase === "plan") {
|
|
128
|
+
const planContent = getLatestSynthesizedPlan(orchestrator.active.dir);
|
|
129
|
+
if (!planContent) {
|
|
130
|
+
orchestrator.active.state.reviewCycle = null;
|
|
131
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
132
|
+
return "No synthesized plan found. Choose another option.";
|
|
133
|
+
}
|
|
134
|
+
const payload = { planContent, planFilePath: join(orchestrator.active.dir, "plans") };
|
|
135
|
+
|
|
136
|
+
const { opened, reviewId } = await openPlannotator(pi, "plan-review", payload);
|
|
137
|
+
if (!opened) {
|
|
138
|
+
orchestrator.active.state.reviewCycle = null;
|
|
139
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
140
|
+
return "Plannotator is not available. Choose another option.";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let result: { approved: boolean; feedback?: string };
|
|
144
|
+
ctx.ui?.setWorkingMessage?.("Waiting for Plannotator plan review…");
|
|
145
|
+
try {
|
|
146
|
+
result = await waitForPlannotatorResult(orchestrator, reviewId);
|
|
147
|
+
} catch {
|
|
148
|
+
orchestrator.active.state.reviewCycle = null;
|
|
149
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
150
|
+
return "Plannotator review cancelled. Choose another option.";
|
|
151
|
+
} finally {
|
|
152
|
+
ctx.ui?.setWorkingMessage?.();
|
|
153
|
+
}
|
|
154
|
+
orchestrator.active.state.reviewCycle = null;
|
|
155
|
+
if (result.approved) {
|
|
156
|
+
orchestrator.active.state.reviewPass += 1;
|
|
157
|
+
orchestrator.active.reviewPass = orchestrator.active.state.reviewPass;
|
|
158
|
+
orchestrator.active.state.step = "user_gate";
|
|
159
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
160
|
+
return "Plannotator approved the plan. Choose next action.";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
orchestrator.active.state.step = "synthesize";
|
|
164
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
165
|
+
const feedback = result.feedback ? `\n\nFeedback:\n${result.feedback}` : "";
|
|
166
|
+
return `Plannotator requested changes.${feedback}\n\nUser wants to continue. Run /pp when ready to advance.`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
orchestrator.active.state.reviewCycle = null;
|
|
170
|
+
orchestrator.active.state.step = "llm_work";
|
|
171
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
172
|
+
return "User wants a Plannotator code review. Call pp_specify_reviews with the list of repositories and commit ranges to review. Include all repos where you made changes.";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const phase = orchestrator.active.state.phase;
|
|
176
|
+
const config = kind === "auto-deep" ? deepReviewConfig(orchestrator.config) : orchestrator.config;
|
|
177
|
+
const reviewers = phase === "brainstorm"
|
|
178
|
+
? config.brainstormReviewers
|
|
179
|
+
: phase === "plan"
|
|
180
|
+
? config.planReviewers
|
|
181
|
+
: config.codeReviewers;
|
|
182
|
+
const enabledCount = Object.values(reviewers).filter((v) => v.enabled).length;
|
|
183
|
+
if (enabledCount === 0) {
|
|
184
|
+
orchestrator.active.state.reviewCycle = null;
|
|
185
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
186
|
+
const label = phase === "brainstorm" ? "brainstorm" : phase === "plan" ? "plan" : "code";
|
|
187
|
+
return `No ${label} reviewers enabled. Choose another option.`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
orchestrator.reviewTransitionToken = -1;
|
|
191
|
+
orchestrator.pendingSubagentSpawns = enabledCount;
|
|
192
|
+
const spawnFn = phase === "brainstorm"
|
|
193
|
+
? () => spawnBrainstormReviewers(pi, orchestrator.cwd, orchestrator.active!.dir, orchestrator.active!.taskId, config, pass)
|
|
194
|
+
: phase === "plan"
|
|
195
|
+
? () => spawnPlanReviewers(pi, orchestrator.cwd, orchestrator.active!.dir, orchestrator.active!.taskId, config)
|
|
196
|
+
: () => spawnCodeReviewers(pi, orchestrator.cwd, orchestrator.active!.dir, orchestrator.active!.taskId, config, pass);
|
|
197
|
+
spawnFn().then((result) => {
|
|
198
|
+
orchestrator.failedReviewerVariants = result.failedVariants;
|
|
199
|
+
if (result.spawned === 0) orchestrator.pendingSubagentSpawns = 0;
|
|
200
|
+
for (const id of result.agentIds ?? []) {
|
|
201
|
+
orchestrator.spawnedAgentIds.delete(id);
|
|
202
|
+
}
|
|
203
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
204
|
+
tryCompleteReviewCycle(orchestrator);
|
|
205
|
+
}).catch((err) => {
|
|
206
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
207
|
+
tryCompleteReviewCycle(orchestrator);
|
|
208
|
+
console.error(`[pi-pi] spawn reviewers failed (${phase}): ${err.message}`);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
orchestrator.active.state.reviewCycle.step = "await_reviewers";
|
|
212
|
+
orchestrator.active.state.step = "await_reviewers";
|
|
213
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
214
|
+
return `Started review cycle pass ${pass} (${kind}). Awaiting reviewers.`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function stopTask(orchestrator: Orchestrator): string {
|
|
218
|
+
if (!orchestrator.active) return "No active task.";
|
|
219
|
+
orchestrator.abortAllSubagents();
|
|
220
|
+
orchestrator.active.state.phase = "done";
|
|
221
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
222
|
+
const desc = orchestrator.active.description;
|
|
223
|
+
orchestrator.cleanupActive();
|
|
224
|
+
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
225
|
+
taskStore?.clearAll?.();
|
|
226
|
+
return `Task "${desc}" stopped.`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function finalizeReviewCycle(task: ActiveTask): void {
|
|
230
|
+
if (!task.state.reviewCycle) return;
|
|
231
|
+
const kind = task.state.reviewCycle.kind;
|
|
232
|
+
task.state.reviewPass = task.state.reviewCycle.pass;
|
|
233
|
+
task.reviewPass = task.state.reviewPass;
|
|
234
|
+
if (!task.state.reviewPassByKind) task.state.reviewPassByKind = {};
|
|
235
|
+
task.state.reviewPassByKind[kind] = (task.state.reviewPassByKind[kind] ?? 0) + 1;
|
|
236
|
+
task.state.reviewCycle = null;
|
|
237
|
+
task.state.step = "user_gate";
|
|
238
|
+
saveTask(task.dir, task.state);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function runUserGateDialog(orchestrator: Orchestrator, ctx: any, summary: string): Promise<string> {
|
|
242
|
+
if (!orchestrator.active) return "No active task.";
|
|
243
|
+
if (orchestrator.userGatePending) return "A user-gate dialog is already in progress.";
|
|
244
|
+
orchestrator.userGatePending = true;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
return await runUserGateDialogInner(orchestrator, ctx, summary);
|
|
248
|
+
} finally {
|
|
249
|
+
orchestrator.userGatePending = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function runUserGateDialogInner(orchestrator: Orchestrator, ctx: any, summary: string): Promise<string> {
|
|
254
|
+
if (!orchestrator.active) return "No active task.";
|
|
255
|
+
const pi = orchestrator.pi;
|
|
256
|
+
const phase = orchestrator.active.state.phase;
|
|
257
|
+
const task = orchestrator.active;
|
|
258
|
+
|
|
259
|
+
if (orchestrator.spawnedAgentIds.size > 0 || orchestrator.pendingSubagentSpawns > 0) {
|
|
260
|
+
const count = orchestrator.spawnedAgentIds.size + orchestrator.pendingSubagentSpawns;
|
|
261
|
+
return `${count} subagent(s) still running or spawning.`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const byKind = task.state.reviewPassByKind ?? {};
|
|
265
|
+
const autoCount = byKind["auto"] ?? 0;
|
|
266
|
+
const deepCount = byKind["auto-deep"] ?? 0;
|
|
267
|
+
const autoLabel = autoCount > 0 ? `Review (pass ${autoCount + 1})` : "Review";
|
|
268
|
+
const deepLabel = deepCount > 0 ? `Deep review (pass ${deepCount + 1})` : "Deep review";
|
|
269
|
+
|
|
270
|
+
const continueMessage = "User wants to continue. Run /pp when ready to advance.";
|
|
271
|
+
|
|
272
|
+
if (phase === "brainstorm" && task.type === "implement") {
|
|
273
|
+
const choice = await selectOption(ctx, summary, ["Approve brainstorm", autoLabel, deepLabel, "Continue brainstorming", "Stop task"]);
|
|
274
|
+
finalizeReviewCycle(task);
|
|
275
|
+
if (choice === "Approve brainstorm") {
|
|
276
|
+
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
277
|
+
return result.ok ? "Brainstorm approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
278
|
+
}
|
|
279
|
+
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
280
|
+
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
281
|
+
if (choice === "Stop task") return stopTask(orchestrator);
|
|
282
|
+
setStep(orchestrator, "llm_work");
|
|
283
|
+
return continueMessage;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (phase === "brainstorm" && task.type === "brainstorm") {
|
|
287
|
+
const choice = await selectOption(ctx, summary, ["Approve brainstorm", autoLabel, deepLabel, "Continue brainstorming", "Finish brainstorming", "Stop task"]);
|
|
288
|
+
finalizeReviewCycle(task);
|
|
289
|
+
if (choice === "Approve brainstorm") {
|
|
290
|
+
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
291
|
+
return result.ok ? "Brainstorm approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
292
|
+
}
|
|
293
|
+
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
294
|
+
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
295
|
+
if (choice === "Finish brainstorming" || choice === "Stop task") return stopTask(orchestrator);
|
|
296
|
+
setStep(orchestrator, "llm_work");
|
|
297
|
+
return continueMessage;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (phase === "debug") {
|
|
301
|
+
const choice = await selectOption(ctx, summary, ["Approve debug", autoLabel, deepLabel, "Continue debugging", "Finish debugging", "Stop task"]);
|
|
302
|
+
finalizeReviewCycle(task);
|
|
303
|
+
if (choice === "Approve debug") {
|
|
304
|
+
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
305
|
+
return result.ok ? "Debug approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
306
|
+
}
|
|
307
|
+
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
308
|
+
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
309
|
+
if (choice === "Finish debugging" || choice === "Stop task") return stopTask(orchestrator);
|
|
310
|
+
setStep(orchestrator, "llm_work");
|
|
311
|
+
return continueMessage;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (phase === "plan") {
|
|
315
|
+
const choice = await selectOption(ctx, summary, [
|
|
316
|
+
"Approve plan",
|
|
317
|
+
autoLabel,
|
|
318
|
+
deepLabel,
|
|
319
|
+
"Review in Plannotator",
|
|
320
|
+
"Review on my own",
|
|
321
|
+
"Continue planning",
|
|
322
|
+
"Stop task",
|
|
323
|
+
]);
|
|
324
|
+
finalizeReviewCycle(task);
|
|
325
|
+
if (choice === "Approve plan") {
|
|
326
|
+
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
327
|
+
return result.ok ? "Plan approved. Transitioned to implement." : `Transition blocked: ${result.error}`;
|
|
328
|
+
}
|
|
329
|
+
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
330
|
+
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
331
|
+
if (choice === "Review in Plannotator") return enterReviewCycle(orchestrator, ctx, "plannotator");
|
|
332
|
+
if (choice === "Stop task") return stopTask(orchestrator);
|
|
333
|
+
setStep(orchestrator, "synthesize");
|
|
334
|
+
return continueMessage;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (phase === "implement") {
|
|
338
|
+
const choice = await selectOption(ctx, summary, [
|
|
339
|
+
"Approve implementation",
|
|
340
|
+
autoLabel,
|
|
341
|
+
deepLabel,
|
|
342
|
+
"Review in Plannotator",
|
|
343
|
+
"Review on my own",
|
|
344
|
+
"Continue implementation",
|
|
345
|
+
"Stop task",
|
|
346
|
+
]);
|
|
347
|
+
finalizeReviewCycle(task);
|
|
348
|
+
if (choice === "Approve implementation") {
|
|
349
|
+
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
350
|
+
return result.ok ? "Implementation approved. Task completed." : `Transition blocked: ${result.error}`;
|
|
351
|
+
}
|
|
352
|
+
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
353
|
+
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
354
|
+
if (choice === "Review in Plannotator") return enterReviewCycle(orchestrator, ctx, "plannotator");
|
|
355
|
+
if (choice === "Stop task") return stopTask(orchestrator);
|
|
356
|
+
setStep(orchestrator, "llm_work");
|
|
357
|
+
return continueMessage;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return "No dialog available for this phase.";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function registerOrchestratorTools(orchestrator: Orchestrator): void {
|
|
364
|
+
registerPhaseCompleteTool(orchestrator);
|
|
365
|
+
registerCommitTool(orchestrator);
|
|
366
|
+
registerSpecifyReviewsTool(orchestrator);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function openCodeReviewDirect(
|
|
370
|
+
pi: ExtensionAPI,
|
|
371
|
+
payload: Record<string, unknown>,
|
|
372
|
+
): Promise<{ approved: boolean; feedback?: string } | { error: string }> {
|
|
373
|
+
return new Promise((resolve) => {
|
|
374
|
+
pi.events.emit("plannotator:request", {
|
|
375
|
+
requestId: crypto.randomUUID(),
|
|
376
|
+
action: "code-review",
|
|
377
|
+
payload,
|
|
378
|
+
respond: (response: any) => {
|
|
379
|
+
if (response.status === "handled") {
|
|
380
|
+
resolve({ approved: !!response.result?.approved, feedback: response.result?.feedback });
|
|
381
|
+
} else {
|
|
382
|
+
resolve({ error: response.error || "Plannotator not available" });
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
390
|
+
const pi = orchestrator.pi;
|
|
391
|
+
|
|
392
|
+
pi.registerTool({
|
|
393
|
+
name: "pp_specify_reviews",
|
|
394
|
+
label: "pi-pi",
|
|
395
|
+
description:
|
|
396
|
+
"Specify which repositories and commit ranges to open in Plannotator for code review. " +
|
|
397
|
+
"Called when the user requests a Plannotator code review. " +
|
|
398
|
+
"Plannotator will open sequentially for each entry. Results are returned after all reviews complete.",
|
|
399
|
+
parameters: Type.Object({
|
|
400
|
+
reviews: Type.Array(Type.Object({
|
|
401
|
+
cwd: Type.String({ description: "Absolute path to the git repository" }),
|
|
402
|
+
range: Type.String({ description: "Git commit range, e.g. 'origin/main..HEAD', 'abc123..def456', 'HEAD~3..HEAD'" }),
|
|
403
|
+
})),
|
|
404
|
+
}),
|
|
405
|
+
async execute(_toolCallId, params: any, _signal, _onUpdate, ctx) {
|
|
406
|
+
if (!params.reviews || params.reviews.length === 0) {
|
|
407
|
+
return { content: [{ type: "text" as const, text: "No reviews specified." }], isError: true as const, details: {} };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const results: string[] = [];
|
|
411
|
+
let hasNeedsChanges = false;
|
|
412
|
+
for (const review of params.reviews) {
|
|
413
|
+
ctx.ui?.setWorkingMessage?.(`Waiting for Plannotator review: ${review.range}…`);
|
|
414
|
+
const result = await openCodeReviewDirect(pi, {
|
|
415
|
+
cwd: review.cwd,
|
|
416
|
+
diffType: `range:${review.range}`,
|
|
417
|
+
});
|
|
418
|
+
if ("error" in result) {
|
|
419
|
+
results.push(`${review.cwd} (${review.range}): ${result.error}`);
|
|
420
|
+
} else {
|
|
421
|
+
const status = result.approved ? "APPROVED" : "NEEDS_CHANGES";
|
|
422
|
+
if (!result.approved) hasNeedsChanges = true;
|
|
423
|
+
const feedback = result.feedback ? `\nFeedback: ${result.feedback}` : "";
|
|
424
|
+
results.push(`${review.cwd} (${review.range}): ${status}${feedback}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
ctx.ui?.setWorkingMessage?.();
|
|
428
|
+
|
|
429
|
+
const summary = results.join("\n\n");
|
|
430
|
+
|
|
431
|
+
if (hasNeedsChanges) {
|
|
432
|
+
if (orchestrator.active) {
|
|
433
|
+
orchestrator.active.state.step = "llm_work";
|
|
434
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
content: [{ type: "text" as const, text: `Plannotator review complete.\n\n${summary}\n\nFix the issues noted above, then call pp_phase_complete when done.` }],
|
|
438
|
+
details: {},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
ctx.ui?.setWorkingMessage?.("Waiting for user input…");
|
|
443
|
+
try {
|
|
444
|
+
const gateResult = await runUserGateDialog(orchestrator, ctx, `Plannotator review complete.\n\n${summary}`);
|
|
445
|
+
return { content: [{ type: "text" as const, text: gateResult }], details: {} };
|
|
446
|
+
} finally {
|
|
447
|
+
ctx.ui?.setWorkingMessage?.();
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function loadPhaseReviewOutputs(taskDir: string, phase: string, pass: number): { name: string; content: string }[] {
|
|
454
|
+
if (phase === "brainstorm") return loadBrainstormReviewOutputs(taskDir, pass);
|
|
455
|
+
if (phase === "plan") return loadPlanReviewOutputs(taskDir);
|
|
456
|
+
return loadCodeReviewOutputs(taskDir, pass);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function registerCommitTool(orchestrator: Orchestrator): void {
|
|
460
|
+
const pi = orchestrator.pi;
|
|
461
|
+
|
|
462
|
+
pi.registerTool({
|
|
463
|
+
name: "pp_commit",
|
|
464
|
+
label: "pi-pi",
|
|
465
|
+
description:
|
|
466
|
+
"Commit modified files with a descriptive message. Call after completing a logical " +
|
|
467
|
+
"unit of work (e.g. implementing one plan item, fixing a bug, adding a test). " +
|
|
468
|
+
"The message should describe WHAT changed and WHY, not list files.",
|
|
469
|
+
parameters: Type.Object({
|
|
470
|
+
message: Type.String({ description: "Commit message describing the change (max 72 chars for first line)" }),
|
|
471
|
+
}),
|
|
472
|
+
async execute(_toolCallId, params: any) {
|
|
473
|
+
if (!orchestrator.active) {
|
|
474
|
+
return { content: [{ type: "text" as const, text: "No active task." }], isError: true as const, details: {} };
|
|
475
|
+
}
|
|
476
|
+
if (!orchestrator.config.autoCommit) {
|
|
477
|
+
return { content: [{ type: "text" as const, text: "autoCommit is disabled in config." }], details: {} };
|
|
478
|
+
}
|
|
479
|
+
if (orchestrator.active.modifiedFiles.size === 0) {
|
|
480
|
+
return { content: [{ type: "text" as const, text: "No modified files to commit." }], details: {} };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const files = [...orchestrator.active.modifiedFiles];
|
|
484
|
+
const result = autoCommit(files, params.message, orchestrator.cwd);
|
|
485
|
+
if (result.ok) {
|
|
486
|
+
orchestrator.active.modifiedFiles.clear();
|
|
487
|
+
orchestrator.active.state.modifiedFiles = [];
|
|
488
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
489
|
+
orchestrator.commitReminderSent = false;
|
|
490
|
+
return { content: [{ type: "text" as const, text: `Committed ${files.length} file(s): ${result.commitHash ?? "ok"}` }], details: {} };
|
|
491
|
+
}
|
|
492
|
+
return { content: [{ type: "text" as const, text: `Commit failed: ${result.error}` }], isError: true as const, details: {} };
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
|
|
498
|
+
const pi = orchestrator.pi;
|
|
499
|
+
|
|
500
|
+
pi.registerTool({
|
|
501
|
+
name: "pp_phase_complete",
|
|
502
|
+
label: "pi-pi",
|
|
503
|
+
description:
|
|
504
|
+
"Call when the current phase is complete. Shows a dialog to the user for " +
|
|
505
|
+
"approval. The user's choice is returned — act on it accordingly.",
|
|
506
|
+
parameters: Type.Object({
|
|
507
|
+
summary: Type.String({ description: "Brief summary of what was accomplished in this phase" }),
|
|
508
|
+
}),
|
|
509
|
+
async execute(_toolCallId, params: any, _signal, _onUpdate, ctx) {
|
|
510
|
+
if (!orchestrator.active) {
|
|
511
|
+
return { content: [{ type: "text" as const, text: "No active task." }], isError: true as const, details: {} };
|
|
512
|
+
}
|
|
513
|
+
const step = orchestrator.active.state.step;
|
|
514
|
+
if (step === "await_planners" || step === "await_reviewers") {
|
|
515
|
+
return { content: [{ type: "text" as const, text: "Subagents are still running. Wait for them to complete before calling pp_phase_complete." }], isError: true as const, details: {} };
|
|
516
|
+
}
|
|
517
|
+
if (orchestrator.spawnedAgentIds.size > 0 || orchestrator.pendingSubagentSpawns > 0) {
|
|
518
|
+
const count = orchestrator.spawnedAgentIds.size + orchestrator.pendingSubagentSpawns;
|
|
519
|
+
return { content: [{ type: "text" as const, text: `${count} subagent(s) still running. Wait for them to complete before calling pp_phase_complete.` }], isError: true as const, details: {} };
|
|
520
|
+
}
|
|
521
|
+
ctx.ui.setWorkingMessage?.("Waiting for user approval…");
|
|
522
|
+
try {
|
|
523
|
+
const { showActiveTaskMenu } = await import("./pp-menu.js");
|
|
524
|
+
const text = await showActiveTaskMenu(orchestrator, ctx, params.summary, "tool");
|
|
525
|
+
if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
|
|
526
|
+
return { content: [{ type: "text" as const, text: "Phase transition in progress." }], details: {} };
|
|
527
|
+
}
|
|
528
|
+
if (!text) {
|
|
529
|
+
return { content: [{ type: "text" as const, text: "No action selected." }], details: {} };
|
|
530
|
+
}
|
|
531
|
+
return { content: [{ type: "text" as const, text }], details: {} };
|
|
532
|
+
} finally {
|
|
533
|
+
ctx.ui.setWorkingMessage?.();
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
540
|
+
const pi = orchestrator.pi;
|
|
541
|
+
|
|
542
|
+
function trackSubagentEvent(data: any, event: "created" | "started" | "first_tool" | "first_turn" | "completed" | "failed"): void {
|
|
543
|
+
if (!orchestrator.active || !data?.id) return;
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
const lifecycle = orchestrator.agentLifecycle.get(data.id) ?? {};
|
|
546
|
+
if (data.type && lifecycle.type == null) lifecycle.type = data.type;
|
|
547
|
+
if (data.description && lifecycle.description == null) lifecycle.description = data.description;
|
|
548
|
+
lifecycle.phase = orchestrator.active.state.phase;
|
|
549
|
+
lifecycle.step = orchestrator.active.state.step ?? undefined;
|
|
550
|
+
lifecycle.lastEventAt = now;
|
|
551
|
+
if (event === "created" && lifecycle.createdAt == null) lifecycle.createdAt = now;
|
|
552
|
+
if (event === "started" && lifecycle.startedAt == null) lifecycle.startedAt = now;
|
|
553
|
+
if (event === "first_tool" && lifecycle.firstToolAt == null) lifecycle.firstToolAt = now;
|
|
554
|
+
if (event === "first_turn" && lifecycle.firstTurnAt == null) lifecycle.firstTurnAt = now;
|
|
555
|
+
orchestrator.agentLifecycle.set(data.id, lifecycle);
|
|
556
|
+
|
|
557
|
+
const ageMs = lifecycle.createdAt == null ? 0 : now - lifecycle.createdAt;
|
|
558
|
+
const startedDeltaMs = lifecycle.startedAt == null || lifecycle.createdAt == null ? undefined : lifecycle.startedAt - lifecycle.createdAt;
|
|
559
|
+
const firstToolDeltaMs = lifecycle.firstToolAt == null || lifecycle.createdAt == null ? undefined : lifecycle.firstToolAt - lifecycle.createdAt;
|
|
560
|
+
const firstTurnDeltaMs = lifecycle.firstTurnAt == null || lifecycle.createdAt == null ? undefined : lifecycle.firstTurnAt - lifecycle.createdAt;
|
|
561
|
+
const pending = orchestrator.pendingSubagentSpawns;
|
|
562
|
+
const running = orchestrator.spawnedAgentIds.size;
|
|
563
|
+
const desc = data.description || lifecycle.description || data.type || lifecycle.type || data.id;
|
|
564
|
+
appendTaskLog(orchestrator.active.dir, "subagent-lifecycle.jsonl", {
|
|
565
|
+
timestamp: new Date(now).toISOString(),
|
|
566
|
+
id: data.id,
|
|
567
|
+
event,
|
|
568
|
+
description: desc,
|
|
569
|
+
type: data.type ?? lifecycle.type ?? null,
|
|
570
|
+
phase: lifecycle.phase ?? null,
|
|
571
|
+
step: lifecycle.step ?? null,
|
|
572
|
+
running,
|
|
573
|
+
pending,
|
|
574
|
+
ageMs,
|
|
575
|
+
createdToStartedMs: startedDeltaMs ?? null,
|
|
576
|
+
createdToFirstToolMs: firstToolDeltaMs ?? null,
|
|
577
|
+
createdToFirstTurnMs: firstTurnDeltaMs ?? null,
|
|
578
|
+
toolName: data.toolName ?? null,
|
|
579
|
+
turnCount: data.turnCount ?? null,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function startStaleAgentWatchdog(): void {
|
|
584
|
+
if (orchestrator.staleAgentTimer) return;
|
|
585
|
+
orchestrator.staleAgentTimer = setInterval(() => {
|
|
586
|
+
if (!orchestrator.active || orchestrator.agentSpawnTimes.size === 0) {
|
|
587
|
+
clearInterval(orchestrator.staleAgentTimer!);
|
|
588
|
+
orchestrator.staleAgentTimer = null;
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const mgr = (globalThis as any)[Symbol.for("pi-subagents:manager")];
|
|
592
|
+
const now = Date.now();
|
|
593
|
+
const staleMs = orchestrator.config.timeouts.agentStale;
|
|
594
|
+
for (const [id, spawnTime] of orchestrator.agentSpawnTimes) {
|
|
595
|
+
const record = mgr?.getRecord?.(id);
|
|
596
|
+
if (record?.status === "running" || record?.status === "queued") continue;
|
|
597
|
+
const lastActivity = orchestrator.agentLifecycle.get(id)?.lastEventAt ?? spawnTime;
|
|
598
|
+
if (now - lastActivity > staleMs) {
|
|
599
|
+
const desc = orchestrator.agentDescriptions.get(id) || id;
|
|
600
|
+
pi.events.emit("subagents:rpc:stop", { requestId: crypto.randomUUID(), agentId: id });
|
|
601
|
+
orchestrator.spawnedAgentIds.delete(id);
|
|
602
|
+
orchestrator.agentSpawnTimes.delete(id);
|
|
603
|
+
orchestrator.agentDescriptions.delete(id);
|
|
604
|
+
orchestrator.agentLifecycle.delete(id);
|
|
605
|
+
pi.sendMessage(
|
|
606
|
+
{
|
|
607
|
+
customType: "pp-agent-stale",
|
|
608
|
+
content: `Aborted stale agent "${desc}" — no completion after ${Math.round(staleMs / 1000)}s.`,
|
|
609
|
+
display: true,
|
|
610
|
+
},
|
|
611
|
+
{ deliverAs: "steer" },
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (orchestrator.agentSpawnTimes.size === 0) {
|
|
616
|
+
clearInterval(orchestrator.staleAgentTimer!);
|
|
617
|
+
orchestrator.staleAgentTimer = null;
|
|
618
|
+
}
|
|
619
|
+
}, 30000);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
pi.events.on("subagents:created", (data: any) => {
|
|
623
|
+
if (!orchestrator.active || !data?.id) return;
|
|
624
|
+
orchestrator.spawnedAgentIds.add(data.id);
|
|
625
|
+
orchestrator.agentSpawnTimes.set(data.id, Date.now());
|
|
626
|
+
if (orchestrator.pendingSubagentSpawns > 0) orchestrator.pendingSubagentSpawns--;
|
|
627
|
+
if (data.description) {
|
|
628
|
+
orchestrator.agentDescriptions.set(data.id, data.description);
|
|
629
|
+
}
|
|
630
|
+
trackSubagentEvent(data, "created");
|
|
631
|
+
startStaleAgentWatchdog();
|
|
632
|
+
const mgr = (globalThis as any)[Symbol.for("pi-subagents:manager")];
|
|
633
|
+
mgr?.refreshWidget?.(orchestrator.lastCtx?.ui);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
pi.events.on("subagents:started", (data: any) => {
|
|
637
|
+
if (!orchestrator.active || !data?.id) return;
|
|
638
|
+
trackSubagentEvent(data, "started");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
pi.events.on("subagents:first_tool", (data: any) => {
|
|
642
|
+
if (!orchestrator.active || !data?.id) return;
|
|
643
|
+
trackSubagentEvent(data, "first_tool");
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
pi.events.on("subagents:first_turn", (data: any) => {
|
|
647
|
+
if (!orchestrator.active || !data?.id) return;
|
|
648
|
+
trackSubagentEvent(data, "first_turn");
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
function checkPlannerCompletion(): void {
|
|
652
|
+
if (
|
|
653
|
+
!orchestrator.active ||
|
|
654
|
+
orchestrator.active.state.phase !== "plan" ||
|
|
655
|
+
orchestrator.active.state.step !== "await_planners" ||
|
|
656
|
+
orchestrator.spawnedAgentIds.size > 0 ||
|
|
657
|
+
orchestrator.pendingSubagentSpawns > 0
|
|
658
|
+
) return;
|
|
659
|
+
|
|
660
|
+
const plansDir = join(orchestrator.active.dir, "plans");
|
|
661
|
+
const hasPlanFiles = existsSync(plansDir) &&
|
|
662
|
+
readdirSync(plansDir).some((f) => f.endsWith(".md") && !f.includes("synthesized") && !f.includes("review_"));
|
|
663
|
+
|
|
664
|
+
const failedPlannerVariants = [...orchestrator.failedPlannerVariants];
|
|
665
|
+
if (
|
|
666
|
+
failedPlannerVariants.length > 0 &&
|
|
667
|
+
!orchestrator.plannerFailureDialogPending &&
|
|
668
|
+
orchestrator.lastCtx
|
|
669
|
+
) {
|
|
670
|
+
orchestrator.plannerFailureDialogPending = true;
|
|
671
|
+
void (async () => {
|
|
672
|
+
try {
|
|
673
|
+
const variantsText = failedPlannerVariants.join(", ");
|
|
674
|
+
const question = hasPlanFiles
|
|
675
|
+
? `Some planners failed: ${variantsText}. Choose how to proceed.`
|
|
676
|
+
: `All planner outputs failed: ${variantsText}. Choose how to proceed.`;
|
|
677
|
+
const options = hasPlanFiles
|
|
678
|
+
? ["Retry failed planners", "Work with available planner outputs", "Stop task"]
|
|
679
|
+
: ["Retry failed planners", "Stop task"];
|
|
680
|
+
const choice = await selectOption(orchestrator.lastCtx, question, options);
|
|
681
|
+
|
|
682
|
+
if (choice === "Retry failed planners") {
|
|
683
|
+
const failedSet = new Set(failedPlannerVariants);
|
|
684
|
+
const planners: typeof orchestrator.config.planners = {};
|
|
685
|
+
for (const [name, cfg] of Object.entries(orchestrator.config.planners)) {
|
|
686
|
+
if (failedSet.has(name)) planners[name] = cfg;
|
|
687
|
+
}
|
|
688
|
+
const retryCount = Object.keys(planners).length;
|
|
689
|
+
if (retryCount > 0) {
|
|
690
|
+
const retryConfig = { ...orchestrator.config, planners };
|
|
691
|
+
orchestrator.failedPlannerVariants = [];
|
|
692
|
+
orchestrator.pendingSubagentSpawns = retryCount;
|
|
693
|
+
spawnPlanners(pi, orchestrator.cwd, orchestrator.active!.dir, orchestrator.active!.taskId, retryConfig).then((result) => {
|
|
694
|
+
orchestrator.failedPlannerVariants = result.failedVariants;
|
|
695
|
+
if (result.spawned === 0) orchestrator.pendingSubagentSpawns = 0;
|
|
696
|
+
for (const id of result.agentIds ?? []) {
|
|
697
|
+
orchestrator.spawnedAgentIds.delete(id);
|
|
698
|
+
}
|
|
699
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
700
|
+
checkPlannerCompletion();
|
|
701
|
+
}).catch((err) => {
|
|
702
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
703
|
+
console.error(`[pi-pi] retry spawnPlanners failed: ${err.message}`);
|
|
704
|
+
checkPlannerCompletion();
|
|
705
|
+
});
|
|
706
|
+
pi.sendUserMessage(`[PI-PI] Retrying failed planners: ${variantsText}.`, { deliverAs: "followUp" });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (choice === "Stop task") {
|
|
712
|
+
pi.sendUserMessage(`[PI-PI] ${stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (hasPlanFiles) {
|
|
717
|
+
orchestrator.failedPlannerVariants = [];
|
|
718
|
+
}
|
|
719
|
+
} finally {
|
|
720
|
+
orchestrator.plannerFailureDialogPending = false;
|
|
721
|
+
checkPlannerCompletion();
|
|
722
|
+
}
|
|
723
|
+
})();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (!hasPlanFiles) {
|
|
728
|
+
pi.sendMessage(
|
|
729
|
+
{
|
|
730
|
+
customType: "pp-planners-error",
|
|
731
|
+
content: "All planner subagents finished but no plan files were produced. You must create the plan yourself based on USER_REQUEST.md and RESEARCH.md.",
|
|
732
|
+
display: true,
|
|
733
|
+
},
|
|
734
|
+
{ deliverAs: "followUp" },
|
|
735
|
+
);
|
|
736
|
+
orchestrator.active.state.step = "synthesize";
|
|
737
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
orchestrator.failedPlannerVariants = [];
|
|
742
|
+
orchestrator.active.state.step = "synthesize";
|
|
743
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
744
|
+
pi.sendUserMessage("[PI-PI] All planners completed. Read their outputs and synthesize the plan.", { deliverAs: "followUp" });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function checkReviewCycleCompletion(): void {
|
|
748
|
+
if (
|
|
749
|
+
!orchestrator.active?.state.reviewCycle ||
|
|
750
|
+
orchestrator.active.state.reviewCycle.step !== "await_reviewers" ||
|
|
751
|
+
orchestrator.spawnedAgentIds.size > 0 ||
|
|
752
|
+
orchestrator.pendingSubagentSpawns > 0
|
|
753
|
+
) return;
|
|
754
|
+
|
|
755
|
+
const failedReviewerVariants = [...orchestrator.failedReviewerVariants];
|
|
756
|
+
if (
|
|
757
|
+
failedReviewerVariants.length > 0 &&
|
|
758
|
+
!orchestrator.reviewerFailureDialogPending &&
|
|
759
|
+
orchestrator.lastCtx
|
|
760
|
+
) {
|
|
761
|
+
orchestrator.reviewerFailureDialogPending = true;
|
|
762
|
+
void (async () => {
|
|
763
|
+
try {
|
|
764
|
+
const variantsText = failedReviewerVariants.join(", ");
|
|
765
|
+
const choice = await selectOption(
|
|
766
|
+
orchestrator.lastCtx,
|
|
767
|
+
`Some reviewers failed: ${variantsText}. Choose how to proceed.`,
|
|
768
|
+
["Retry failed reviewers", "Work with available reviewer outputs", "Continue without review", "Stop task"],
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
if (choice === "Retry failed reviewers") {
|
|
772
|
+
const cycle = orchestrator.active?.state.reviewCycle;
|
|
773
|
+
if (cycle) {
|
|
774
|
+
const pass = cycle.pass;
|
|
775
|
+
const reviewConfig = cycle.kind === "auto-deep" ? deepReviewConfig(orchestrator.config) : orchestrator.config;
|
|
776
|
+
const phase = orchestrator.active!.state.phase;
|
|
777
|
+
const sourceReviewers = phase === "brainstorm"
|
|
778
|
+
? reviewConfig.brainstormReviewers
|
|
779
|
+
: phase === "plan"
|
|
780
|
+
? reviewConfig.planReviewers
|
|
781
|
+
: reviewConfig.codeReviewers;
|
|
782
|
+
const failedSet = new Set(failedReviewerVariants);
|
|
783
|
+
const scopedReviewers: typeof sourceReviewers = {};
|
|
784
|
+
for (const [name, cfg] of Object.entries(sourceReviewers)) {
|
|
785
|
+
if (failedSet.has(name)) scopedReviewers[name] = cfg;
|
|
786
|
+
}
|
|
787
|
+
const retryCount = Object.keys(scopedReviewers).length;
|
|
788
|
+
if (retryCount > 0) {
|
|
789
|
+
const retryConfig = phase === "brainstorm"
|
|
790
|
+
? { ...reviewConfig, brainstormReviewers: scopedReviewers }
|
|
791
|
+
: phase === "plan"
|
|
792
|
+
? { ...reviewConfig, planReviewers: scopedReviewers }
|
|
793
|
+
: { ...reviewConfig, codeReviewers: scopedReviewers };
|
|
794
|
+
const spawnFn = phase === "brainstorm"
|
|
795
|
+
? () => spawnBrainstormReviewers(pi, orchestrator.cwd, orchestrator.active!.dir, orchestrator.active!.taskId, retryConfig, pass)
|
|
796
|
+
: phase === "plan"
|
|
797
|
+
? () => spawnPlanReviewers(pi, orchestrator.cwd, orchestrator.active!.dir, orchestrator.active!.taskId, retryConfig)
|
|
798
|
+
: () => spawnCodeReviewers(pi, orchestrator.cwd, orchestrator.active!.dir, orchestrator.active!.taskId, retryConfig, pass);
|
|
799
|
+
orchestrator.failedReviewerVariants = [];
|
|
800
|
+
orchestrator.pendingSubagentSpawns = retryCount;
|
|
801
|
+
cycle.step = "await_reviewers";
|
|
802
|
+
orchestrator.active!.state.step = "await_reviewers";
|
|
803
|
+
saveTask(orchestrator.active!.dir, orchestrator.active!.state);
|
|
804
|
+
spawnFn().then((result) => {
|
|
805
|
+
orchestrator.failedReviewerVariants = result.failedVariants;
|
|
806
|
+
if (result.spawned === 0) orchestrator.pendingSubagentSpawns = 0;
|
|
807
|
+
for (const id of result.agentIds ?? []) {
|
|
808
|
+
orchestrator.spawnedAgentIds.delete(id);
|
|
809
|
+
}
|
|
810
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
811
|
+
checkReviewCycleCompletion();
|
|
812
|
+
}).catch((err) => {
|
|
813
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
814
|
+
console.error(`[pi-pi] retry spawn reviewers failed (${phase}): ${err.message}`);
|
|
815
|
+
checkReviewCycleCompletion();
|
|
816
|
+
});
|
|
817
|
+
pi.sendUserMessage(`[PI-PI] Retrying failed reviewers: ${variantsText}.`, { deliverAs: "followUp" });
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (choice === "Stop task") {
|
|
824
|
+
pi.sendUserMessage(`[PI-PI] ${stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (choice === "Continue without review") {
|
|
829
|
+
if (!orchestrator.active?.state.reviewCycle) return;
|
|
830
|
+
orchestrator.failedReviewerVariants = [];
|
|
831
|
+
orchestrator.active.state.reviewCycle = null;
|
|
832
|
+
orchestrator.active.state.step = "user_gate";
|
|
833
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
834
|
+
pi.sendUserMessage("[PI-PI] Review cycle skipped. Use /pp to choose the next action.", { deliverAs: "followUp" });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
orchestrator.failedReviewerVariants = [];
|
|
839
|
+
} finally {
|
|
840
|
+
orchestrator.reviewerFailureDialogPending = false;
|
|
841
|
+
checkReviewCycleCompletion();
|
|
842
|
+
}
|
|
843
|
+
})();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
tryCompleteReviewCycle(orchestrator);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
pi.events.on("subagents:completed", (data: any) => {
|
|
851
|
+
if (!orchestrator.active || !data?.id) return;
|
|
852
|
+
trackSubagentEvent(data, "completed");
|
|
853
|
+
orchestrator.spawnedAgentIds.delete(data.id);
|
|
854
|
+
orchestrator.agentDescriptions.delete(data.id);
|
|
855
|
+
orchestrator.agentSpawnTimes.delete(data.id);
|
|
856
|
+
orchestrator.agentLifecycle.delete(data.id);
|
|
857
|
+
|
|
858
|
+
const desc = data.description || data.type || data.id;
|
|
859
|
+
const duration = data.durationMs ? `${(data.durationMs / 1000).toFixed(1)}s` : "";
|
|
860
|
+
const tokens = data.tokens?.total ? `${data.tokens.total} tok` : "";
|
|
861
|
+
const stats = [duration, tokens].filter(Boolean).join(", ");
|
|
862
|
+
|
|
863
|
+
pi.sendMessage(
|
|
864
|
+
{
|
|
865
|
+
customType: "pp-subagent-result",
|
|
866
|
+
content: `${desc} completed${stats ? ` (${stats})` : ""}. Use get_subagent_result to read the output.`,
|
|
867
|
+
display: false,
|
|
868
|
+
},
|
|
869
|
+
{ deliverAs: "steer" },
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
checkPlannerCompletion();
|
|
873
|
+
checkReviewCycleCompletion();
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
pi.events.on("subagents:failed", (data: any) => {
|
|
877
|
+
if (!orchestrator.active || !data?.id) return;
|
|
878
|
+
trackSubagentEvent(data, "failed");
|
|
879
|
+
orchestrator.spawnedAgentIds.delete(data.id);
|
|
880
|
+
orchestrator.agentSpawnTimes.delete(data.id);
|
|
881
|
+
const desc = orchestrator.agentDescriptions.get(data.id) || data.type || data.id;
|
|
882
|
+
orchestrator.agentDescriptions.delete(data.id);
|
|
883
|
+
orchestrator.agentLifecycle.delete(data.id);
|
|
884
|
+
|
|
885
|
+
if (data.status === "stopped" || data.status === "aborted") {
|
|
886
|
+
checkPlannerCompletion();
|
|
887
|
+
checkReviewCycleCompletion();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const isApiError = data.status === "error" && (data.toolUses ?? 0) === 0;
|
|
892
|
+
if (isApiError && orchestrator.spawnedAgentIds.size > 0) {
|
|
893
|
+
orchestrator.abortAllSubagents();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
pi.sendMessage(
|
|
897
|
+
{
|
|
898
|
+
customType: "pp-subagent-error",
|
|
899
|
+
content: isApiError
|
|
900
|
+
? `**${desc}** failed (model/API error): ${data.error || "unknown error"}. All subagents aborted. Do NOT retry — the model is likely unavailable. Report the error to the user and ask how to proceed.`
|
|
901
|
+
: `**${desc}** failed: ${data.error || "unknown error"}. Do NOT retry — continue with available information.`,
|
|
902
|
+
display: true,
|
|
903
|
+
},
|
|
904
|
+
{ deliverAs: "steer" },
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
checkPlannerCompletion();
|
|
908
|
+
checkReviewCycleCompletion();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
pi.on("session_before_switch" as any, async () => {
|
|
912
|
+
if (!orchestrator.active) return;
|
|
913
|
+
cancelPendingPlannotatorWait(orchestrator);
|
|
914
|
+
orchestrator.abortAllSubagents();
|
|
915
|
+
unregisterAgentDefinitions(pi);
|
|
916
|
+
await orchestrator.cleanupActive();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
920
|
+
orchestrator.lastCtx = ctx;
|
|
921
|
+
orchestrator.cwd = ctx.cwd;
|
|
922
|
+
|
|
923
|
+
const subagentsMgr = (globalThis as any)[Symbol.for("pi-subagents:manager")];
|
|
924
|
+
subagentsMgr?.refreshWidget?.(ctx.ui);
|
|
925
|
+
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
926
|
+
taskStore?.refreshWidget?.(ctx.ui);
|
|
927
|
+
|
|
928
|
+
if ((globalThis as any)[SUBAGENT_SESSION_KEY]) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const duplicates = orchestrator.checkForConflictingExtensions();
|
|
933
|
+
if (duplicates.length > 0) {
|
|
934
|
+
const msg = `pi-pi bundles its own versions of pi-subagents, pi-tasks, and pi-ask-user. ` +
|
|
935
|
+
`Duplicate tools detected: ${duplicates.join(", ")}. ` +
|
|
936
|
+
`Remove the conflicting packages: pi remove npm:@tintinweb/pi-subagents npm:@tintinweb/pi-tasks npm:pi-ask-user`;
|
|
937
|
+
ctx.ui.notify(msg, "error");
|
|
938
|
+
console.error(`[pi-pi] FATAL: ${msg}`);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
const { setPI, initFlantOnStartup } = await import("./flant-infra.js");
|
|
944
|
+
setPI(pi);
|
|
945
|
+
await initFlantOnStartup(pi);
|
|
946
|
+
} catch (err: any) {
|
|
947
|
+
console.error(`[pi-pi] Flant infra init failed: ${err.message}`);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
try {
|
|
951
|
+
orchestrator.config = loadConfig(orchestrator.cwd);
|
|
952
|
+
} catch (err: any) {
|
|
953
|
+
console.error(`[pi-pi] Failed to load config on session start: ${err.message}`);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
registerCbmTools(pi, orchestrator.cwd);
|
|
958
|
+
registerExaTools(pi);
|
|
959
|
+
registerAstSearchTool(pi, orchestrator.cwd);
|
|
960
|
+
registerOrchestratorTools(orchestrator);
|
|
961
|
+
setExtensionOnlyMode(pi);
|
|
962
|
+
orchestrator.registerAgents();
|
|
963
|
+
|
|
964
|
+
const found = getActiveTask(orchestrator.cwd, orchestrator.config.timeouts.lockStale);
|
|
965
|
+
if (found) {
|
|
966
|
+
ctx.ui.notify(
|
|
967
|
+
`Paused task: "${taskName(found.dir)}" (${found.type}, phase: ${found.state.phase}). Run /pp and choose Resume to continue.`,
|
|
968
|
+
"info",
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
pi.on("input", async (event, ctx) => {
|
|
974
|
+
if (!orchestrator.active) return;
|
|
975
|
+
if (event.source !== "interactive") return;
|
|
976
|
+
|
|
977
|
+
const step = orchestrator.active.state.step;
|
|
978
|
+
if (step === "await_planners" || step === "await_reviewers") {
|
|
979
|
+
ctx.ui.notify("Waiting for subagents to finish. Your input will be available after they complete.", "warning");
|
|
980
|
+
return { action: "handled" as const };
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
985
|
+
orchestrator.lastCtx = ctx;
|
|
986
|
+
if (orchestrator.taskDoneCompactionPending || orchestrator.phaseCompactionPending) {
|
|
987
|
+
ctx.abort();
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (!orchestrator.active || orchestrator.active.state.phase === "done") return;
|
|
991
|
+
|
|
992
|
+
const step = orchestrator.active.state.step;
|
|
993
|
+
if (step === "await_planners" || step === "await_reviewers") {
|
|
994
|
+
ctx.abort();
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
orchestrator.nudgeHalted = false;
|
|
999
|
+
orchestrator.updateStatus(ctx);
|
|
1000
|
+
|
|
1001
|
+
const phasePrompt = orchestrator.getPhasePrompt(ctx);
|
|
1002
|
+
const systemContextFiles = loadContextFiles(orchestrator.cwd, "main", "system");
|
|
1003
|
+
const systemSnippets = systemContextFiles.map((f) => f.content).join("\n\n");
|
|
1004
|
+
|
|
1005
|
+
const fullAddition = [WORKING_PRINCIPLES, COMMUNICATION, systemSnippets, phasePrompt].filter(Boolean).join("\n\n");
|
|
1006
|
+
if (!fullAddition) return;
|
|
1007
|
+
|
|
1008
|
+
return {
|
|
1009
|
+
systemPrompt: event.systemPrompt + "\n\n" + fullAddition,
|
|
1010
|
+
};
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
pi.on("tool_call", async (event, _ctx) => {
|
|
1014
|
+
if (event.toolName === "Agent" && orchestrator.active) {
|
|
1015
|
+
const input = event.input as Record<string, unknown>;
|
|
1016
|
+
const requestedType = ((input.subagent_type as string) || "").toLowerCase();
|
|
1017
|
+
const isExplore = !requestedType || requestedType === "explore";
|
|
1018
|
+
const isLibrarian = requestedType === "librarian";
|
|
1019
|
+
|
|
1020
|
+
if (isExplore) {
|
|
1021
|
+
input.subagent_type = "explore";
|
|
1022
|
+
input.model = orchestrator.config.agents.explore.model;
|
|
1023
|
+
input.thinking = orchestrator.config.agents.explore.thinking;
|
|
1024
|
+
} else if (isLibrarian) {
|
|
1025
|
+
input.subagent_type = "librarian";
|
|
1026
|
+
input.model = orchestrator.config.agents.librarian.model;
|
|
1027
|
+
input.thinking = orchestrator.config.agents.librarian.thinking;
|
|
1028
|
+
} else {
|
|
1029
|
+
input.subagent_type = "task";
|
|
1030
|
+
input.model = orchestrator.config.agents.task.model;
|
|
1031
|
+
input.thinking = orchestrator.config.agents.task.thinking;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
1036
|
+
const input = event.input as { file_path?: string; filePath?: string; path?: string };
|
|
1037
|
+
const rawPath = input.file_path || input.filePath || input.path || "";
|
|
1038
|
+
const resolvedPath = resolve(orchestrator.cwd, rawPath);
|
|
1039
|
+
const ppStateDir = resolve(orchestrator.cwd, ".pp", "state");
|
|
1040
|
+
const ppDir = resolve(orchestrator.cwd, ".pp");
|
|
1041
|
+
|
|
1042
|
+
if (resolvedPath.startsWith(ppStateDir + "/") || resolvedPath === ppStateDir) {
|
|
1043
|
+
if (!resolvedPath.endsWith(".md")) {
|
|
1044
|
+
return { block: true, reason: "Cannot write non-.md files in .pp/state/" };
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const fileName = basename(resolvedPath);
|
|
1049
|
+
if (fileName === "state.json" && (resolvedPath.startsWith(ppDir + "/") || resolvedPath === ppDir)) {
|
|
1050
|
+
return { block: true, reason: "state.json is managed by the extension" };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (fileName === "config.json" && (resolvedPath.startsWith(ppDir + "/") || resolvedPath === ppDir)) {
|
|
1054
|
+
return { block: true, reason: "config.json is managed by the user, not the LLM" };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
pi.on("tool_result", async (event, _ctx) => {
|
|
1061
|
+
if (!orchestrator.active) return;
|
|
1062
|
+
|
|
1063
|
+
if ((event.toolName === "edit" || event.toolName === "write") && !event.isError) {
|
|
1064
|
+
const input = event.input as { file_path?: string; filePath?: string; path?: string };
|
|
1065
|
+
const filePath = input.file_path || input.filePath || input.path;
|
|
1066
|
+
if (!filePath) return;
|
|
1067
|
+
|
|
1068
|
+
const resolvedWrite = resolve(orchestrator.cwd, filePath);
|
|
1069
|
+
const taskDir = orchestrator.active.dir;
|
|
1070
|
+
if (resolvedWrite === join(taskDir, "USER_REQUEST.md")) {
|
|
1071
|
+
const content = readFileSync(resolvedWrite, "utf-8");
|
|
1072
|
+
const result = validateUserRequest(content);
|
|
1073
|
+
if (!result.ok) {
|
|
1074
|
+
return {
|
|
1075
|
+
content: [
|
|
1076
|
+
...event.content,
|
|
1077
|
+
{ type: "text" as const, text: `\n\n<validation-error>\nUSER_REQUEST.md structure is invalid:\n${result.errors.map((e) => `- ${e}`).join("\n")}\n\nFix immediately. Keep exactly: # User Request, ## Problem, ## Constraints. No other sections.\n</validation-error>` },
|
|
1078
|
+
],
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (resolvedWrite === join(taskDir, "RESEARCH.md")) {
|
|
1083
|
+
const content = readFileSync(resolvedWrite, "utf-8");
|
|
1084
|
+
const result = validateResearch(content);
|
|
1085
|
+
if (!result.ok) {
|
|
1086
|
+
return {
|
|
1087
|
+
content: [
|
|
1088
|
+
...event.content,
|
|
1089
|
+
{ type: "text" as const, text: `\n\n<validation-error>\nRESEARCH.md structure is invalid:\n${result.errors.map((e) => `- ${e}`).join("\n")}\n\nFix immediately. Keep exactly: ## Affected Code, ## Architecture Context, ## Constraints & Edge Cases, ## Open Questions (optional). No other sections.\n</validation-error>` },
|
|
1090
|
+
],
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const artifactsDir = join(taskDir, "artifacts");
|
|
1095
|
+
if (resolvedWrite.startsWith(artifactsDir + "/") && resolvedWrite.endsWith(".md")) {
|
|
1096
|
+
const content = readFileSync(resolvedWrite, "utf-8");
|
|
1097
|
+
const result = validateArtifact(content);
|
|
1098
|
+
if (!result.ok) {
|
|
1099
|
+
return {
|
|
1100
|
+
content: [
|
|
1101
|
+
...event.content,
|
|
1102
|
+
{ type: "text" as const, text: `\n\n<validation-error>\nArtifact structure is invalid:\n${result.errors.map((e) => `- ${e}`).join("\n")}\n\nFix immediately. Artifact files must start with # <Title>.\n</validation-error>` },
|
|
1103
|
+
],
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (filePath.includes(".pp/")) return;
|
|
1109
|
+
|
|
1110
|
+
if (orchestrator.active.state.phase !== "implement") return;
|
|
1111
|
+
|
|
1112
|
+
orchestrator.active.modifiedFiles.add(filePath);
|
|
1113
|
+
orchestrator.active.state.modifiedFiles = [...orchestrator.active.modifiedFiles];
|
|
1114
|
+
if (!orchestrator.active.state.repoCwd) {
|
|
1115
|
+
const dir = resolve(filePath, "..");
|
|
1116
|
+
try {
|
|
1117
|
+
const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: dir, timeout: 5000 });
|
|
1118
|
+
if (result.code === 0 && result.stdout.trim()) {
|
|
1119
|
+
orchestrator.active.state.repoCwd = result.stdout.trim();
|
|
1120
|
+
}
|
|
1121
|
+
} catch {}
|
|
1122
|
+
}
|
|
1123
|
+
try { saveTask(orchestrator.active.dir, orchestrator.active.state); } catch {}
|
|
1124
|
+
|
|
1125
|
+
const afterEditResults = runAfterEdit(filePath, orchestrator.config, orchestrator.cwd);
|
|
1126
|
+
const failures = afterEditResults.filter((r) => !r.ok);
|
|
1127
|
+
|
|
1128
|
+
if (failures.length > 0) {
|
|
1129
|
+
const failureText = failures
|
|
1130
|
+
.map((f) => `afterEdit command failed: ${f.command}\n${f.output}`)
|
|
1131
|
+
.join("\n\n");
|
|
1132
|
+
return {
|
|
1133
|
+
content: [
|
|
1134
|
+
...event.content,
|
|
1135
|
+
{ type: "text" as const, text: `\n\n<afterEdit>\n${failureText}\n</afterEdit>` },
|
|
1136
|
+
],
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const lspAvailable = pi.getAllTools().some((t) => t.name === "lsp");
|
|
1141
|
+
if (lspAvailable) {
|
|
1142
|
+
return {
|
|
1143
|
+
content: [
|
|
1144
|
+
...event.content,
|
|
1145
|
+
{ type: "text" as const, text: `\n\nRun lsp diagnostics on ${filePath} to check for errors.` },
|
|
1146
|
+
],
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
pi.on("session_before_compact", async (event, _ctx) => {
|
|
1154
|
+
if (orchestrator.taskDoneCompactionPending) {
|
|
1155
|
+
const summary = orchestrator.taskDoneCompactionSummary;
|
|
1156
|
+
orchestrator.taskDoneCompactionPending = false;
|
|
1157
|
+
orchestrator.taskDoneCompactionSummary = "";
|
|
1158
|
+
return {
|
|
1159
|
+
compaction: {
|
|
1160
|
+
summary,
|
|
1161
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
1162
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (!orchestrator.active || orchestrator.active.state.phase === "done") return;
|
|
1168
|
+
|
|
1169
|
+
if (orchestrator.phaseCompactionPending) {
|
|
1170
|
+
return {
|
|
1171
|
+
compaction: {
|
|
1172
|
+
summary: `Previous phase (${orchestrator.active.state.phase}) completed. Transitioning to next phase.`,
|
|
1173
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
1174
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
1175
|
+
},
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const artifacts = getPhaseArtifacts(orchestrator.active.dir, orchestrator.active.state.phase);
|
|
1180
|
+
if (artifacts.length === 0) return;
|
|
1181
|
+
|
|
1182
|
+
const artifactText = artifacts
|
|
1183
|
+
.map((a) => `=== ${a.name} ===\n${a.content}`)
|
|
1184
|
+
.join("\n\n");
|
|
1185
|
+
|
|
1186
|
+
pi.sendMessage(
|
|
1187
|
+
{
|
|
1188
|
+
customType: "pp-artifact-reinject",
|
|
1189
|
+
content: `[PI-PI ARTIFACTS — re-injected after compaction]\n\n${artifactText}`,
|
|
1190
|
+
display: false,
|
|
1191
|
+
},
|
|
1192
|
+
{ deliverAs: "steer" },
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
return;
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
1199
|
+
if (!orchestrator.active || orchestrator.active.state.phase === "done") return;
|
|
1200
|
+
orchestrator.updateStatus(ctx);
|
|
1201
|
+
|
|
1202
|
+
if (
|
|
1203
|
+
orchestrator.active.state.phase === "implement" &&
|
|
1204
|
+
orchestrator.config.autoCommit &&
|
|
1205
|
+
orchestrator.active.modifiedFiles.size > 0 &&
|
|
1206
|
+
!orchestrator.commitReminderSent
|
|
1207
|
+
) {
|
|
1208
|
+
orchestrator.commitReminderSent = true;
|
|
1209
|
+
pi.sendMessage(
|
|
1210
|
+
{
|
|
1211
|
+
customType: "pp-commit-reminder",
|
|
1212
|
+
content: `You have ${orchestrator.active.modifiedFiles.size} uncommitted file(s). If you've completed a logical unit of work, call pp_commit with a descriptive message.`,
|
|
1213
|
+
display: false,
|
|
1214
|
+
},
|
|
1215
|
+
{ deliverAs: "steer" },
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const phase = orchestrator.active.state.phase;
|
|
1220
|
+
|
|
1221
|
+
const msg = event.message as any;
|
|
1222
|
+
if (msg?.stopReason === "aborted") return;
|
|
1223
|
+
if (msg?.stopReason === "error") {
|
|
1224
|
+
const errorMsg = msg.errorMessage || "unknown error";
|
|
1225
|
+
console.error(`[pi-pi] Turn ended with error: ${errorMsg}`);
|
|
1226
|
+
console.error(`[pi-pi] model=${msg.model} provider=${msg.provider} api=${msg.api}`);
|
|
1227
|
+
console.error(`[pi-pi] usage: input=${msg.usage?.input} output=${msg.usage?.output} cache_read=${msg.usage?.cacheRead}`);
|
|
1228
|
+
console.error(`[pi-pi] content blocks: ${msg.content?.length ?? 0}`);
|
|
1229
|
+
for (const c of msg.content || []) {
|
|
1230
|
+
if (c.type === "toolCall") console.error(`[pi-pi] toolCall: ${c.name}`);
|
|
1231
|
+
if (c.type === "text" && c.text) console.error(`[pi-pi] text: ${c.text.slice(0, 100)}`);
|
|
1232
|
+
if (c.type === "thinking") console.error(`[pi-pi] thinking: ${c.thinking?.slice(0, 100) || "(redacted)"}`);
|
|
1233
|
+
}
|
|
1234
|
+
orchestrator.errorRetryCount = (orchestrator.errorRetryCount ?? 0) + 1;
|
|
1235
|
+
const retryDelays = [2000, 6000, 24000];
|
|
1236
|
+
if (orchestrator.errorRetryCount <= retryDelays.length) {
|
|
1237
|
+
const delay = retryDelays[orchestrator.errorRetryCount - 1];
|
|
1238
|
+
ctx.ui.notify(`API error (attempt ${orchestrator.errorRetryCount}/${retryDelays.length}): ${errorMsg}. Retrying in ${delay / 1000}s...`, "warning");
|
|
1239
|
+
const taskToken = orchestrator.activeTaskToken;
|
|
1240
|
+
if (orchestrator.pendingRetryTimer) clearTimeout(orchestrator.pendingRetryTimer);
|
|
1241
|
+
orchestrator.pendingRetryTimer = setTimeout(() => {
|
|
1242
|
+
orchestrator.pendingRetryTimer = null;
|
|
1243
|
+
if (orchestrator.activeTaskToken !== taskToken || !orchestrator.active) return;
|
|
1244
|
+
pi.sendUserMessage(`[PI-PI] Previous request failed due to an API error. Continue working on the current phase (${phase}).`);
|
|
1245
|
+
}, delay);
|
|
1246
|
+
} else {
|
|
1247
|
+
ctx.ui.notify(`API error persisted after 3 retries: ${errorMsg}. Stopping auto-retry.`, "error");
|
|
1248
|
+
orchestrator.errorRetryCount = 0;
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
orchestrator.errorRetryCount = 0;
|
|
1253
|
+
|
|
1254
|
+
if ((globalThis as any)[SUBAGENT_SESSION_KEY]) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (orchestrator.active.state.step === "await_planners" || orchestrator.active.state.step === "await_reviewers") {
|
|
1259
|
+
if (!orchestrator.awaitPollTimer) {
|
|
1260
|
+
orchestrator.awaitPollTimer = setInterval(() => {
|
|
1261
|
+
if (!orchestrator.active) {
|
|
1262
|
+
clearInterval(orchestrator.awaitPollTimer!);
|
|
1263
|
+
orchestrator.awaitPollTimer = null;
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const taskDir = orchestrator.active.dir;
|
|
1267
|
+
if (orchestrator.active.state.step === "await_planners") {
|
|
1268
|
+
const plansDir = join(taskDir, "plans");
|
|
1269
|
+
const plannerCount = Object.values(orchestrator.config.planners).filter((v) => v.enabled).length;
|
|
1270
|
+
if (existsSync(plansDir)) {
|
|
1271
|
+
const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md") && !f.includes("synthesized") && !f.includes("review_"));
|
|
1272
|
+
if (planFiles.length >= plannerCount) {
|
|
1273
|
+
clearInterval(orchestrator.awaitPollTimer!);
|
|
1274
|
+
orchestrator.awaitPollTimer = null;
|
|
1275
|
+
orchestrator.spawnedAgentIds.clear();
|
|
1276
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
1277
|
+
orchestrator.active.state.step = "synthesize";
|
|
1278
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
1279
|
+
pi.sendUserMessage("[PI-PI] All planners completed. Read their outputs and synthesize the plan.");
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
} else if (orchestrator.active.state.step === "await_reviewers" && orchestrator.active.state.reviewCycle) {
|
|
1283
|
+
const cycle = orchestrator.active.state.reviewCycle;
|
|
1284
|
+
const reviewConfig = cycle.kind === "auto-deep" ? deepReviewConfig(orchestrator.config) : orchestrator.config;
|
|
1285
|
+
const phase = orchestrator.active.state.phase;
|
|
1286
|
+
const reviewers = phase === "brainstorm"
|
|
1287
|
+
? reviewConfig.brainstormReviewers
|
|
1288
|
+
: phase === "plan"
|
|
1289
|
+
? reviewConfig.planReviewers
|
|
1290
|
+
: reviewConfig.codeReviewers;
|
|
1291
|
+
const reviewerCount = Object.values(reviewers).filter((v) => v.enabled).length;
|
|
1292
|
+
const outputs = loadPhaseReviewOutputs(taskDir, phase, cycle.pass);
|
|
1293
|
+
if (outputs.length >= reviewerCount) {
|
|
1294
|
+
clearInterval(orchestrator.awaitPollTimer!);
|
|
1295
|
+
orchestrator.awaitPollTimer = null;
|
|
1296
|
+
orchestrator.spawnedAgentIds.clear();
|
|
1297
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
1298
|
+
|
|
1299
|
+
if (orchestrator.reviewTransitionToken === orchestrator.activeTaskToken) return;
|
|
1300
|
+
orchestrator.reviewTransitionToken = orchestrator.activeTaskToken;
|
|
1301
|
+
|
|
1302
|
+
cycle.step = "apply_feedback";
|
|
1303
|
+
orchestrator.active.state.step = "apply_feedback";
|
|
1304
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
1305
|
+
const rendered = outputs.map((o) => `=== ${o.name} ===\n${o.content}`).join("\n\n");
|
|
1306
|
+
pi.sendMessage(
|
|
1307
|
+
{ customType: "pp-review-ready", content: `[PI-PI] Reviewer outputs are ready.\n\n${rendered}`, display: false },
|
|
1308
|
+
{ deliverAs: "followUp" },
|
|
1309
|
+
);
|
|
1310
|
+
pi.sendUserMessage("[PI-PI] Review cycle is ready for apply_feedback. Read reviewer outputs and proceed.");
|
|
1311
|
+
}
|
|
1312
|
+
} else {
|
|
1313
|
+
clearInterval(orchestrator.awaitPollTimer!);
|
|
1314
|
+
orchestrator.awaitPollTimer = null;
|
|
1315
|
+
}
|
|
1316
|
+
}, 5000);
|
|
1317
|
+
}
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (orchestrator.awaitPollTimer) {
|
|
1321
|
+
clearInterval(orchestrator.awaitPollTimer);
|
|
1322
|
+
orchestrator.awaitPollTimer = null;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (orchestrator.active.type === "brainstorm" && phase === "brainstorm") return;
|
|
1326
|
+
|
|
1327
|
+
const contentParts = Array.isArray(msg?.content) ? msg.content : [];
|
|
1328
|
+
const hasText = contentParts.some((c: any) => c.type === "text" && c.text?.trim());
|
|
1329
|
+
const hasToolCalls = contentParts.some((c: any) => c.type === "toolCall");
|
|
1330
|
+
const hasToolResults = event.toolResults && event.toolResults.length > 0;
|
|
1331
|
+
const turnWasEmpty = !hasText && !hasToolCalls && !hasToolResults;
|
|
1332
|
+
|
|
1333
|
+
if (!turnWasEmpty) {
|
|
1334
|
+
if (hasText && !hasToolCalls) {
|
|
1335
|
+
const step = orchestrator.active.state.step;
|
|
1336
|
+
if (step !== "await_planners" && step !== "await_reviewers") {
|
|
1337
|
+
pi.sendMessage(
|
|
1338
|
+
{
|
|
1339
|
+
customType: "pp-phase-complete-reminder",
|
|
1340
|
+
content: `[PI-PI] You stopped without calling pp_phase_complete. If you are done with the ${phase} phase, call pp_phase_complete now. If not, continue working.`,
|
|
1341
|
+
display: false,
|
|
1342
|
+
},
|
|
1343
|
+
{ deliverAs: "followUp" },
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (orchestrator.nudgeHalted) return;
|
|
1350
|
+
if (orchestrator.spawnedAgentIds.size > 0 || orchestrator.pendingSubagentSpawns > 0) return;
|
|
1351
|
+
|
|
1352
|
+
const step = orchestrator.active.state.step;
|
|
1353
|
+
if (step === "await_planners" || step === "await_reviewers") return;
|
|
1354
|
+
|
|
1355
|
+
const now = Date.now();
|
|
1356
|
+
|
|
1357
|
+
orchestrator.nudgeTimestamps.push(now);
|
|
1358
|
+
orchestrator.nudgeTimestamps = orchestrator.nudgeTimestamps.filter((t) => now - t < 60000);
|
|
1359
|
+
|
|
1360
|
+
const sendNudge = () => {
|
|
1361
|
+
pi.sendMessage(
|
|
1362
|
+
{
|
|
1363
|
+
customType: "pp-continuation",
|
|
1364
|
+
content: `[PI-PI] Your previous response was interrupted. Continue working on the current phase (${phase}). Pick up where you left off.`,
|
|
1365
|
+
display: false,
|
|
1366
|
+
},
|
|
1367
|
+
{ deliverAs: "followUp" },
|
|
1368
|
+
);
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
if (orchestrator.nudgeTimestamps.length <= 3) {
|
|
1372
|
+
sendNudge();
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (orchestrator.nudgeTimestamps.length >= 5) {
|
|
1377
|
+
orchestrator.cooldownHits.push(now);
|
|
1378
|
+
orchestrator.cooldownHits = orchestrator.cooldownHits.filter((t) => now - t < 20 * 60 * 1000);
|
|
1379
|
+
|
|
1380
|
+
if (orchestrator.cooldownHits.length >= 5) {
|
|
1381
|
+
orchestrator.nudgeHalted = true;
|
|
1382
|
+
orchestrator.nudgeTimestamps = [];
|
|
1383
|
+
orchestrator.cooldownHits = [];
|
|
1384
|
+
pi.sendMessage(
|
|
1385
|
+
{
|
|
1386
|
+
customType: "pp-continuation-halted",
|
|
1387
|
+
content: "Agent has been repeatedly interrupted. Auto-continuation paused. Send any message to resume nudging.",
|
|
1388
|
+
display: true,
|
|
1389
|
+
},
|
|
1390
|
+
{ deliverAs: "steer" },
|
|
1391
|
+
);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
pi.sendMessage(
|
|
1396
|
+
{
|
|
1397
|
+
customType: "pp-continuation-cooldown",
|
|
1398
|
+
content: "Agent interrupted repeatedly. Waiting 60 seconds before retrying.",
|
|
1399
|
+
display: true,
|
|
1400
|
+
},
|
|
1401
|
+
{ deliverAs: "steer" },
|
|
1402
|
+
);
|
|
1403
|
+
orchestrator.nudgeTimestamps = [];
|
|
1404
|
+
const cooldownToken = orchestrator.activeTaskToken;
|
|
1405
|
+
setTimeout(() => {
|
|
1406
|
+
if (orchestrator.activeTaskToken !== cooldownToken || !orchestrator.active) return;
|
|
1407
|
+
sendNudge();
|
|
1408
|
+
}, 60000);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
sendNudge();
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
}
|