@posthog/agent 1.30.0 → 2.0.1

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.
Files changed (144) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +221 -219
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
  4. package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
  5. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
  6. package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
  7. package/dist/adapters/claude/permissions/permission-options.js +117 -0
  8. package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
  9. package/dist/adapters/claude/questions/utils.d.ts +132 -0
  10. package/dist/adapters/claude/questions/utils.js +63 -0
  11. package/dist/adapters/claude/questions/utils.js.map +1 -0
  12. package/dist/adapters/claude/tools.d.ts +18 -0
  13. package/dist/adapters/claude/tools.js +95 -0
  14. package/dist/adapters/claude/tools.js.map +1 -0
  15. package/dist/agent-DBQY1BfC.d.ts +123 -0
  16. package/dist/agent.d.ts +5 -0
  17. package/dist/agent.js +3656 -0
  18. package/dist/agent.js.map +1 -0
  19. package/dist/claude-cli/cli.js +3695 -2746
  20. package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
  21. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  22. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
  23. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  24. package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
  25. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  26. package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
  27. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  28. package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
  29. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  30. package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
  31. package/dist/gateway-models.d.ts +24 -0
  32. package/dist/gateway-models.js +93 -0
  33. package/dist/gateway-models.js.map +1 -0
  34. package/dist/index.d.ts +172 -1203
  35. package/dist/index.js +3704 -6826
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger-DDBiMOOD.d.ts +24 -0
  38. package/dist/posthog-api.d.ts +40 -0
  39. package/dist/posthog-api.js +175 -0
  40. package/dist/posthog-api.js.map +1 -0
  41. package/dist/server/agent-server.d.ts +41 -0
  42. package/dist/server/agent-server.js +4451 -0
  43. package/dist/server/agent-server.js.map +1 -0
  44. package/dist/server/bin.d.ts +1 -0
  45. package/dist/server/bin.js +4507 -0
  46. package/dist/server/bin.js.map +1 -0
  47. package/dist/types.d.ts +129 -0
  48. package/dist/types.js +1 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +66 -14
  51. package/src/acp-extensions.ts +93 -61
  52. package/src/adapters/acp-connection.ts +494 -0
  53. package/src/adapters/base-acp-agent.ts +150 -0
  54. package/src/adapters/claude/claude-agent.ts +596 -0
  55. package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
  56. package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
  57. package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
  58. package/src/adapters/claude/hooks.ts +64 -0
  59. package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
  60. package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
  61. package/src/adapters/claude/permissions/permission-options.ts +103 -0
  62. package/src/adapters/claude/plan/utils.ts +56 -0
  63. package/src/adapters/claude/questions/utils.ts +92 -0
  64. package/src/adapters/claude/session/commands.ts +38 -0
  65. package/src/adapters/claude/session/mcp-config.ts +37 -0
  66. package/src/adapters/claude/session/models.ts +12 -0
  67. package/src/adapters/claude/session/options.ts +236 -0
  68. package/src/adapters/claude/tool-meta.ts +143 -0
  69. package/src/adapters/claude/tools.ts +53 -611
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +97 -734
  73. package/src/execution-mode.ts +43 -0
  74. package/src/gateway-models.ts +135 -0
  75. package/src/index.ts +79 -0
  76. package/src/otel-log-writer.test.ts +105 -0
  77. package/src/otel-log-writer.ts +94 -0
  78. package/src/posthog-api.ts +75 -235
  79. package/src/resume.ts +115 -0
  80. package/src/sagas/apply-snapshot-saga.test.ts +690 -0
  81. package/src/sagas/apply-snapshot-saga.ts +88 -0
  82. package/src/sagas/capture-tree-saga.test.ts +892 -0
  83. package/src/sagas/capture-tree-saga.ts +141 -0
  84. package/src/sagas/resume-saga.test.ts +558 -0
  85. package/src/sagas/resume-saga.ts +332 -0
  86. package/src/sagas/test-fixtures.ts +250 -0
  87. package/src/server/agent-server.test.ts +220 -0
  88. package/src/server/agent-server.ts +748 -0
  89. package/src/server/bin.ts +88 -0
  90. package/src/server/jwt.ts +65 -0
  91. package/src/server/schemas.ts +47 -0
  92. package/src/server/types.ts +13 -0
  93. package/src/server/utils/retry.test.ts +122 -0
  94. package/src/server/utils/retry.ts +61 -0
  95. package/src/server/utils/sse-parser.test.ts +93 -0
  96. package/src/server/utils/sse-parser.ts +46 -0
  97. package/src/session-log-writer.test.ts +140 -0
  98. package/src/session-log-writer.ts +137 -0
  99. package/src/test/assertions.ts +114 -0
  100. package/src/test/controllers/sse-controller.ts +107 -0
  101. package/src/test/fixtures/api.ts +111 -0
  102. package/src/test/fixtures/config.ts +33 -0
  103. package/src/test/fixtures/notifications.ts +92 -0
  104. package/src/test/mocks/claude-sdk.ts +251 -0
  105. package/src/test/mocks/msw-handlers.ts +48 -0
  106. package/src/test/setup.ts +114 -0
  107. package/src/test/wait.ts +41 -0
  108. package/src/tree-tracker.ts +173 -0
  109. package/src/types.ts +51 -154
  110. package/src/utils/acp-content.ts +58 -0
  111. package/src/utils/async-mutex.test.ts +104 -0
  112. package/src/utils/async-mutex.ts +31 -0
  113. package/src/utils/common.ts +15 -0
  114. package/src/utils/gateway.ts +9 -6
  115. package/src/utils/logger.ts +0 -30
  116. package/src/utils/streams.ts +220 -0
  117. package/CLAUDE.md +0 -331
  118. package/dist/templates/plan-template.md +0 -41
  119. package/src/adapters/claude/claude.ts +0 -1543
  120. package/src/adapters/claude/mcp-server.ts +0 -810
  121. package/src/adapters/claude/utils.ts +0 -267
  122. package/src/agents/execution.ts +0 -37
  123. package/src/agents/planning.ts +0 -60
  124. package/src/agents/research.ts +0 -160
  125. package/src/file-manager.ts +0 -306
  126. package/src/git-manager.ts +0 -577
  127. package/src/prompt-builder.ts +0 -499
  128. package/src/schemas.ts +0 -241
  129. package/src/session-store.ts +0 -259
  130. package/src/task-manager.ts +0 -163
  131. package/src/template-manager.ts +0 -236
  132. package/src/templates/plan-template.md +0 -41
  133. package/src/todo-manager.ts +0 -180
  134. package/src/tools/registry.ts +0 -129
  135. package/src/tools/types.ts +0 -127
  136. package/src/utils/tapped-stream.ts +0 -60
  137. package/src/workflow/config.ts +0 -53
  138. package/src/workflow/steps/build.ts +0 -135
  139. package/src/workflow/steps/finalize.ts +0 -241
  140. package/src/workflow/steps/plan.ts +0 -167
  141. package/src/workflow/steps/research.ts +0 -223
  142. package/src/workflow/types.ts +0 -62
  143. package/src/workflow/utils.ts +0 -53
  144. package/src/worktree-manager.ts +0 -928
@@ -1,60 +0,0 @@
1
- import type { Logger } from "./logger.js";
2
-
3
- type MessageCallback = (line: string) => void;
4
-
5
- export interface TappedStreamOptions {
6
- onMessage: MessageCallback;
7
- logger?: Logger;
8
- }
9
-
10
- /**
11
- * Creates a WritableStream wrapper that taps all newline-delimited messages,
12
- * forwarding each complete line for persistence.
13
- *
14
- * This aligns with ACP's transport model - all messages flow through
15
- * newline-delimited JSON-RPC streams, so we intercept at the transport layer
16
- * and persist everything.
17
- */
18
- export function createTappedWritableStream(
19
- underlying: WritableStream<Uint8Array>,
20
- options: TappedStreamOptions,
21
- ): WritableStream<Uint8Array> {
22
- const { onMessage, logger } = options;
23
- const decoder = new TextDecoder();
24
- let buffer = "";
25
- let _messageCount = 0;
26
-
27
- return new WritableStream({
28
- async write(chunk: Uint8Array) {
29
- // Decode and buffer
30
- buffer += decoder.decode(chunk, { stream: true });
31
-
32
- // Process complete lines (newline-delimited)
33
- const lines = buffer.split("\n");
34
- buffer = lines.pop() ?? "";
35
-
36
- for (const line of lines) {
37
- if (!line.trim()) continue;
38
- _messageCount++;
39
-
40
- onMessage(line);
41
- }
42
-
43
- // Forward to underlying stream
44
- const writer = underlying.getWriter();
45
- await writer.write(chunk);
46
- writer.releaseLock();
47
- },
48
- async close() {
49
- const writer = underlying.getWriter();
50
- await writer.close();
51
- writer.releaseLock();
52
- },
53
- async abort(reason: unknown) {
54
- logger?.warn("Tapped stream aborted", { reason });
55
- const writer = underlying.getWriter();
56
- await writer.abort(reason);
57
- writer.releaseLock();
58
- },
59
- });
60
- }
@@ -1,53 +0,0 @@
1
- import { buildStep } from "./steps/build.js";
2
- import { finalizeStep } from "./steps/finalize.js";
3
- import { planStep } from "./steps/plan.js";
4
- import { researchStep } from "./steps/research.js";
5
- import type { WorkflowDefinition } from "./types.js";
6
-
7
- const MODELS = {
8
- SONNET: "claude-sonnet-4-5",
9
- HAIKU: "claude-haiku-4-5",
10
- };
11
-
12
- export const TASK_WORKFLOW: WorkflowDefinition = [
13
- {
14
- id: "research",
15
- name: "Research",
16
- agent: "research",
17
- model: MODELS.HAIKU,
18
- permissionMode: "plan",
19
- commit: true,
20
- push: true,
21
- run: researchStep,
22
- },
23
- {
24
- id: "plan",
25
- name: "Plan",
26
- agent: "planning",
27
- model: MODELS.SONNET,
28
- permissionMode: "plan",
29
- commit: true,
30
- push: true,
31
- run: planStep,
32
- },
33
- {
34
- id: "build",
35
- name: "Build",
36
- agent: "execution",
37
- model: MODELS.SONNET,
38
- permissionMode: "acceptEdits",
39
- commit: true,
40
- push: true,
41
- run: buildStep,
42
- },
43
- {
44
- id: "finalize",
45
- name: "Finalize",
46
- agent: "system", // not used
47
- model: MODELS.HAIKU, // not used
48
- permissionMode: "plan", // not used
49
- commit: true,
50
- push: true,
51
- run: finalizeStep,
52
- },
53
- ];
@@ -1,135 +0,0 @@
1
- import { query } from "@anthropic-ai/claude-agent-sdk";
2
- import { POSTHOG_NOTIFICATIONS } from "../../acp-extensions.js";
3
- import { EXECUTION_SYSTEM_PROMPT } from "../../agents/execution.js";
4
- import { TodoManager } from "../../todo-manager.js";
5
- import { PermissionMode } from "../../types.js";
6
- import type { WorkflowStepRunner } from "../types.js";
7
-
8
- export const buildStep: WorkflowStepRunner = async ({ step, context }) => {
9
- const {
10
- task,
11
- cwd,
12
- options,
13
- logger,
14
- promptBuilder,
15
- sessionId,
16
- mcpServers,
17
- gitManager,
18
- sendNotification,
19
- } = context;
20
-
21
- const stepLogger = logger.child("BuildStep");
22
-
23
- const latestRun = task.latest_run;
24
- const prExists =
25
- latestRun?.output && typeof latestRun.output === "object"
26
- ? (latestRun.output as Record<string, unknown>).pr_url
27
- : null;
28
-
29
- if (prExists) {
30
- stepLogger.info("PR already exists, skipping build phase", {
31
- taskId: task.id,
32
- });
33
- return { status: "skipped" };
34
- }
35
-
36
- stepLogger.info("Starting build phase", { taskId: task.id });
37
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_START, {
38
- sessionId,
39
- phase: "build",
40
- });
41
-
42
- const executionPrompt = await promptBuilder.buildExecutionPrompt(task, cwd);
43
- const fullPrompt = `${EXECUTION_SYSTEM_PROMPT}\n\n${executionPrompt}`;
44
-
45
- const configuredPermissionMode =
46
- options.permissionMode ??
47
- (typeof step.permissionMode === "string"
48
- ? (step.permissionMode as PermissionMode)
49
- : step.permissionMode) ??
50
- PermissionMode.ACCEPT_EDITS;
51
-
52
- const baseOptions: Record<string, unknown> = {
53
- model: step.model,
54
- cwd,
55
- permissionMode: configuredPermissionMode,
56
- settingSources: ["local"],
57
- mcpServers,
58
- // Allow all tools for build phase - full read/write access needed for implementation
59
- allowedTools: [
60
- "Task",
61
- "Bash",
62
- "BashOutput",
63
- "KillBash",
64
- "Edit",
65
- "Read",
66
- "Write",
67
- "Glob",
68
- "Grep",
69
- "NotebookEdit",
70
- "WebFetch",
71
- "WebSearch",
72
- "ListMcpResources",
73
- "ReadMcpResource",
74
- "TodoWrite",
75
- ],
76
- };
77
-
78
- // Add fine-grained permission hook if provided
79
- if (options.canUseTool) {
80
- baseOptions.canUseTool = options.canUseTool;
81
- }
82
-
83
- const response = query({
84
- prompt: fullPrompt,
85
- options: { ...baseOptions, ...(options.queryOverrides || {}) },
86
- });
87
-
88
- // Track commits made during Claude Code execution
89
- const commitTracker = await gitManager.trackCommitsDuring();
90
-
91
- // Track todos from TodoWrite tool calls
92
- const todoManager = new TodoManager(context.fileManager, stepLogger);
93
-
94
- try {
95
- for await (const message of response) {
96
- const todoList = await todoManager.checkAndPersistFromMessage(
97
- message,
98
- task.id,
99
- );
100
- if (todoList) {
101
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
102
- sessionId,
103
- kind: "todos",
104
- content: todoList,
105
- });
106
- }
107
- }
108
- } catch (error) {
109
- stepLogger.error("Error during build step query", error);
110
- throw error;
111
- }
112
-
113
- // Finalize: commit any remaining changes and optionally push
114
- const { commitCreated, pushedBranch } = await commitTracker.finalize({
115
- commitMessage: `Implementation for ${task.title}`,
116
- push: step.push,
117
- });
118
-
119
- context.stepResults[step.id] = { commitCreated };
120
-
121
- if (!commitCreated) {
122
- stepLogger.warn("No changes to commit in build phase", { taskId: task.id });
123
- } else {
124
- stepLogger.info("Build commits finalized", {
125
- taskId: task.id,
126
- pushedBranch,
127
- });
128
- }
129
-
130
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
131
- sessionId,
132
- phase: "build",
133
- });
134
- return { status: "completed" };
135
- };
@@ -1,241 +0,0 @@
1
- import type { LocalArtifact } from "../../file-manager.js";
2
- import type { Task, TaskRunArtifact } from "../../types.js";
3
- import type { WorkflowStepRunner } from "../types.js";
4
- import { finalizeStepGitActions } from "../utils.js";
5
-
6
- const MAX_SNIPPET_LENGTH = 1200;
7
-
8
- export const finalizeStep: WorkflowStepRunner = async ({ step, context }) => {
9
- const { task, logger, fileManager, gitManager, posthogAPI, runId } = context;
10
-
11
- const stepLogger = logger.child("FinalizeStep");
12
- const artifacts = await fileManager.collectTaskArtifacts(task.id);
13
- let uploadedArtifacts: TaskRunArtifact[] | undefined;
14
-
15
- if (artifacts.length && posthogAPI && runId) {
16
- try {
17
- const payload = artifacts.map((artifact) => ({
18
- name: artifact.name,
19
- type: artifact.type,
20
- content: artifact.content,
21
- content_type: artifact.contentType,
22
- }));
23
- uploadedArtifacts = await posthogAPI.uploadTaskArtifacts(
24
- task.id,
25
- runId,
26
- payload,
27
- );
28
- stepLogger.info("Uploaded task artifacts to PostHog", {
29
- taskId: task.id,
30
- uploadedCount: uploadedArtifacts.length,
31
- });
32
- } catch (error) {
33
- stepLogger.warn("Failed to upload task artifacts", {
34
- taskId: task.id,
35
- error: error instanceof Error ? error.message : String(error),
36
- });
37
- }
38
- } else {
39
- stepLogger.debug("Skipping artifact upload", {
40
- hasArtifacts: artifacts.length > 0,
41
- hasPostHogApi: Boolean(posthogAPI),
42
- runId,
43
- });
44
- }
45
-
46
- const prBody = buildPullRequestBody(task, artifacts, uploadedArtifacts);
47
- await fileManager.cleanupTaskDirectory(task.id);
48
- await gitManager.addAllPostHogFiles();
49
-
50
- // Commit the deletion of artifacts
51
- await finalizeStepGitActions(context, step, {
52
- commitMessage: `Cleanup task artifacts for ${task.title}`,
53
- allowEmptyCommit: true,
54
- });
55
-
56
- context.stepResults[step.id] = {
57
- prBody,
58
- uploadedArtifacts,
59
- artifactCount: artifacts.length,
60
- };
61
-
62
- return { status: "completed" };
63
- };
64
-
65
- function buildPullRequestBody(
66
- task: Task,
67
- artifacts: LocalArtifact[],
68
- uploaded?: TaskRunArtifact[],
69
- ): string {
70
- const lines: string[] = [];
71
- const taskSlug = (task as unknown as Record<string, unknown>).slug || task.id;
72
-
73
- lines.push("## Task context");
74
- lines.push(`- **Task**: ${taskSlug}`);
75
- lines.push(`- **Title**: ${task.title}`);
76
- lines.push(`- **Origin**: ${task.origin_product}`);
77
-
78
- if (task.description) {
79
- lines.push("");
80
- lines.push(`> ${task.description.trim().split("\n").join("\n> ")}`);
81
- }
82
-
83
- const usedFiles = new Set<string>();
84
-
85
- const contextArtifact = artifacts.find(
86
- (artifact) => artifact.name === "context.md",
87
- );
88
- if (contextArtifact) {
89
- lines.push("");
90
- lines.push("### Task prompt");
91
- lines.push(contextArtifact.content);
92
- usedFiles.add(contextArtifact.name);
93
- }
94
-
95
- const researchArtifact = artifacts.find(
96
- (artifact) => artifact.name === "research.json",
97
- );
98
- if (researchArtifact) {
99
- usedFiles.add(researchArtifact.name);
100
- const researchSection = formatResearchSection(researchArtifact.content);
101
- if (researchSection) {
102
- lines.push("");
103
- lines.push(researchSection);
104
- }
105
- }
106
-
107
- const planArtifact = artifacts.find(
108
- (artifact) => artifact.name === "plan.md",
109
- );
110
- if (planArtifact) {
111
- lines.push("");
112
- lines.push("### Implementation plan");
113
- lines.push(planArtifact.content);
114
- usedFiles.add(planArtifact.name);
115
- }
116
-
117
- const todoArtifact = artifacts.find(
118
- (artifact) => artifact.name === "todos.json",
119
- );
120
- if (todoArtifact) {
121
- const summary = summarizeTodos(todoArtifact.content);
122
- if (summary) {
123
- lines.push("");
124
- lines.push("### Todo list");
125
- lines.push(summary);
126
- }
127
- usedFiles.add(todoArtifact.name);
128
- }
129
-
130
- const remainingArtifacts = artifacts.filter(
131
- (artifact) => !usedFiles.has(artifact.name),
132
- );
133
- if (remainingArtifacts.length) {
134
- lines.push("");
135
- lines.push("### Additional artifacts");
136
- for (const artifact of remainingArtifacts) {
137
- lines.push(`#### ${artifact.name}`);
138
- lines.push(renderCodeFence(artifact.content));
139
- }
140
- }
141
-
142
- const artifactList =
143
- uploaded ??
144
- artifacts.map((artifact) => ({
145
- name: artifact.name,
146
- type: artifact.type,
147
- }));
148
-
149
- if (artifactList.length) {
150
- lines.push("");
151
- lines.push("### Uploaded artifacts");
152
- for (const artifact of artifactList) {
153
- const rawStoragePath =
154
- "storage_path" in artifact
155
- ? (artifact as Record<string, unknown>).storage_path
156
- : undefined;
157
- const storagePath =
158
- typeof rawStoragePath === "string" ? rawStoragePath : undefined;
159
- const storage =
160
- storagePath && storagePath.trim().length > 0
161
- ? ` – \`${storagePath.trim()}\``
162
- : "";
163
- lines.push(`- ${artifact.name} (${artifact.type})${storage}`);
164
- }
165
- }
166
-
167
- return lines.join("\n\n");
168
- }
169
-
170
- function renderCodeFence(content: string): string {
171
- const snippet = truncate(content, MAX_SNIPPET_LENGTH);
172
- return ["```", snippet, "```"].join("\n");
173
- }
174
-
175
- function truncate(value: string, maxLength: number): string {
176
- if (value.length <= maxLength) {
177
- return value;
178
- }
179
- return `${value.slice(0, maxLength)}\n…`;
180
- }
181
-
182
- function formatResearchSection(content: string): string | null {
183
- try {
184
- const parsed = JSON.parse(content);
185
- const sections: string[] = [];
186
-
187
- if (parsed.context) {
188
- sections.push("### Research summary");
189
- sections.push(parsed.context);
190
- }
191
-
192
- if (parsed.questions?.length) {
193
- sections.push("");
194
- sections.push("### Questions needing answers");
195
- for (const question of parsed.questions) {
196
- sections.push(`- ${question.question ?? question}`);
197
- }
198
- }
199
-
200
- if (parsed.answers?.length) {
201
- sections.push("");
202
- sections.push("### Answers provided");
203
- for (const answer of parsed.answers) {
204
- const questionId = answer.questionId
205
- ? ` (Q: ${answer.questionId})`
206
- : "";
207
- sections.push(
208
- `- ${answer.selectedOption || answer.customInput || "answer"}${questionId}`,
209
- );
210
- }
211
- }
212
-
213
- return sections.length ? sections.join("\n") : null;
214
- } catch {
215
- return null;
216
- }
217
- }
218
-
219
- function summarizeTodos(content: string): string | null {
220
- try {
221
- const data = JSON.parse(content);
222
- const total = data?.metadata?.total ?? data?.items?.length;
223
- const completed =
224
- data?.metadata?.completed ??
225
- data?.items?.filter(
226
- (item: { status?: string }) => item.status === "completed",
227
- ).length;
228
-
229
- const lines = [`Progress: ${completed}/${total} completed`];
230
-
231
- if (data?.items?.length) {
232
- for (const item of data.items) {
233
- lines.push(`- [${item.status}] ${item.content}`);
234
- }
235
- }
236
-
237
- return lines.join("\n");
238
- } catch {
239
- return null;
240
- }
241
- }
@@ -1,167 +0,0 @@
1
- import { query } from "@anthropic-ai/claude-agent-sdk";
2
- import { POSTHOG_NOTIFICATIONS } from "../../acp-extensions.js";
3
- import { PLANNING_SYSTEM_PROMPT } from "../../agents/planning.js";
4
- import { TodoManager } from "../../todo-manager.js";
5
- import type { WorkflowStepRunner } from "../types.js";
6
- import { finalizeStepGitActions } from "../utils.js";
7
-
8
- export const planStep: WorkflowStepRunner = async ({ step, context }) => {
9
- const {
10
- task,
11
- cwd,
12
- isCloudMode,
13
- options,
14
- logger,
15
- fileManager,
16
- gitManager,
17
- promptBuilder,
18
- sessionId,
19
- mcpServers,
20
- sendNotification,
21
- } = context;
22
-
23
- const stepLogger = logger.child("PlanStep");
24
-
25
- const existingPlan = await fileManager.readPlan(task.id);
26
- if (existingPlan) {
27
- stepLogger.info("Plan already exists, skipping step", { taskId: task.id });
28
- return { status: "skipped" };
29
- }
30
-
31
- const researchData = await fileManager.readResearch(task.id);
32
- if (researchData?.questions && !researchData.answered) {
33
- stepLogger.info("Waiting for answered research questions", {
34
- taskId: task.id,
35
- });
36
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
37
- sessionId,
38
- phase: "research_questions",
39
- });
40
- return { status: "skipped", halt: true };
41
- }
42
-
43
- stepLogger.info("Starting planning phase", { taskId: task.id });
44
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_START, {
45
- sessionId,
46
- phase: "planning",
47
- });
48
- let researchContext = "";
49
- if (researchData) {
50
- researchContext += `## Research Context\n\n${researchData.context}\n\n`;
51
- if (researchData.keyFiles.length > 0) {
52
- researchContext += `**Key Files:**\n${researchData.keyFiles.map((f) => `- ${f}`).join("\n")}\n\n`;
53
- }
54
- if (researchData.blockers && researchData.blockers.length > 0) {
55
- researchContext += `**Considerations:**\n${researchData.blockers.map((b) => `- ${b}`).join("\n")}\n\n`;
56
- }
57
-
58
- // Add answered questions if they exist
59
- if (
60
- researchData.questions &&
61
- researchData.answers &&
62
- researchData.answered
63
- ) {
64
- researchContext += `## Implementation Decisions\n\n`;
65
- for (const question of researchData.questions) {
66
- const answer = researchData.answers.find(
67
- (a) => a.questionId === question.id,
68
- );
69
-
70
- researchContext += `### ${question.question}\n\n`;
71
- if (answer) {
72
- researchContext += `**Selected:** ${answer.selectedOption}\n`;
73
- if (answer.customInput) {
74
- researchContext += `**Details:** ${answer.customInput}\n`;
75
- }
76
- } else {
77
- researchContext += `**Selected:** Not answered\n`;
78
- }
79
- researchContext += `\n`;
80
- }
81
- }
82
- }
83
-
84
- const planningPrompt = await promptBuilder.buildPlanningPrompt(task, cwd);
85
- const fullPrompt = `${PLANNING_SYSTEM_PROMPT}\n\n${planningPrompt}\n\n${researchContext}`;
86
-
87
- const baseOptions: Record<string, unknown> = {
88
- model: step.model,
89
- cwd,
90
- permissionMode: "plan",
91
- settingSources: ["local"],
92
- mcpServers,
93
- // Allow research tools: read-only operations, web search, MCP resources, and ExitPlanMode
94
- allowedTools: [
95
- "Read",
96
- "Glob",
97
- "Grep",
98
- "WebFetch",
99
- "WebSearch",
100
- "ListMcpResources",
101
- "ReadMcpResource",
102
- "ExitPlanMode",
103
- "TodoWrite",
104
- "BashOutput",
105
- ],
106
- };
107
-
108
- const response = query({
109
- prompt: fullPrompt,
110
- options: { ...baseOptions, ...(options.queryOverrides || {}) },
111
- });
112
-
113
- const todoManager = new TodoManager(fileManager, stepLogger);
114
-
115
- let planContent = "";
116
- try {
117
- for await (const message of response) {
118
- const todoList = await todoManager.checkAndPersistFromMessage(
119
- message,
120
- task.id,
121
- );
122
- if (todoList) {
123
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
124
- sessionId,
125
- kind: "todos",
126
- content: todoList,
127
- });
128
- }
129
-
130
- // Extract text content for plan
131
- if (message.type === "assistant" && message.message?.content) {
132
- for (const block of message.message.content) {
133
- if (block.type === "text" && block.text) {
134
- planContent += `${block.text}\n`;
135
- }
136
- }
137
- }
138
- }
139
- } catch (error) {
140
- stepLogger.error("Error during plan step query", error);
141
- throw error;
142
- }
143
-
144
- if (planContent.trim()) {
145
- await fileManager.writePlan(task.id, planContent.trim());
146
- stepLogger.info("Plan completed", { taskId: task.id });
147
- }
148
-
149
- await gitManager.addAllPostHogFiles();
150
- await finalizeStepGitActions(context, step, {
151
- commitMessage: `Planning phase for ${task.title}`,
152
- });
153
-
154
- if (!isCloudMode) {
155
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
156
- sessionId,
157
- phase: "planning",
158
- });
159
- return { status: "completed", halt: true };
160
- }
161
-
162
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
163
- sessionId,
164
- phase: "planning",
165
- });
166
- return { status: "completed" };
167
- };