@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,1214 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, 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 { Orchestrator } from "./orchestrator.js";
|
|
6
|
+
import { registerCommandHandlers } from "./command-handlers.js";
|
|
7
|
+
import { registerEventHandlers, runUserGateDialog } from "./event-handlers.js";
|
|
8
|
+
import { createTask, loadTask, saveTask } from "./state.js";
|
|
9
|
+
|
|
10
|
+
vi.mock("./cbm.js", () => ({ registerCbmTools: vi.fn() }));
|
|
11
|
+
vi.mock("./exa.js", () => ({ registerExaTools: vi.fn() }));
|
|
12
|
+
vi.mock("./ast-search.js", () => ({ registerAstSearchTool: vi.fn() }));
|
|
13
|
+
vi.mock("./agents/registry.js", () => ({
|
|
14
|
+
registerAgentDefinitions: vi.fn(),
|
|
15
|
+
unregisterAgentDefinitions: vi.fn(),
|
|
16
|
+
setExtensionOnlyMode: vi.fn(),
|
|
17
|
+
spawnViaRpc: vi.fn(async (_pi: any, _type: string) => ({ id: `mock-${Math.random().toString(36).slice(2)}` })),
|
|
18
|
+
waitForCompletion: vi.fn(async () => undefined),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("./config.js", async (importOriginal) => {
|
|
22
|
+
const original = await importOriginal<typeof import("./config.js")>();
|
|
23
|
+
return { ...original, loadConfig: vi.fn(() => ({
|
|
24
|
+
mainModel: {
|
|
25
|
+
implement: { model: "test/model", thinking: "high" },
|
|
26
|
+
debug: { model: "test/model", thinking: "high" },
|
|
27
|
+
brainstorm: { model: "test/model", thinking: "high" },
|
|
28
|
+
},
|
|
29
|
+
planners: { test: { enabled: true, model: "test/planner", thinking: "low" } },
|
|
30
|
+
planReviewers: {},
|
|
31
|
+
brainstormReviewers: { test: { enabled: true, model: "test/reviewer", thinking: "low" } },
|
|
32
|
+
codeReviewers: { test: { enabled: true, model: "test/reviewer", thinking: "low" } },
|
|
33
|
+
agents: {
|
|
34
|
+
explore: { model: "test/explore", thinking: "low" },
|
|
35
|
+
librarian: { model: "test/librarian", thinking: "medium" },
|
|
36
|
+
task: { model: "test/task", thinking: "medium" },
|
|
37
|
+
},
|
|
38
|
+
commands: { afterEdit: [], afterImplement: [] },
|
|
39
|
+
timeouts: { afterEdit: 1000, afterImplement: 1000, agentSpawn: 1000, agentReadyPing: 1000, lockStale: 600000, lockUpdate: 30000 },
|
|
40
|
+
autoCommit: false,
|
|
41
|
+
})) };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
type Handler = (...args: any[]) => any;
|
|
45
|
+
|
|
46
|
+
const tempDirs: string[] = [];
|
|
47
|
+
|
|
48
|
+
const VALID_USER_REQUEST = `# User Request
|
|
49
|
+
Fix the auth bug.
|
|
50
|
+
|
|
51
|
+
## Problem
|
|
52
|
+
Auth tokens expire incorrectly.
|
|
53
|
+
|
|
54
|
+
## Constraints
|
|
55
|
+
Must be backward compatible.
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const VALID_RESEARCH = `## Affected Code
|
|
59
|
+
src/auth.ts:validateToken — validates JWT tokens
|
|
60
|
+
|
|
61
|
+
## Architecture Context
|
|
62
|
+
- Auth middleware calls validateToken on every request
|
|
63
|
+
|
|
64
|
+
## Constraints & Edge Cases
|
|
65
|
+
- MUST: Existing tokens must remain valid
|
|
66
|
+
- RISK: Token refresh flow may break
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
function makeValidPlan(checklist: string[]): string {
|
|
70
|
+
return `# Plan
|
|
71
|
+
|
|
72
|
+
## Scope
|
|
73
|
+
Fix token validation in auth middleware. Does not change token format.
|
|
74
|
+
|
|
75
|
+
## Checklist
|
|
76
|
+
${checklist.join("\n")}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeTempDir(): string {
|
|
81
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-pi-integration-"));
|
|
82
|
+
tempDirs.push(dir);
|
|
83
|
+
return dir;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
vi.restoreAllMocks();
|
|
88
|
+
for (const dir of tempDirs.splice(0)) {
|
|
89
|
+
rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function makePi() {
|
|
94
|
+
const handlers = new Map<string, Handler>();
|
|
95
|
+
const eventHandlers = new Map<string, Handler[]>();
|
|
96
|
+
const commands = new Map<string, { handler: Handler; description?: string }>();
|
|
97
|
+
const tools = new Map<string, any>();
|
|
98
|
+
|
|
99
|
+
const pi = {
|
|
100
|
+
on: vi.fn((name: string, handler: Handler) => {
|
|
101
|
+
handlers.set(name, handler);
|
|
102
|
+
}),
|
|
103
|
+
events: {
|
|
104
|
+
on: vi.fn((name: string, handler: Handler) => {
|
|
105
|
+
const list = eventHandlers.get(name) ?? [];
|
|
106
|
+
list.push(handler);
|
|
107
|
+
eventHandlers.set(name, list);
|
|
108
|
+
return () => {
|
|
109
|
+
const idx = list.indexOf(handler);
|
|
110
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
emit: vi.fn((name: string, data?: any) => {
|
|
114
|
+
const list = eventHandlers.get(name) ?? [];
|
|
115
|
+
for (const h of list) h(data);
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
registerCommand: vi.fn((name: string, cmd: any) => {
|
|
119
|
+
commands.set(name, cmd);
|
|
120
|
+
}),
|
|
121
|
+
registerTool: vi.fn((tool: any) => {
|
|
122
|
+
tools.set(tool.name, tool);
|
|
123
|
+
}),
|
|
124
|
+
getAllTools: vi.fn().mockReturnValue([]),
|
|
125
|
+
sendMessage: vi.fn(),
|
|
126
|
+
sendUserMessage: vi.fn(),
|
|
127
|
+
setModel: vi.fn().mockResolvedValue(true),
|
|
128
|
+
setThinkingLevel: vi.fn(),
|
|
129
|
+
setSessionName: vi.fn(),
|
|
130
|
+
_handlers: handlers,
|
|
131
|
+
_eventHandlers: eventHandlers,
|
|
132
|
+
_commands: commands,
|
|
133
|
+
_tools: tools,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return pi;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function makeConfig() {
|
|
140
|
+
return {
|
|
141
|
+
mainModel: {
|
|
142
|
+
implement: { model: "test/model", thinking: "high" },
|
|
143
|
+
debug: { model: "test/model", thinking: "high" },
|
|
144
|
+
brainstorm: { model: "test/model", thinking: "high" },
|
|
145
|
+
},
|
|
146
|
+
planners: {
|
|
147
|
+
test: { enabled: true, model: "test/planner", thinking: "low" },
|
|
148
|
+
},
|
|
149
|
+
planReviewers: {},
|
|
150
|
+
brainstormReviewers: {
|
|
151
|
+
test: { enabled: true, model: "test/reviewer", thinking: "low" },
|
|
152
|
+
},
|
|
153
|
+
codeReviewers: {
|
|
154
|
+
test: { enabled: true, model: "test/reviewer", thinking: "low" },
|
|
155
|
+
},
|
|
156
|
+
agents: {
|
|
157
|
+
explore: { model: "test/explore", thinking: "low" },
|
|
158
|
+
librarian: { model: "test/librarian", thinking: "medium" },
|
|
159
|
+
task: { model: "test/task", thinking: "medium" },
|
|
160
|
+
},
|
|
161
|
+
commands: { afterEdit: [], afterImplement: [] },
|
|
162
|
+
timeouts: { afterEdit: 1000, afterImplement: 1000, agentSpawn: 1000, agentReadyPing: 1000, lockStale: 600000, lockUpdate: 30000 },
|
|
163
|
+
autoCommit: false,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function makeCtx(overrides: Record<string, any> = {}) {
|
|
168
|
+
return {
|
|
169
|
+
cwd: "/project",
|
|
170
|
+
hasUI: true,
|
|
171
|
+
ui: {
|
|
172
|
+
confirm: vi.fn(),
|
|
173
|
+
select: vi.fn(),
|
|
174
|
+
custom: vi.fn().mockResolvedValue(undefined),
|
|
175
|
+
notify: vi.fn(),
|
|
176
|
+
setStatus: vi.fn(),
|
|
177
|
+
},
|
|
178
|
+
abort: vi.fn(),
|
|
179
|
+
waitForIdle: vi.fn().mockResolvedValue(undefined),
|
|
180
|
+
compact: vi.fn((opts?: any) => {
|
|
181
|
+
if (opts?.onComplete) setTimeout(opts.onComplete, 0);
|
|
182
|
+
}),
|
|
183
|
+
modelRegistry: {
|
|
184
|
+
getAvailable: vi.fn().mockReturnValue([
|
|
185
|
+
{ provider: "test", id: "model" },
|
|
186
|
+
]),
|
|
187
|
+
},
|
|
188
|
+
...overrides,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function setupOrchestrator(cwd: string) {
|
|
193
|
+
const pi = makePi();
|
|
194
|
+
const orchestrator = new Orchestrator(pi as any);
|
|
195
|
+
registerEventHandlers(orchestrator);
|
|
196
|
+
registerCommandHandlers(orchestrator);
|
|
197
|
+
|
|
198
|
+
const sessionStartHandler = pi._handlers.get("session_start");
|
|
199
|
+
if (sessionStartHandler) {
|
|
200
|
+
await sessionStartHandler({}, makeCtx({ cwd }));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { pi, orchestrator };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getCommand(pi: ReturnType<typeof makePi>, name: string): Handler {
|
|
207
|
+
const cmd = pi._commands.get(name);
|
|
208
|
+
if (!cmd) throw new Error(`Command ${name} not registered`);
|
|
209
|
+
return cmd.handler;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getTool(pi: ReturnType<typeof makePi>, name: string): any {
|
|
213
|
+
const tool = pi._tools.get(name);
|
|
214
|
+
if (!tool) throw new Error(`Tool ${name} not registered`);
|
|
215
|
+
return tool;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function emitSubagentCreated(pi: ReturnType<typeof makePi>, id: string, description: string) {
|
|
219
|
+
pi.events.emit("subagents:created", { id, description });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function emitSubagentStarted(pi: ReturnType<typeof makePi>, id: string, description: string) {
|
|
223
|
+
pi.events.emit("subagents:started", { id, description });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function emitSubagentFirstTool(pi: ReturnType<typeof makePi>, id: string, description: string, toolName = "read") {
|
|
227
|
+
pi.events.emit("subagents:first_tool", { id, description, toolName });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function emitSubagentFirstTurn(pi: ReturnType<typeof makePi>, id: string, description: string, turnCount = 1) {
|
|
231
|
+
pi.events.emit("subagents:first_turn", { id, description, turnCount });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function emitSubagentCompleted(pi: ReturnType<typeof makePi>, id: string, description: string) {
|
|
235
|
+
pi.events.emit("subagents:completed", { id, description, result: "done" });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function emitSubagentFailed(pi: ReturnType<typeof makePi>, id: string, error: string) {
|
|
239
|
+
pi.events.emit("subagents:failed", { id, error });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
describe("implement pipeline: brainstorm → plan → implement → done", () => {
|
|
243
|
+
it("walks through the full implement pipeline with phase transitions", async () => {
|
|
244
|
+
const cwd = makeTempDir();
|
|
245
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
246
|
+
const ctx = makeCtx();
|
|
247
|
+
|
|
248
|
+
await orchestrator.startTask(ctx as any, "implement", "Add feature X");
|
|
249
|
+
|
|
250
|
+
expect(orchestrator.active).not.toBeNull();
|
|
251
|
+
expect(orchestrator.active!.state.phase).toBe("brainstorm");
|
|
252
|
+
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
253
|
+
expect(orchestrator.active!.type).toBe("implement");
|
|
254
|
+
const taskDir = orchestrator.active!.dir;
|
|
255
|
+
|
|
256
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
257
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
258
|
+
|
|
259
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
260
|
+
|
|
261
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
262
|
+
const result1 = await ppPhaseComplete.execute("call-1", { summary: "Research complete" }, undefined, undefined, ctx);
|
|
263
|
+
expect(result1.content[0].text).toContain("Transitioned to plan");
|
|
264
|
+
|
|
265
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
266
|
+
|
|
267
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
268
|
+
|
|
269
|
+
const plansDir = join(taskDir, "plans");
|
|
270
|
+
expect(existsSync(plansDir)).toBe(true);
|
|
271
|
+
|
|
272
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
273
|
+
writeFileSync(
|
|
274
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
275
|
+
makeValidPlan([
|
|
276
|
+
"- [ ] P1. Draft implementation step — Done when: first proposed step is documented",
|
|
277
|
+
"- [ ] P2. Draft verification step — Done when: second proposed step is documented",
|
|
278
|
+
]),
|
|
279
|
+
"utf-8",
|
|
280
|
+
);
|
|
281
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
282
|
+
|
|
283
|
+
expect(orchestrator.active!.state.step).toBe("synthesize");
|
|
284
|
+
|
|
285
|
+
const synthPath = join(plansDir, `${Math.floor(Date.now() / 1000)}_synthesized.md`);
|
|
286
|
+
writeFileSync(
|
|
287
|
+
synthPath,
|
|
288
|
+
makeValidPlan([
|
|
289
|
+
"- [ ] P1. Implement X — Done when: implementation for X is complete",
|
|
290
|
+
"- [ ] P2. Implement Y — Done when: implementation for Y is complete",
|
|
291
|
+
]),
|
|
292
|
+
"utf-8",
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
ctx.ui.select.mockResolvedValueOnce("Approve plan");
|
|
296
|
+
|
|
297
|
+
const result2 = await ppPhaseComplete.execute("call-2", { summary: "Plan synthesized" }, undefined, undefined, ctx);
|
|
298
|
+
expect(result2.content[0].text).toContain("Transitioned to implement");
|
|
299
|
+
|
|
300
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
301
|
+
|
|
302
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
303
|
+
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
304
|
+
|
|
305
|
+
const synthContent = readFileSync(synthPath, "utf-8");
|
|
306
|
+
writeFileSync(synthPath, synthContent.replace(/- \[ \]/g, "- [x]"), "utf-8");
|
|
307
|
+
|
|
308
|
+
ctx.ui.select.mockResolvedValueOnce("Approve implementation");
|
|
309
|
+
|
|
310
|
+
const result3 = await ppPhaseComplete.execute("call-3", { summary: "All items implemented" }, undefined, undefined, ctx);
|
|
311
|
+
expect(result3.content[0].text).toContain("Task completed");
|
|
312
|
+
|
|
313
|
+
expect(orchestrator.active).toBeNull();
|
|
314
|
+
|
|
315
|
+
const finalState = loadTask(taskDir);
|
|
316
|
+
expect(finalState.phase).toBe("done");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("blocks brainstorm→plan transition without artifacts", async () => {
|
|
320
|
+
const cwd = makeTempDir();
|
|
321
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
322
|
+
const ctx = makeCtx();
|
|
323
|
+
|
|
324
|
+
await orchestrator.startTask(ctx as any, "implement", "Test task");
|
|
325
|
+
|
|
326
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
327
|
+
|
|
328
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
329
|
+
const result = await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
330
|
+
expect(result.content[0].text).toContain("Transition blocked");
|
|
331
|
+
expect(result.content[0].text).toContain("USER_REQUEST.md");
|
|
332
|
+
expect(orchestrator.active!.state.phase).toBe("brainstorm");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("blocks implement→done transition with unchecked items", async () => {
|
|
336
|
+
const cwd = makeTempDir();
|
|
337
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
338
|
+
const ctx = makeCtx();
|
|
339
|
+
|
|
340
|
+
await orchestrator.startTask(ctx as any, "implement", "Test task");
|
|
341
|
+
const taskDir = orchestrator.active!.dir;
|
|
342
|
+
|
|
343
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
344
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
345
|
+
|
|
346
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
347
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
348
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
349
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
350
|
+
|
|
351
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
352
|
+
const plansDir = join(taskDir, "plans");
|
|
353
|
+
mkdirSync(plansDir, { recursive: true });
|
|
354
|
+
writeFileSync(
|
|
355
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
356
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
357
|
+
"utf-8",
|
|
358
|
+
);
|
|
359
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
360
|
+
|
|
361
|
+
writeFileSync(
|
|
362
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
363
|
+
makeValidPlan(["- [ ] P1. Unchecked item — Done when: this item remains unchecked"]),
|
|
364
|
+
"utf-8",
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
ctx.ui.select.mockResolvedValueOnce("Approve plan");
|
|
368
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
369
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
370
|
+
|
|
371
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
372
|
+
|
|
373
|
+
ctx.ui.select.mockResolvedValueOnce("Approve implementation");
|
|
374
|
+
const result = await ppPhaseComplete.execute("call-3", { summary: "done" }, undefined, undefined, ctx);
|
|
375
|
+
expect(result.content[0].text).toContain("Transition blocked");
|
|
376
|
+
expect(result.content[0].text).toContain("unchecked");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("review cycle lifecycle", () => {
|
|
381
|
+
it("auto review cycle: spawn → complete → apply_feedback → user gate", async () => {
|
|
382
|
+
const cwd = makeTempDir();
|
|
383
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
384
|
+
const ctx = makeCtx();
|
|
385
|
+
|
|
386
|
+
await orchestrator.startTask(ctx as any, "implement", "Test review");
|
|
387
|
+
const taskDir = orchestrator.active!.dir;
|
|
388
|
+
|
|
389
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
390
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
391
|
+
|
|
392
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
393
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
394
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
395
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
396
|
+
|
|
397
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
398
|
+
const plansDir = join(taskDir, "plans");
|
|
399
|
+
mkdirSync(plansDir, { recursive: true });
|
|
400
|
+
writeFileSync(
|
|
401
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
402
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
403
|
+
"utf-8",
|
|
404
|
+
);
|
|
405
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
406
|
+
|
|
407
|
+
writeFileSync(
|
|
408
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
409
|
+
makeValidPlan(["- [x] P1. Done item — Done when: synthesized plan is fully checked"]),
|
|
410
|
+
"utf-8",
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
ctx.ui.select.mockResolvedValueOnce("Approve plan");
|
|
414
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
415
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
416
|
+
|
|
417
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
418
|
+
|
|
419
|
+
ctx.ui.select.mockResolvedValueOnce("Review");
|
|
420
|
+
const result = await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
421
|
+
expect(result.content[0].text).toContain("Awaiting reviewers");
|
|
422
|
+
|
|
423
|
+
expect(orchestrator.active!.state.reviewCycle).not.toBeNull();
|
|
424
|
+
expect(["await_reviewers", "apply_feedback"]).toContain(orchestrator.active!.state.reviewCycle!.step);
|
|
425
|
+
expect(orchestrator.active!.state.reviewCycle!.pass).toBe(1);
|
|
426
|
+
|
|
427
|
+
const reviewsDir = join(taskDir, "code-reviews");
|
|
428
|
+
mkdirSync(reviewsDir, { recursive: true });
|
|
429
|
+
writeFileSync(join(reviewsDir, `${Math.floor(Date.now() / 1000)}_test_round-1.md`), "LGTM", "utf-8");
|
|
430
|
+
|
|
431
|
+
emitSubagentCreated(pi, "reviewer-1", "Code reviewer (test)");
|
|
432
|
+
emitSubagentCompleted(pi, "reviewer-1", "Code reviewer (test)");
|
|
433
|
+
|
|
434
|
+
expect(orchestrator.active!.state.reviewCycle!.step).toBe("apply_feedback");
|
|
435
|
+
expect(orchestrator.active!.state.step).toBe("apply_feedback");
|
|
436
|
+
|
|
437
|
+
ctx.ui.select.mockResolvedValueOnce("Approve implementation");
|
|
438
|
+
const result4 = await ppPhaseComplete.execute("call-4", { summary: "feedback applied" }, undefined, undefined, ctx);
|
|
439
|
+
|
|
440
|
+
expect(orchestrator.active).toBeNull();
|
|
441
|
+
expect(result4.content[0].text).toContain("Task completed");
|
|
442
|
+
|
|
443
|
+
const finalState = loadTask(taskDir);
|
|
444
|
+
expect(finalState.phase).toBe("done");
|
|
445
|
+
expect(finalState.reviewPass).toBe(1);
|
|
446
|
+
expect(finalState.reviewCycle).toBeNull();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("review cycle completes even when all reviewers fail", async () => {
|
|
450
|
+
const cwd = makeTempDir();
|
|
451
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
452
|
+
const ctx = makeCtx();
|
|
453
|
+
|
|
454
|
+
await orchestrator.startTask(ctx as any, "implement", "Test failure");
|
|
455
|
+
const taskDir = orchestrator.active!.dir;
|
|
456
|
+
|
|
457
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
458
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
459
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
460
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
461
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
462
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
463
|
+
|
|
464
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
465
|
+
const plansDir = join(taskDir, "plans");
|
|
466
|
+
mkdirSync(plansDir, { recursive: true });
|
|
467
|
+
writeFileSync(
|
|
468
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
469
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
470
|
+
"utf-8",
|
|
471
|
+
);
|
|
472
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
473
|
+
|
|
474
|
+
writeFileSync(
|
|
475
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
476
|
+
makeValidPlan(["- [x] P1. Done item — Done when: synthesized plan is fully checked"]),
|
|
477
|
+
"utf-8",
|
|
478
|
+
);
|
|
479
|
+
ctx.ui.select.mockResolvedValueOnce("Approve plan");
|
|
480
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
481
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
482
|
+
|
|
483
|
+
ctx.ui.select.mockResolvedValueOnce("Review");
|
|
484
|
+
await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
485
|
+
|
|
486
|
+
emitSubagentCreated(pi, "reviewer-1", "Code reviewer (test)");
|
|
487
|
+
emitSubagentFailed(pi, "reviewer-1", "model error");
|
|
488
|
+
|
|
489
|
+
expect(orchestrator.active!.state.reviewCycle!.step).toBe("apply_feedback");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("zero enabled reviewers returns error without stalling", async () => {
|
|
493
|
+
const cwd = makeTempDir();
|
|
494
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
495
|
+
const ctx = makeCtx();
|
|
496
|
+
|
|
497
|
+
await orchestrator.startTask(ctx as any, "implement", "Test no reviewers");
|
|
498
|
+
orchestrator.config = { ...orchestrator.config, codeReviewers: {} } as any;
|
|
499
|
+
const taskDir = orchestrator.active!.dir;
|
|
500
|
+
|
|
501
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
502
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
503
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
504
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
505
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
506
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
507
|
+
|
|
508
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
509
|
+
const plansDir = join(taskDir, "plans");
|
|
510
|
+
mkdirSync(plansDir, { recursive: true });
|
|
511
|
+
writeFileSync(
|
|
512
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
513
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
514
|
+
"utf-8",
|
|
515
|
+
);
|
|
516
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
517
|
+
|
|
518
|
+
writeFileSync(
|
|
519
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
520
|
+
makeValidPlan(["- [x] P1. Done item — Done when: synthesized plan is fully checked"]),
|
|
521
|
+
"utf-8",
|
|
522
|
+
);
|
|
523
|
+
ctx.ui.select.mockResolvedValueOnce("Approve plan");
|
|
524
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
525
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
526
|
+
|
|
527
|
+
ctx.ui.select.mockResolvedValueOnce("Review");
|
|
528
|
+
ctx.ui.select.mockResolvedValueOnce("Continue implementation");
|
|
529
|
+
const result = await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
530
|
+
|
|
531
|
+
expect(result.content[0].text).toContain("continue");
|
|
532
|
+
expect(orchestrator.active!.state.reviewCycle).toBeNull();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
describe("subagent instrumentation", () => {
|
|
537
|
+
it("tracks created started and first progress milestones for spawned agents", async () => {
|
|
538
|
+
const cwd = makeTempDir();
|
|
539
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
540
|
+
const ctx = makeCtx();
|
|
541
|
+
|
|
542
|
+
await orchestrator.startTask(ctx as any, "implement", "Trace agents");
|
|
543
|
+
|
|
544
|
+
emitSubagentCreated(pi, "explore-1", "Explore agent");
|
|
545
|
+
expect(orchestrator.agentLifecycle.get("explore-1")?.createdAt).toBeTypeOf("number");
|
|
546
|
+
expect(orchestrator.agentLifecycle.get("explore-1")?.phase).toBe("brainstorm");
|
|
547
|
+
const lifecycleLogPath = join(orchestrator.active!.dir, "subagent-lifecycle.jsonl");
|
|
548
|
+
expect(existsSync(lifecycleLogPath)).toBe(true);
|
|
549
|
+
|
|
550
|
+
emitSubagentStarted(pi, "explore-1", "Explore agent");
|
|
551
|
+
expect(orchestrator.agentLifecycle.get("explore-1")?.startedAt).toBeTypeOf("number");
|
|
552
|
+
|
|
553
|
+
emitSubagentFirstTool(pi, "explore-1", "Explore agent", "grep");
|
|
554
|
+
expect(orchestrator.agentLifecycle.get("explore-1")?.firstToolAt).toBeTypeOf("number");
|
|
555
|
+
|
|
556
|
+
emitSubagentFirstTurn(pi, "explore-1", "Explore agent", 1);
|
|
557
|
+
expect(orchestrator.agentLifecycle.get("explore-1")?.firstTurnAt).toBeTypeOf("number");
|
|
558
|
+
|
|
559
|
+
emitSubagentCompleted(pi, "explore-1", "Explore agent");
|
|
560
|
+
expect(orchestrator.agentLifecycle.has("explore-1")).toBe(false);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe("subagent tracking", () => {
|
|
565
|
+
it("blocks pp_phase_complete while subagents are running", async () => {
|
|
566
|
+
const cwd = makeTempDir();
|
|
567
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
568
|
+
const ctx = makeCtx();
|
|
569
|
+
|
|
570
|
+
await orchestrator.startTask(ctx as any, "implement", "Test blocking");
|
|
571
|
+
const taskDir = orchestrator.active!.dir;
|
|
572
|
+
|
|
573
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
574
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
575
|
+
|
|
576
|
+
emitSubagentCreated(pi, "explore-1", "Explore agent");
|
|
577
|
+
|
|
578
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
579
|
+
const result = await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
580
|
+
expect(result.content[0].text).toContain("subagent(s) still running");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("pendingSubagentSpawns blocks pp_phase_complete", async () => {
|
|
584
|
+
const cwd = makeTempDir();
|
|
585
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
586
|
+
const ctx = makeCtx();
|
|
587
|
+
|
|
588
|
+
await orchestrator.startTask(ctx as any, "implement", "Test pending");
|
|
589
|
+
const taskDir = orchestrator.active!.dir;
|
|
590
|
+
|
|
591
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
592
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
593
|
+
|
|
594
|
+
orchestrator.pendingSubagentSpawns = 2;
|
|
595
|
+
|
|
596
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
597
|
+
const result = await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
598
|
+
expect(result.content[0].text).toContain("subagent(s) still running");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("abortAllSubagents resets pendingSubagentSpawns", async () => {
|
|
602
|
+
const cwd = makeTempDir();
|
|
603
|
+
const { orchestrator } = await setupOrchestrator(cwd);
|
|
604
|
+
|
|
605
|
+
orchestrator.spawnedAgentIds.add("agent-1");
|
|
606
|
+
orchestrator.pendingSubagentSpawns = 3;
|
|
607
|
+
|
|
608
|
+
orchestrator.abortAllSubagents();
|
|
609
|
+
|
|
610
|
+
expect(orchestrator.spawnedAgentIds.size).toBe(0);
|
|
611
|
+
expect(orchestrator.pendingSubagentSpawns).toBe(0);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe("standalone brainstorm", () => {
|
|
616
|
+
it("can finish without artifacts (escape hatch)", async () => {
|
|
617
|
+
const cwd = makeTempDir();
|
|
618
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
619
|
+
const ctx = makeCtx();
|
|
620
|
+
|
|
621
|
+
await orchestrator.startTask(ctx as any, "brainstorm", "Explore ideas");
|
|
622
|
+
|
|
623
|
+
expect(orchestrator.active!.state.phase).toBe("brainstorm");
|
|
624
|
+
expect(orchestrator.active!.type).toBe("brainstorm");
|
|
625
|
+
|
|
626
|
+
ctx.ui.select.mockResolvedValueOnce("Finish brainstorming");
|
|
627
|
+
|
|
628
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
629
|
+
const result = await ppPhaseComplete.execute("call-1", { summary: "Explored ideas" }, undefined, undefined, ctx);
|
|
630
|
+
expect(result.content[0].text).toContain("Brainstorm finished");
|
|
631
|
+
|
|
632
|
+
expect(orchestrator.active).toBeNull();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("offers 'Start implementation' when artifacts exist", async () => {
|
|
636
|
+
const cwd = makeTempDir();
|
|
637
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
638
|
+
const ctx = makeCtx();
|
|
639
|
+
|
|
640
|
+
await orchestrator.startTask(ctx as any, "brainstorm", "Explore ideas");
|
|
641
|
+
const taskDir = orchestrator.active!.dir;
|
|
642
|
+
|
|
643
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
644
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
645
|
+
|
|
646
|
+
ctx.ui.select.mockResolvedValueOnce("Start implementation");
|
|
647
|
+
|
|
648
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
649
|
+
await ppPhaseComplete.execute("call-1", { summary: "Conclusions ready" }, undefined, undefined, ctx);
|
|
650
|
+
|
|
651
|
+
expect(orchestrator.active!.type).toBe("implement");
|
|
652
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
653
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
describe("debug flow", () => {
|
|
658
|
+
it("finishes debug and can start implementation", async () => {
|
|
659
|
+
const cwd = makeTempDir();
|
|
660
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
661
|
+
const ctx = makeCtx();
|
|
662
|
+
|
|
663
|
+
await orchestrator.startTask(ctx as any, "debug", "Fix timeout bug");
|
|
664
|
+
|
|
665
|
+
expect(orchestrator.active!.state.phase).toBe("debug");
|
|
666
|
+
expect(orchestrator.active!.type).toBe("debug");
|
|
667
|
+
|
|
668
|
+
const taskDir = orchestrator.active!.dir;
|
|
669
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
670
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
671
|
+
|
|
672
|
+
ctx.ui.select.mockResolvedValueOnce("Implement a fix");
|
|
673
|
+
|
|
674
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
675
|
+
await ppPhaseComplete.execute("call-1", { summary: "Diagnosis complete" }, undefined, undefined, ctx);
|
|
676
|
+
|
|
677
|
+
expect(orchestrator.active!.type).toBe("implement");
|
|
678
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
679
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
describe("planner completion tracking", () => {
|
|
684
|
+
it("transitions await_planners → synthesize when all planners complete", async () => {
|
|
685
|
+
const cwd = makeTempDir();
|
|
686
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
687
|
+
const ctx = makeCtx();
|
|
688
|
+
|
|
689
|
+
await orchestrator.startTask(ctx as any, "implement", "Test planners");
|
|
690
|
+
const taskDir = orchestrator.active!.dir;
|
|
691
|
+
|
|
692
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
693
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
694
|
+
|
|
695
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
696
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
697
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
698
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
699
|
+
|
|
700
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
701
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
702
|
+
|
|
703
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
704
|
+
expect(orchestrator.pendingSubagentSpawns).toBe(0);
|
|
705
|
+
|
|
706
|
+
const plansDir = join(taskDir, "plans");
|
|
707
|
+
mkdirSync(plansDir, { recursive: true });
|
|
708
|
+
writeFileSync(
|
|
709
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
710
|
+
makeValidPlan(["- [ ] P1. Plan content item — Done when: planner plan is written"]),
|
|
711
|
+
"utf-8",
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
715
|
+
expect(orchestrator.active!.state.step).toBe("synthesize");
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("transitions await_planners → synthesize when planner fails", async () => {
|
|
719
|
+
const cwd = makeTempDir();
|
|
720
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
721
|
+
const ctx = makeCtx();
|
|
722
|
+
|
|
723
|
+
await orchestrator.startTask(ctx as any, "implement", "Test planner fail");
|
|
724
|
+
const taskDir = orchestrator.active!.dir;
|
|
725
|
+
|
|
726
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
727
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
728
|
+
|
|
729
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
730
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
731
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
732
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
733
|
+
|
|
734
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
735
|
+
emitSubagentFailed(pi, "planner-1", "model error");
|
|
736
|
+
|
|
737
|
+
expect(orchestrator.active!.state.step).toBe("synthesize");
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("plan transition sets await_planners before compaction callback runs", async () => {
|
|
741
|
+
const cwd = makeTempDir();
|
|
742
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
743
|
+
const compactCallbacks: Array<() => void> = [];
|
|
744
|
+
const ctx = makeCtx({
|
|
745
|
+
compact: vi.fn((opts?: any) => {
|
|
746
|
+
if (opts?.onComplete) compactCallbacks.push(opts.onComplete);
|
|
747
|
+
}),
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
await orchestrator.startTask(ctx as any, "implement", "Test task");
|
|
751
|
+
const taskDir = orchestrator.active!.dir;
|
|
752
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
753
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
754
|
+
|
|
755
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
756
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
757
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
758
|
+
|
|
759
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
760
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
761
|
+
expect(compactCallbacks).toHaveLength(1);
|
|
762
|
+
|
|
763
|
+
compactCallbacks[0]!();
|
|
764
|
+
|
|
765
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
766
|
+
const planBeginMessages = pi.sendUserMessage.mock.calls.filter(
|
|
767
|
+
(c: any[]) => c[0] === "[PI-PI] Entered plan phase. Begin working.",
|
|
768
|
+
);
|
|
769
|
+
expect(planBeginMessages).toHaveLength(0);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("shows planner failure dialog and can proceed with available outputs", async () => {
|
|
773
|
+
const cwd = makeTempDir();
|
|
774
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
775
|
+
const ctx = makeCtx();
|
|
776
|
+
|
|
777
|
+
await orchestrator.startTask(ctx as any, "implement", "Test planner dialog");
|
|
778
|
+
const taskDir = orchestrator.active!.dir;
|
|
779
|
+
|
|
780
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
781
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
782
|
+
|
|
783
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
784
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
785
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
786
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
787
|
+
|
|
788
|
+
const plansDir = join(taskDir, "plans");
|
|
789
|
+
mkdirSync(plansDir, { recursive: true });
|
|
790
|
+
writeFileSync(
|
|
791
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
792
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
793
|
+
"utf-8",
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
797
|
+
orchestrator.failedPlannerVariants = ["test"];
|
|
798
|
+
orchestrator.lastCtx = ctx;
|
|
799
|
+
ctx.ui.custom.mockResolvedValueOnce({ kind: "selection", selections: ["Work with available planner outputs"] });
|
|
800
|
+
|
|
801
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
802
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
803
|
+
|
|
804
|
+
expect(orchestrator.active!.state.step).toBe("synthesize");
|
|
805
|
+
expect(orchestrator.failedPlannerVariants).toEqual([]);
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
describe("reviewer failure handling", () => {
|
|
810
|
+
it("shows reviewer failure dialog and can skip review", async () => {
|
|
811
|
+
const cwd = makeTempDir();
|
|
812
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
813
|
+
const ctx = makeCtx();
|
|
814
|
+
|
|
815
|
+
await orchestrator.startTask(ctx as any, "implement", "Test reviewer dialog");
|
|
816
|
+
const taskDir = orchestrator.active!.dir;
|
|
817
|
+
|
|
818
|
+
orchestrator.active!.state.phase = "implement";
|
|
819
|
+
orchestrator.active!.state.step = "await_reviewers";
|
|
820
|
+
orchestrator.active!.state.reviewCycle = { kind: "auto", step: "await_reviewers", pass: 1 };
|
|
821
|
+
saveTask(taskDir, orchestrator.active!.state);
|
|
822
|
+
|
|
823
|
+
orchestrator.pendingSubagentSpawns = 0;
|
|
824
|
+
orchestrator.failedReviewerVariants = ["test"];
|
|
825
|
+
orchestrator.lastCtx = ctx;
|
|
826
|
+
ctx.ui.custom.mockResolvedValueOnce({ kind: "selection", selections: ["Continue without review"] });
|
|
827
|
+
|
|
828
|
+
emitSubagentCompleted(pi, "reviewer-1", "Code reviewer (test)");
|
|
829
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
830
|
+
|
|
831
|
+
expect(orchestrator.active!.state.reviewCycle).toBeNull();
|
|
832
|
+
expect(orchestrator.active!.state.step).toBe("user_gate");
|
|
833
|
+
expect(orchestrator.failedReviewerVariants).toEqual([]);
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
describe("pp:done cancellation", () => {
|
|
838
|
+
it("marks task done and cleans up", async () => {
|
|
839
|
+
const cwd = makeTempDir();
|
|
840
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
841
|
+
const ctx = makeCtx();
|
|
842
|
+
|
|
843
|
+
await orchestrator.startTask(ctx as any, "implement", "Test done");
|
|
844
|
+
const taskDir = orchestrator.active!.dir;
|
|
845
|
+
|
|
846
|
+
const ppDone = getCommand(pi, "pp:done");
|
|
847
|
+
await ppDone(undefined, ctx);
|
|
848
|
+
|
|
849
|
+
expect(orchestrator.active).toBeNull();
|
|
850
|
+
const state = loadTask(taskDir);
|
|
851
|
+
expect(state.phase).toBe("done");
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
describe("edge cases and regressions", () => {
|
|
856
|
+
it("starting new task while another is active finishes the old one", async () => {
|
|
857
|
+
const cwd = makeTempDir();
|
|
858
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
859
|
+
const ctx = makeCtx();
|
|
860
|
+
|
|
861
|
+
await orchestrator.startTask(ctx as any, "implement", "First task");
|
|
862
|
+
const firstDir = orchestrator.active!.dir;
|
|
863
|
+
|
|
864
|
+
await orchestrator.startTask(ctx as any, "implement", "Second task");
|
|
865
|
+
const secondDir = orchestrator.active!.dir;
|
|
866
|
+
|
|
867
|
+
expect(firstDir).not.toBe(secondDir);
|
|
868
|
+
expect(orchestrator.active!.description).toBe("Second task");
|
|
869
|
+
|
|
870
|
+
const firstState = loadTask(firstDir);
|
|
871
|
+
expect(firstState.phase).toBe("done");
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("pp:done during review cycle cleans up properly", async () => {
|
|
875
|
+
const cwd = makeTempDir();
|
|
876
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
877
|
+
const ctx = makeCtx();
|
|
878
|
+
|
|
879
|
+
await orchestrator.startTask(ctx as any, "implement", "Test review done");
|
|
880
|
+
const taskDir = orchestrator.active!.dir;
|
|
881
|
+
|
|
882
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
883
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
884
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
885
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
886
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
887
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
888
|
+
|
|
889
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
890
|
+
const plansDir = join(taskDir, "plans");
|
|
891
|
+
mkdirSync(plansDir, { recursive: true });
|
|
892
|
+
writeFileSync(
|
|
893
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
894
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
895
|
+
"utf-8",
|
|
896
|
+
);
|
|
897
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
898
|
+
|
|
899
|
+
writeFileSync(
|
|
900
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
901
|
+
makeValidPlan(["- [x] P1. Done item — Done when: synthesized plan is fully checked"]),
|
|
902
|
+
"utf-8",
|
|
903
|
+
);
|
|
904
|
+
ctx.ui.select.mockResolvedValueOnce("Approve plan");
|
|
905
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
906
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
907
|
+
|
|
908
|
+
ctx.ui.select.mockResolvedValueOnce("Review");
|
|
909
|
+
await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
910
|
+
|
|
911
|
+
expect(orchestrator.active!.state.reviewCycle).not.toBeNull();
|
|
912
|
+
expect(["await_reviewers", "apply_feedback"]).toContain(orchestrator.active!.state.reviewCycle!.step);
|
|
913
|
+
|
|
914
|
+
const ppDone = getCommand(pi, "pp:done");
|
|
915
|
+
await ppDone(undefined, ctx);
|
|
916
|
+
|
|
917
|
+
expect(orchestrator.active).toBeNull();
|
|
918
|
+
expect(orchestrator.spawnedAgentIds.size).toBe(0);
|
|
919
|
+
expect(orchestrator.pendingSubagentSpawns).toBe(0);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it("multiple review passes increment reviewPass correctly", async () => {
|
|
923
|
+
const cwd = makeTempDir();
|
|
924
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
925
|
+
const ctx = makeCtx();
|
|
926
|
+
|
|
927
|
+
await orchestrator.startTask(ctx as any, "implement", "Multi-review test");
|
|
928
|
+
const taskDir = orchestrator.active!.dir;
|
|
929
|
+
|
|
930
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
931
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
932
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
933
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
934
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
935
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
936
|
+
|
|
937
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
938
|
+
const plansDir = join(taskDir, "plans");
|
|
939
|
+
mkdirSync(plansDir, { recursive: true });
|
|
940
|
+
writeFileSync(
|
|
941
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
942
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
943
|
+
"utf-8",
|
|
944
|
+
);
|
|
945
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
946
|
+
|
|
947
|
+
writeFileSync(
|
|
948
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
949
|
+
makeValidPlan(["- [x] P1. Done item — Done when: synthesized plan is fully checked"]),
|
|
950
|
+
"utf-8",
|
|
951
|
+
);
|
|
952
|
+
ctx.ui.select.mockResolvedValueOnce("Approve plan");
|
|
953
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
954
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
955
|
+
|
|
956
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
957
|
+
|
|
958
|
+
ctx.ui.select.mockResolvedValueOnce("Review");
|
|
959
|
+
await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
960
|
+
|
|
961
|
+
const reviewsDir = join(taskDir, "code-reviews");
|
|
962
|
+
mkdirSync(reviewsDir, { recursive: true });
|
|
963
|
+
writeFileSync(join(reviewsDir, `${Math.floor(Date.now() / 1000)}_test_round-1.md`), "Needs fixes", "utf-8");
|
|
964
|
+
|
|
965
|
+
emitSubagentCreated(pi, "reviewer-1", "Code reviewer (test)");
|
|
966
|
+
emitSubagentCompleted(pi, "reviewer-1", "Code reviewer (test)");
|
|
967
|
+
|
|
968
|
+
expect(orchestrator.active!.state.reviewCycle!.step).toBe("apply_feedback");
|
|
969
|
+
|
|
970
|
+
ctx.ui.select.mockResolvedValueOnce("Review");
|
|
971
|
+
await ppPhaseComplete.execute("call-4", { summary: "fixes applied" }, undefined, undefined, ctx);
|
|
972
|
+
|
|
973
|
+
expect(orchestrator.active!.state.reviewPass).toBe(1);
|
|
974
|
+
expect(orchestrator.active!.state.reviewPassByKind?.auto).toBe(1);
|
|
975
|
+
expect(orchestrator.active!.state.reviewCycle).not.toBeNull();
|
|
976
|
+
expect(orchestrator.active!.state.reviewCycle!.pass).toBe(2);
|
|
977
|
+
|
|
978
|
+
writeFileSync(join(reviewsDir, `${Math.floor(Date.now() / 1000)}_test_round-2.md`), "LGTM", "utf-8");
|
|
979
|
+
|
|
980
|
+
emitSubagentCreated(pi, "reviewer-2", "Code reviewer (test)");
|
|
981
|
+
emitSubagentCompleted(pi, "reviewer-2", "Code reviewer (test)");
|
|
982
|
+
|
|
983
|
+
ctx.ui.select.mockResolvedValueOnce("Approve implementation");
|
|
984
|
+
const result = await ppPhaseComplete.execute("call-5", { summary: "all good" }, undefined, undefined, ctx);
|
|
985
|
+
|
|
986
|
+
expect(result.content[0].text).toContain("Task completed");
|
|
987
|
+
expect(orchestrator.active).toBeNull();
|
|
988
|
+
|
|
989
|
+
const finalState = loadTask(taskDir);
|
|
990
|
+
expect(finalState.reviewPass).toBe(2);
|
|
991
|
+
expect(finalState.reviewCycle).toBeNull();
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it("continue brainstorming sets step back to llm_work", async () => {
|
|
995
|
+
const cwd = makeTempDir();
|
|
996
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
997
|
+
const ctx = makeCtx();
|
|
998
|
+
|
|
999
|
+
await orchestrator.startTask(ctx as any, "implement", "Continue test");
|
|
1000
|
+
|
|
1001
|
+
ctx.ui.select.mockResolvedValueOnce("Continue brainstorming");
|
|
1002
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1003
|
+
const result = await ppPhaseComplete.execute("call-1", { summary: "not done yet" }, undefined, undefined, ctx);
|
|
1004
|
+
|
|
1005
|
+
expect(result.content[0].text).toContain("continue");
|
|
1006
|
+
expect(orchestrator.active!.state.phase).toBe("brainstorm");
|
|
1007
|
+
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it("continue implementation sets step back to llm_work", async () => {
|
|
1011
|
+
const cwd = makeTempDir();
|
|
1012
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1013
|
+
const ctx = makeCtx();
|
|
1014
|
+
|
|
1015
|
+
await orchestrator.startTask(ctx as any, "implement", "Continue impl test");
|
|
1016
|
+
const taskDir = orchestrator.active!.dir;
|
|
1017
|
+
|
|
1018
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1019
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1020
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
1021
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1022
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1023
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1024
|
+
|
|
1025
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
1026
|
+
const plansDir = join(taskDir, "plans");
|
|
1027
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1028
|
+
writeFileSync(
|
|
1029
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
1030
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
1031
|
+
"utf-8",
|
|
1032
|
+
);
|
|
1033
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
1034
|
+
|
|
1035
|
+
writeFileSync(
|
|
1036
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
1037
|
+
makeValidPlan(["- [ ] P1. Todo item — Done when: item intentionally remains unchecked"]),
|
|
1038
|
+
"utf-8",
|
|
1039
|
+
);
|
|
1040
|
+
ctx.ui.select.mockResolvedValueOnce("Approve plan");
|
|
1041
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
1042
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1043
|
+
|
|
1044
|
+
ctx.ui.select.mockResolvedValueOnce("Continue implementation");
|
|
1045
|
+
const result = await ppPhaseComplete.execute("call-3", { summary: "partial work" }, undefined, undefined, ctx);
|
|
1046
|
+
|
|
1047
|
+
expect(result.content[0].text).toContain("continue");
|
|
1048
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
1049
|
+
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
it("review on my own sets step back to synthesize for plan phase", async () => {
|
|
1053
|
+
const cwd = makeTempDir();
|
|
1054
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1055
|
+
const ctx = makeCtx();
|
|
1056
|
+
|
|
1057
|
+
await orchestrator.startTask(ctx as any, "implement", "Review own plan");
|
|
1058
|
+
const taskDir = orchestrator.active!.dir;
|
|
1059
|
+
|
|
1060
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1061
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1062
|
+
ctx.ui.select.mockResolvedValueOnce("Approve brainstorm");
|
|
1063
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1064
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1065
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1066
|
+
|
|
1067
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
1068
|
+
const plansDir = join(taskDir, "plans");
|
|
1069
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1070
|
+
writeFileSync(
|
|
1071
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
1072
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
1073
|
+
"utf-8",
|
|
1074
|
+
);
|
|
1075
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
1076
|
+
|
|
1077
|
+
writeFileSync(
|
|
1078
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
1079
|
+
makeValidPlan(["- [ ] P1. Todo item — Done when: item intentionally remains unchecked"]),
|
|
1080
|
+
"utf-8",
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
ctx.ui.select.mockResolvedValueOnce("Review on my own");
|
|
1084
|
+
const result = await ppPhaseComplete.execute("call-2", { summary: "plan ready" }, undefined, undefined, ctx);
|
|
1085
|
+
|
|
1086
|
+
expect(result.content[0].text).toContain("continue");
|
|
1087
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
1088
|
+
expect(orchestrator.active!.state.step).toBe("synthesize");
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it("generic description task does not auto-trigger agent", async () => {
|
|
1092
|
+
const cwd = makeTempDir();
|
|
1093
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1094
|
+
const ctx = makeCtx();
|
|
1095
|
+
|
|
1096
|
+
await orchestrator.startTask(ctx as any, "implement", "implement");
|
|
1097
|
+
|
|
1098
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Task created. Describe what you'd like to do.", "info");
|
|
1099
|
+
const sendUserCalls = pi.sendUserMessage.mock.calls.filter(
|
|
1100
|
+
(c: any[]) => typeof c[0] === "string" && c[0].includes("Begin working"),
|
|
1101
|
+
);
|
|
1102
|
+
expect(sendUserCalls.length).toBe(0);
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it("implement --from debug skips brainstorm", async () => {
|
|
1106
|
+
const cwd = makeTempDir();
|
|
1107
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1108
|
+
const ctx = makeCtx();
|
|
1109
|
+
|
|
1110
|
+
await orchestrator.startTask(ctx as any, "debug", "Find bug");
|
|
1111
|
+
const debugDir = orchestrator.active!.dir;
|
|
1112
|
+
writeFileSync(join(debugDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1113
|
+
writeFileSync(join(debugDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1114
|
+
|
|
1115
|
+
await orchestrator.startTask(ctx as any, "implement", "Fix it", debugDir, true);
|
|
1116
|
+
|
|
1117
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
1118
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
1119
|
+
expect(existsSync(join(orchestrator.active!.dir, "USER_REQUEST.md"))).toBe(true);
|
|
1120
|
+
expect(existsSync(join(orchestrator.active!.dir, "RESEARCH.md"))).toBe(true);
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it("implement --from debug with generic description skips blank-task prompt", async () => {
|
|
1124
|
+
const cwd = makeTempDir();
|
|
1125
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1126
|
+
const ctx = makeCtx();
|
|
1127
|
+
|
|
1128
|
+
await orchestrator.startTask(ctx as any, "debug", "Find bug");
|
|
1129
|
+
const debugDir = orchestrator.active!.dir;
|
|
1130
|
+
writeFileSync(join(debugDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1131
|
+
writeFileSync(join(debugDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1132
|
+
|
|
1133
|
+
await orchestrator.startTask(ctx as any, "implement", "implement", debugDir, true);
|
|
1134
|
+
|
|
1135
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith("Task created. Describe what you'd like to do.", "info");
|
|
1136
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
1137
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it("implement --from brainstorm also skips brainstorm and enters plan deterministically", async () => {
|
|
1141
|
+
const cwd = makeTempDir();
|
|
1142
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1143
|
+
const ctx = makeCtx();
|
|
1144
|
+
|
|
1145
|
+
await orchestrator.startTask(ctx as any, "brainstorm", "Explore ideas");
|
|
1146
|
+
const brainstormDir = orchestrator.active!.dir;
|
|
1147
|
+
writeFileSync(join(brainstormDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1148
|
+
writeFileSync(join(brainstormDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1149
|
+
|
|
1150
|
+
const ppImplement = getCommand(pi, "pp:implement");
|
|
1151
|
+
await ppImplement(`--from brainstorm/${brainstormDir.split("/").pop()}`, ctx);
|
|
1152
|
+
|
|
1153
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
1154
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
1155
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith("Task created. Describe what you'd like to do.", "info");
|
|
1156
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
1157
|
+
expect(existsSync(join(orchestrator.active!.dir, "USER_REQUEST.md"))).toBe(true);
|
|
1158
|
+
expect(existsSync(join(orchestrator.active!.dir, "RESEARCH.md"))).toBe(true);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it("pp:implement parses --from=debug syntax and skips brainstorm", async () => {
|
|
1162
|
+
const cwd = makeTempDir();
|
|
1163
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1164
|
+
const ctx = makeCtx();
|
|
1165
|
+
|
|
1166
|
+
await orchestrator.startTask(ctx as any, "debug", "Find bug");
|
|
1167
|
+
const debugDir = orchestrator.active!.dir;
|
|
1168
|
+
writeFileSync(join(debugDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1169
|
+
writeFileSync(join(debugDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1170
|
+
|
|
1171
|
+
const ppImplement = getCommand(pi, "pp:implement");
|
|
1172
|
+
await ppImplement(`--from=debug/${debugDir.split("/").pop()}`, ctx);
|
|
1173
|
+
|
|
1174
|
+
expect(orchestrator.active!.type).toBe("implement");
|
|
1175
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
1176
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
1177
|
+
expect(orchestrator.active!.state.from).toBe(`debug/${debugDir.split("/").pop()}`);
|
|
1178
|
+
expect(orchestrator.active!.state.description).toBe("implement");
|
|
1179
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it("pp:implement parses --from=brainstorm syntax and skips brainstorm", async () => {
|
|
1183
|
+
const cwd = makeTempDir();
|
|
1184
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1185
|
+
const ctx = makeCtx();
|
|
1186
|
+
|
|
1187
|
+
await orchestrator.startTask(ctx as any, "brainstorm", "Explore ideas");
|
|
1188
|
+
const brainstormDir = orchestrator.active!.dir;
|
|
1189
|
+
writeFileSync(join(brainstormDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1190
|
+
writeFileSync(join(brainstormDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1191
|
+
|
|
1192
|
+
const ppImplement = getCommand(pi, "pp:implement");
|
|
1193
|
+
await ppImplement(`--from=brainstorm/${brainstormDir.split("/").pop()}`, ctx);
|
|
1194
|
+
|
|
1195
|
+
expect(orchestrator.active!.type).toBe("implement");
|
|
1196
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
1197
|
+
expect(orchestrator.active!.state.step).toBe("await_planners");
|
|
1198
|
+
expect(orchestrator.active!.state.from).toBe(`brainstorm/${brainstormDir.split("/").pop()}`);
|
|
1199
|
+
expect(orchestrator.active!.state.description).toBe("implement");
|
|
1200
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
it("normalizes legacy planning phase state to plan on load", async () => {
|
|
1204
|
+
const cwd = makeTempDir();
|
|
1205
|
+
const taskDir = createTask(cwd, "implement", "Legacy task");
|
|
1206
|
+
const legacyState = loadTask(taskDir);
|
|
1207
|
+
legacyState.phase = "planning" as any;
|
|
1208
|
+
saveTask(taskDir, legacyState);
|
|
1209
|
+
|
|
1210
|
+
const normalized = loadTask(taskDir);
|
|
1211
|
+
|
|
1212
|
+
expect(normalized.phase).toBe("plan");
|
|
1213
|
+
});
|
|
1214
|
+
});
|