@posthog/agent 1.10.0 → 1.12.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.
Files changed (77) hide show
  1. package/README.md +26 -65
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/src/adapters/types.d.ts +1 -1
  5. package/dist/src/agent.d.ts +5 -13
  6. package/dist/src/agent.d.ts.map +1 -1
  7. package/dist/src/agent.js +300 -187
  8. package/dist/src/agent.js.map +1 -1
  9. package/dist/src/agents/execution.d.ts +1 -1
  10. package/dist/src/agents/execution.js +2 -2
  11. package/dist/src/agents/execution.js.map +1 -1
  12. package/dist/src/agents/research.d.ts +2 -0
  13. package/dist/src/agents/research.d.ts.map +1 -0
  14. package/dist/src/agents/research.js +105 -0
  15. package/dist/src/agents/research.js.map +1 -0
  16. package/dist/src/file-manager.d.ts +19 -0
  17. package/dist/src/file-manager.d.ts.map +1 -1
  18. package/dist/src/file-manager.js +39 -0
  19. package/dist/src/file-manager.js.map +1 -1
  20. package/dist/src/git-manager.d.ts +4 -0
  21. package/dist/src/git-manager.d.ts.map +1 -1
  22. package/dist/src/git-manager.js +42 -1
  23. package/dist/src/git-manager.js.map +1 -1
  24. package/dist/src/posthog-api.d.ts +0 -8
  25. package/dist/src/posthog-api.d.ts.map +1 -1
  26. package/dist/src/posthog-api.js +0 -32
  27. package/dist/src/posthog-api.js.map +1 -1
  28. package/dist/src/prompt-builder.d.ts +1 -0
  29. package/dist/src/prompt-builder.d.ts.map +1 -1
  30. package/dist/src/prompt-builder.js +40 -0
  31. package/dist/src/prompt-builder.js.map +1 -1
  32. package/dist/src/structured-extraction.d.ts +22 -0
  33. package/dist/src/structured-extraction.d.ts.map +1 -0
  34. package/dist/src/structured-extraction.js +136 -0
  35. package/dist/src/structured-extraction.js.map +1 -0
  36. package/dist/src/task-progress-reporter.d.ts +0 -6
  37. package/dist/src/task-progress-reporter.d.ts.map +1 -1
  38. package/dist/src/task-progress-reporter.js +2 -26
  39. package/dist/src/task-progress-reporter.js.map +1 -1
  40. package/dist/src/template-manager.d.ts.map +1 -1
  41. package/dist/src/template-manager.js +26 -4
  42. package/dist/src/template-manager.js.map +1 -1
  43. package/dist/src/types.d.ts +7 -4
  44. package/dist/src/types.d.ts.map +1 -1
  45. package/dist/src/types.js +0 -1
  46. package/dist/src/types.js.map +1 -1
  47. package/package.json +5 -5
  48. package/src/adapters/types.ts +1 -1
  49. package/src/agent.ts +326 -195
  50. package/src/agents/execution.ts +2 -2
  51. package/src/agents/research.ts +103 -0
  52. package/src/file-manager.ts +64 -0
  53. package/src/git-manager.ts +53 -1
  54. package/src/posthog-api.ts +0 -40
  55. package/src/prompt-builder.ts +53 -0
  56. package/src/structured-extraction.ts +167 -0
  57. package/src/task-progress-reporter.ts +2 -34
  58. package/src/template-manager.ts +35 -5
  59. package/src/types.ts +8 -7
  60. package/dist/src/agent-registry.d.ts +0 -16
  61. package/dist/src/agent-registry.d.ts.map +0 -1
  62. package/dist/src/agent-registry.js +0 -56
  63. package/dist/src/agent-registry.js.map +0 -1
  64. package/dist/src/stage-executor.d.ts +0 -19
  65. package/dist/src/stage-executor.d.ts.map +0 -1
  66. package/dist/src/stage-executor.js +0 -135
  67. package/dist/src/stage-executor.js.map +0 -1
  68. package/dist/src/workflow-registry.d.ts +0 -11
  69. package/dist/src/workflow-registry.d.ts.map +0 -1
  70. package/dist/src/workflow-registry.js +0 -27
  71. package/dist/src/workflow-registry.js.map +0 -1
  72. package/dist/src/workflow-types.d.ts +0 -45
  73. package/dist/src/workflow-types.d.ts.map +0 -1
  74. package/src/agent-registry.ts +0 -59
  75. package/src/stage-executor.ts +0 -160
  76. package/src/workflow-registry.ts +0 -30
  77. package/src/workflow-types.ts +0 -52
package/dist/src/agent.js CHANGED
@@ -6,11 +6,9 @@ import { GitManager } from './git-manager.js';
6
6
  import { TemplateManager } from './template-manager.js';
7
7
  import { ClaudeAdapter } from './adapters/claude/claude-adapter.js';
8
8
  import { Logger } from './utils/logger.js';
9
- import { AgentRegistry } from './agent-registry.js';
10
- import { WorkflowRegistry } from './workflow-registry.js';
11
- import { StageExecutor } from './stage-executor.js';
12
9
  import { PromptBuilder } from './prompt-builder.js';
13
10
  import { TaskProgressReporter } from './task-progress-reporter.js';
11
+ import { OpenAIExtractor } from './structured-extraction.js';
14
12
  export { PermissionMode } from './types.js';
15
13
 
16
14
  class Agent {
@@ -23,10 +21,9 @@ class Agent {
23
21
  templateManager;
24
22
  adapter;
25
23
  logger;
26
- agentRegistry;
27
- workflowRegistry;
28
- stageExecutor;
29
24
  progressReporter;
25
+ promptBuilder;
26
+ extractor;
30
27
  mcpServers;
31
28
  debug;
32
29
  constructor(config = {}) {
@@ -65,24 +62,23 @@ class Agent {
65
62
  // TODO: Add author config from environment or config
66
63
  });
67
64
  this.templateManager = new TemplateManager();
68
- this.agentRegistry = new AgentRegistry();
69
65
  if (config.posthogApiUrl && config.posthogApiKey) {
70
66
  this.posthogAPI = new PostHogAPIClient({
71
67
  apiUrl: config.posthogApiUrl,
72
68
  apiKey: config.posthogApiKey,
73
69
  });
74
70
  }
75
- this.workflowRegistry = new WorkflowRegistry(this.posthogAPI);
76
- const promptBuilder = new PromptBuilder({
71
+ this.promptBuilder = new PromptBuilder({
77
72
  getTaskFiles: (taskId) => this.getTaskFiles(taskId),
78
73
  generatePlanTemplate: (vars) => this.templateManager.generatePlan(vars),
79
74
  posthogClient: this.posthogAPI,
80
75
  logger: this.logger.child('PromptBuilder')
81
76
  });
82
- this.stageExecutor = new StageExecutor(this.agentRegistry, this.logger, promptBuilder, undefined, // eventHandler set via setEventHandler below
83
- this.mcpServers);
84
- this.stageExecutor.setEventHandler((event) => this.emitEvent(event));
85
77
  this.progressReporter = new TaskProgressReporter(this.posthogAPI, this.logger);
78
+ // Initialize OpenAI extractor if API key is available
79
+ if (process.env.OPENAI_API_KEY) {
80
+ this.extractor = new OpenAIExtractor(this.logger.child('OpenAIExtractor'));
81
+ }
86
82
  }
87
83
  /**
88
84
  * Enable or disable debug logging
@@ -113,206 +109,307 @@ class Agent {
113
109
  throw error;
114
110
  }
115
111
  }
116
- // Workflow-based execution
117
- async runWorkflow(taskOrId, workflowId, options = {}) {
112
+ // Adaptive task execution - 3 phases (research → plan → build)
113
+ async runTask(taskOrId, options = {}) {
118
114
  await this._configureLlmGateway();
119
115
  const task = typeof taskOrId === 'string' ? await this.fetchTask(taskOrId) : taskOrId;
120
- await this.workflowRegistry.loadWorkflows();
121
- const workflow = this.workflowRegistry.getWorkflow(workflowId);
122
- if (!workflow) {
123
- throw new Error(`Workflow ${workflowId} not found`);
116
+ const cwd = options.repositoryPath || this.workingDirectory;
117
+ const isCloudMode = options.isCloudMode ?? false;
118
+ const taskSlug = task.slug || task.id;
119
+ this.logger.info('Starting adaptive task execution', { taskId: task.id, taskSlug, isCloudMode });
120
+ // Initialize progress reporter for task run tracking (needed for PR attachment)
121
+ await this.progressReporter.start(task.id, { totalSteps: 3 }); // 3 phases: research, plan, build
122
+ this.emitEvent(this.adapter.createStatusEvent('run_started', { runId: this.progressReporter.runId }));
123
+ // Phase 1: Branch check
124
+ const existingBranch = await this.gitManager.getTaskBranch(taskSlug);
125
+ if (!existingBranch) {
126
+ this.logger.info('Creating task branch', { taskSlug });
127
+ const branchName = `posthog/task-${taskSlug}`;
128
+ await this.gitManager.createOrSwitchToBranch(branchName);
129
+ this.emitEvent(this.adapter.createStatusEvent('branch_created', { branch: branchName }));
130
+ // Initial commit
131
+ await this.fileManager.ensureGitignore();
132
+ await this.gitManager.addAllPostHogFiles();
133
+ if (isCloudMode) {
134
+ await this.gitManager.commitAndPush(`Initialize task ${taskSlug}`, { allowEmpty: true });
135
+ }
136
+ else {
137
+ await this.gitManager.commitChanges(`Initialize task ${taskSlug}`);
138
+ }
124
139
  }
125
- const orderedStages = [...workflow.stages].sort((a, b) => a.position - b.position);
126
- // Ensure task is assigned to workflow and positioned at first stage
127
- if (this.posthogAPI) {
128
- try {
129
- if (task.workflow !== workflowId) {
130
- await this.posthogAPI.updateTask(task.id, { workflow: workflowId });
131
- task.workflow = workflowId;
140
+ else {
141
+ this.logger.info('Switching to existing task branch', { branch: existingBranch });
142
+ await this.gitManager.switchToBranch(existingBranch);
143
+ }
144
+ // Phase 2: Research
145
+ const researchExists = await this.fileManager.readResearch(task.id);
146
+ if (!researchExists) {
147
+ this.logger.info('Starting research phase', { taskId: task.id });
148
+ this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'research' }));
149
+ // Run research agent
150
+ const researchPrompt = await this.promptBuilder.buildResearchPrompt(task, cwd);
151
+ const { RESEARCH_SYSTEM_PROMPT } = await import('./agents/research.js');
152
+ const fullPrompt = RESEARCH_SYSTEM_PROMPT + '\n\n' + researchPrompt;
153
+ const baseOptions = {
154
+ model: 'claude-sonnet-4-5-20250929',
155
+ cwd,
156
+ permissionMode: 'plan',
157
+ settingSources: ['local'],
158
+ mcpServers: this.mcpServers,
159
+ };
160
+ const response = query({
161
+ prompt: fullPrompt,
162
+ options: { ...baseOptions, ...(options.queryOverrides || {}) },
163
+ });
164
+ let researchContent = '';
165
+ for await (const message of response) {
166
+ this.emitEvent(this.adapter.createRawSDKEvent(message));
167
+ const transformed = this.adapter.transform(message);
168
+ if (transformed) {
169
+ this.emitEvent(transformed);
170
+ }
171
+ if (message.type === 'assistant' && message.message?.content) {
172
+ for (const c of message.message.content) {
173
+ if (c.type === 'text' && c.text)
174
+ researchContent += c.text + '\n';
175
+ }
132
176
  }
133
177
  }
134
- catch (e) {
135
- this.logger.warn('Failed to sync task workflow before execution', { error: e.message });
178
+ // Write research.md
179
+ if (researchContent.trim()) {
180
+ await this.fileManager.writeResearch(task.id, researchContent.trim());
181
+ this.logger.info('Research completed', { taskId: task.id });
136
182
  }
137
- }
138
- const executionId = this.taskManager.generateExecutionId();
139
- this.logger.info('Starting workflow execution', { taskId: task.id, workflowId, executionId });
140
- this.taskManager.startExecution(task.id, 'plan_and_build', executionId);
141
- await this.progressReporter.start(task.id, {
142
- totalSteps: orderedStages.length,
143
- });
144
- // Set initial stage on the newly created run
145
- const firstStage = orderedStages[0];
146
- if (this.posthogAPI && this.progressReporter.runId && firstStage) {
147
- try {
148
- await this.posthogAPI.updateTaskRun(task.id, this.progressReporter.runId, {
149
- current_stage: firstStage.id
183
+ // Commit research
184
+ await this.gitManager.addAllPostHogFiles();
185
+ // Extract questions using structured output and save to questions.json
186
+ if (this.extractor) {
187
+ try {
188
+ this.logger.info('Extracting questions from research.md', { taskId: task.id });
189
+ const questions = await this.extractQuestionsFromResearch(task.id, false);
190
+ this.logger.info('Questions extracted successfully', { taskId: task.id, count: questions.length });
191
+ // Save questions.json
192
+ await this.fileManager.writeQuestions(task.id, {
193
+ questions,
194
+ answered: false,
195
+ answers: null,
196
+ });
197
+ this.logger.info('Questions saved to questions.json', { taskId: task.id });
198
+ // Emit event for Array to pick up (local mode)
199
+ if (!isCloudMode) {
200
+ this.emitEvent({
201
+ type: 'artifact',
202
+ ts: Date.now(),
203
+ kind: 'research_questions',
204
+ content: questions,
205
+ });
206
+ this.logger.info('Emitted research_questions artifact event', { taskId: task.id });
207
+ }
208
+ }
209
+ catch (error) {
210
+ this.logger.error('Failed to extract questions', { error: error instanceof Error ? error.message : String(error) });
211
+ this.emitEvent({
212
+ type: 'error',
213
+ ts: Date.now(),
214
+ message: `Failed to extract questions: ${error instanceof Error ? error.message : String(error)}`,
215
+ });
216
+ }
217
+ }
218
+ else {
219
+ this.logger.warn('OpenAI extractor not available (OPENAI_API_KEY not set), skipping question extraction');
220
+ this.emitEvent({
221
+ type: 'status',
222
+ ts: Date.now(),
223
+ phase: 'extraction_skipped',
224
+ message: 'Question extraction skipped - OPENAI_API_KEY not configured',
150
225
  });
151
226
  }
152
- catch (e) {
153
- this.logger.warn('Failed to set initial stage on run', { error: e.message });
227
+ if (isCloudMode) {
228
+ await this.gitManager.commitAndPush(`Research phase for ${task.title}`);
229
+ }
230
+ else {
231
+ await this.gitManager.commitChanges(`Research phase for ${task.title}`);
232
+ this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'research' }));
233
+ return; // Local mode: return to user
154
234
  }
155
235
  }
156
- try {
157
- let startIndex = 0;
158
- const currentStageId = task.current_stage;
159
- // If task is already at the last stage, fail gracefully without progressing
160
- if (currentStageId) {
161
- const currIdx = orderedStages.findIndex(s => s.id === currentStageId);
162
- const atLastStage = currIdx >= 0 && currIdx === orderedStages.length - 1;
163
- if (atLastStage) {
164
- const finalStageKey = orderedStages[currIdx]?.key;
165
- this.emitEvent(this.adapter.createStatusEvent('no_next_stage', { stage: finalStageKey }));
166
- await this.progressReporter.noNextStage(finalStageKey);
167
- await this.progressReporter.complete();
168
- this.taskManager.completeExecution(executionId, { task, workflow });
169
- return { task, workflow };
236
+ // Phase 3: Auto-answer questions (cloud mode only)
237
+ if (isCloudMode) {
238
+ const questionsData = await this.fileManager.readQuestions(task.id);
239
+ if (questionsData && !questionsData.answered) {
240
+ this.logger.info('Auto-answering research questions', { taskId: task.id });
241
+ // Extract questions with recommended answers using structured output
242
+ if (this.extractor) {
243
+ const questionsWithAnswers = await this.extractQuestionsFromResearch(task.id, true);
244
+ // Save answers to questions.json
245
+ await this.fileManager.writeQuestions(task.id, {
246
+ questions: questionsWithAnswers.map(qa => ({
247
+ id: qa.id,
248
+ question: qa.question,
249
+ options: qa.options,
250
+ })),
251
+ answered: true,
252
+ answers: questionsWithAnswers.map(qa => ({
253
+ questionId: qa.id,
254
+ selectedOption: qa.recommendedAnswer,
255
+ customInput: qa.justification,
256
+ })),
257
+ });
258
+ this.logger.info('Auto-answers saved to questions.json', { taskId: task.id });
259
+ await this.gitManager.addAllPostHogFiles();
260
+ await this.gitManager.commitAndPush(`Answer research questions for ${task.title}`);
261
+ }
262
+ else {
263
+ this.logger.warn('OpenAI extractor not available, skipping auto-answer');
170
264
  }
171
265
  }
172
- if (options.resumeFromCurrentStage && currentStageId) {
173
- const idx = orderedStages.findIndex(s => s.id === currentStageId);
174
- if (idx >= 0)
175
- startIndex = idx;
266
+ }
267
+ // Phase 4: Plan
268
+ const planExists = await this.readPlan(task.id);
269
+ if (!planExists) {
270
+ // Check if questions have been answered
271
+ const questionsData = await this.fileManager.readQuestions(task.id);
272
+ if (!questionsData || !questionsData.answered) {
273
+ this.logger.info('Waiting for user answers to research questions');
274
+ this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'research_questions' }));
275
+ return; // Wait for user to answer questions
176
276
  }
177
- // Align server-side stage when restarting from a different stage
178
- if (this.posthogAPI && this.progressReporter.runId) {
179
- const targetStage = orderedStages[startIndex];
180
- if (targetStage && targetStage.id !== currentStageId) {
181
- try {
182
- await this.posthogAPI.updateTaskRun(task.id, this.progressReporter.runId, {
183
- current_stage: targetStage.id
184
- });
185
- }
186
- catch (e) {
187
- this.logger.warn('Failed to update run stage', { error: e.message });
277
+ this.logger.info('Starting planning phase', { taskId: task.id });
278
+ this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'planning' }));
279
+ // Build context with research + questions + answers
280
+ const research = await this.fileManager.readResearch(task.id);
281
+ let researchContext = '';
282
+ if (research) {
283
+ researchContext += `## Research Analysis\n\n${research}\n\n`;
284
+ }
285
+ // Add questions and answers
286
+ researchContext += `## Implementation Decisions\n\n`;
287
+ const answers = questionsData.answers || [];
288
+ for (const question of questionsData.questions) {
289
+ const answer = answers.find((a) => a.questionId === question.id);
290
+ researchContext += `### ${question.question}\n\n`;
291
+ if (answer) {
292
+ researchContext += `**Selected:** ${answer.selectedOption}\n`;
293
+ if (answer.customInput) {
294
+ researchContext += `**Details:** ${answer.customInput}\n`;
188
295
  }
189
296
  }
297
+ else {
298
+ this.logger.warn('No answer found for question', { questionId: question.id });
299
+ researchContext += `**Selected:** Not answered\n`;
300
+ }
301
+ researchContext += '\n';
190
302
  }
191
- for (let i = startIndex; i < orderedStages.length; i++) {
192
- const stage = orderedStages[i];
193
- await this.progressReporter.stageStarted(stage.key, i);
194
- await this.executeStage(task, stage, options);
195
- await this.progressReporter.stageCompleted(stage.key, i + 1);
196
- if (options.autoProgress) {
197
- const hasNext = i < orderedStages.length - 1;
198
- if (hasNext) {
199
- await this.progressToNextStage(task.id, stage.key);
303
+ // Run planning agent with full context
304
+ const planningPrompt = await this.promptBuilder.buildPlanningPrompt(task, cwd);
305
+ const { PLANNING_SYSTEM_PROMPT } = await import('./agents/planning.js');
306
+ const fullPrompt = PLANNING_SYSTEM_PROMPT + '\n\n' + planningPrompt + '\n\n' + researchContext;
307
+ const baseOptions = {
308
+ model: 'claude-sonnet-4-5-20250929',
309
+ cwd,
310
+ permissionMode: 'plan',
311
+ settingSources: ['local'],
312
+ mcpServers: this.mcpServers,
313
+ };
314
+ const response = query({
315
+ prompt: fullPrompt,
316
+ options: { ...baseOptions, ...(options.queryOverrides || {}) },
317
+ });
318
+ let planContent = '';
319
+ for await (const message of response) {
320
+ this.emitEvent(this.adapter.createRawSDKEvent(message));
321
+ const transformed = this.adapter.transform(message);
322
+ if (transformed) {
323
+ this.emitEvent(transformed);
324
+ }
325
+ if (message.type === 'assistant' && message.message?.content) {
326
+ for (const c of message.message.content) {
327
+ if (c.type === 'text' && c.text)
328
+ planContent += c.text + '\n';
200
329
  }
201
330
  }
202
331
  }
203
- await this.progressReporter.complete();
204
- this.taskManager.completeExecution(executionId, { task, workflow });
205
- return { task, workflow };
206
- }
207
- catch (error) {
208
- await this.progressReporter.fail(error);
209
- this.taskManager.failExecution(executionId, error);
210
- throw error;
211
- }
212
- }
213
- async executeStage(task, stage, options = {}) {
214
- this.emitEvent(this.adapter.createStatusEvent('stage_start', { stage: stage.key }));
215
- const overrides = options.stageOverrides?.[stage.key];
216
- const agentName = stage.agent_name || 'code_generation';
217
- const agentDef = this.agentRegistry.getAgent(agentName);
218
- const isManual = stage.is_manual_only === true;
219
- const stageKeyLower = (stage.key || '').toLowerCase().trim();
220
- const isPlanningByKey = stageKeyLower === 'plan' || stageKeyLower.includes('plan');
221
- const isPlanning = !isManual && ((agentDef?.agent_type === 'planning') || isPlanningByKey);
222
- const shouldCreatePlanningBranch = overrides?.createPlanningBranch !== false; // default true
223
- const shouldCreateImplBranch = overrides?.createImplementationBranch !== false; // default true
224
- if (isPlanning && shouldCreatePlanningBranch) {
225
- const planningBranch = await this.createPlanningBranch(task.id);
226
- await this.updateTaskBranch(task.id, planningBranch);
227
- this.emitEvent(this.adapter.createStatusEvent('branch_created', { stage: stage.key, branch: planningBranch }));
228
- await this.progressReporter.branchCreated(stage.key, planningBranch);
229
- }
230
- else if (!isPlanning && !isManual && shouldCreateImplBranch) {
231
- const implBranch = await this.createImplementationBranch(task.id);
232
- await this.updateTaskBranch(task.id, implBranch);
233
- this.emitEvent(this.adapter.createStatusEvent('branch_created', { stage: stage.key, branch: implBranch }));
234
- await this.progressReporter.branchCreated(stage.key, implBranch);
235
- }
236
- const result = await this.stageExecutor.execute(task, stage, options);
237
- if (result.plan) {
238
- await this.writePlan(task.id, result.plan);
239
- await this.commitPlan(task.id, task.title);
240
- this.emitEvent(this.adapter.createStatusEvent('commit_made', { stage: stage.key, kind: 'plan' }));
241
- await this.progressReporter.commitMade(stage.key, 'plan');
332
+ // Write plan.md
333
+ if (planContent.trim()) {
334
+ await this.writePlan(task.id, planContent.trim());
335
+ this.logger.info('Plan completed', { taskId: task.id });
336
+ }
337
+ // Commit plan
338
+ await this.gitManager.addAllPostHogFiles();
339
+ if (isCloudMode) {
340
+ await this.gitManager.commitAndPush(`Planning phase for ${task.title}`);
341
+ }
342
+ else {
343
+ await this.gitManager.commitChanges(`Planning phase for ${task.title}`);
344
+ this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'planning' }));
345
+ return; // Local mode: return to user
346
+ }
242
347
  }
243
- if (isManual) {
244
- const defaultOpenPR = true; // manual stages default to PR for review
245
- const openPR = overrides?.openPullRequest ?? defaultOpenPR;
246
- if (openPR) {
247
- // Ensure we're on an implementation branch for PRs
248
- let branchName = await this.gitManager.getCurrentBranch();
249
- const onTaskBranch = branchName.includes(`posthog/task-${task.id}`);
250
- if (!onTaskBranch && (overrides?.createImplementationBranch !== false)) {
251
- const implBranch = await this.createImplementationBranch(task.id);
252
- await this.updateTaskBranch(task.id, implBranch);
253
- branchName = implBranch;
254
- this.emitEvent(this.adapter.createStatusEvent('branch_created', { stage: stage.key, branch: implBranch }));
255
- await this.progressReporter.branchCreated(stage.key, implBranch);
256
- }
257
- try {
258
- const prUrl = await this.createPullRequest(task.id, branchName, task.title, task.description);
259
- await this.updateTaskBranch(task.id, branchName);
260
- await this.attachPullRequestToTask(task.id, prUrl, branchName);
261
- this.emitEvent(this.adapter.createStatusEvent('pr_created', { stage: stage.key, prUrl }));
262
- await this.progressReporter.pullRequestCreated(stage.key, prUrl);
348
+ // Phase 5: Build
349
+ const latestRun = task.latest_run;
350
+ const prExists = latestRun?.output && latestRun.output.pr_url;
351
+ if (!prExists) {
352
+ this.logger.info('Starting build phase', { taskId: task.id });
353
+ this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'build' }));
354
+ // Run execution agent
355
+ const executionPrompt = await this.promptBuilder.buildExecutionPrompt(task, cwd);
356
+ const { EXECUTION_SYSTEM_PROMPT } = await import('./agents/execution.js');
357
+ const fullPrompt = EXECUTION_SYSTEM_PROMPT + '\n\n' + executionPrompt;
358
+ const { PermissionMode } = await import('./types.js');
359
+ const permissionMode = options.permissionMode || PermissionMode.ACCEPT_EDITS;
360
+ const baseOptions = {
361
+ model: 'claude-sonnet-4-5-20250929',
362
+ cwd,
363
+ permissionMode,
364
+ settingSources: ['local'],
365
+ mcpServers: this.mcpServers,
366
+ };
367
+ const response = query({
368
+ prompt: fullPrompt,
369
+ options: { ...baseOptions, ...(options.queryOverrides || {}) },
370
+ });
371
+ for await (const message of response) {
372
+ this.emitEvent(this.adapter.createRawSDKEvent(message));
373
+ const transformed = this.adapter.transform(message);
374
+ if (transformed) {
375
+ this.emitEvent(transformed);
263
376
  }
264
- catch { }
265
377
  }
266
- // Do not auto-progress on manual stages
267
- this.emitEvent(this.adapter.createStatusEvent('stage_complete', { stage: stage.key }));
268
- return;
269
- }
270
- if (result.results) {
271
- const existingPlan = await this.readPlan(task.id);
272
- const planSummary = existingPlan ? existingPlan.split('\n')[0] : undefined;
273
- await this.commitImplementation(task.id, task.title, planSummary);
274
- this.emitEvent(this.adapter.createStatusEvent('commit_made', { stage: stage.key, kind: 'implementation' }));
275
- await this.progressReporter.commitMade(stage.key, 'implementation');
276
- }
277
- // PR creation on complete stage (or if explicitly requested), regardless of whether edits occurred
278
- {
279
- const defaultOpenPR = stage.key.toLowerCase().includes('complete');
280
- const openPR = overrides?.openPullRequest ?? defaultOpenPR;
281
- if (openPR) {
378
+ // Commit and push implementation
379
+ // Stage ALL changes (not just .posthog/)
380
+ const hasChanges = await this.gitManager.hasChanges();
381
+ if (hasChanges) {
382
+ await this.gitManager.addFiles(['.']); // Stage all changes
383
+ await this.gitManager.commitChanges(`Implementation for ${task.title}`);
384
+ // Push to origin
282
385
  const branchName = await this.gitManager.getCurrentBranch();
283
- try {
284
- const prUrl = await this.createPullRequest(task.id, branchName, task.title, task.description);
285
- await this.updateTaskBranch(task.id, branchName);
286
- await this.attachPullRequestToTask(task.id, prUrl, branchName);
287
- this.emitEvent(this.adapter.createStatusEvent('pr_created', { stage: stage.key, prUrl }));
288
- await this.progressReporter.pullRequestCreated(stage.key, prUrl);
289
- }
290
- catch { }
386
+ await this.gitManager.pushBranch(branchName);
387
+ this.logger.info('Implementation committed and pushed', { taskId: task.id });
291
388
  }
292
- }
293
- this.emitEvent(this.adapter.createStatusEvent('stage_complete', { stage: stage.key }));
294
- }
295
- async progressToNextStage(taskId, currentStageKey) {
296
- if (!this.posthogAPI || !this.progressReporter.runId) {
297
- throw new Error('PostHog API not configured or no active run. Cannot progress stage.');
298
- }
299
- try {
300
- await this.posthogAPI.progressTaskRun(taskId, this.progressReporter.runId);
301
- }
302
- catch (error) {
303
- if (error instanceof Error && error.message.includes('No next stage available')) {
304
- this.logger.warn('No next stage available when attempting to progress run', {
305
- taskId,
306
- runId: this.progressReporter.runId,
307
- stage: currentStageKey,
308
- error: error.message,
309
- });
310
- this.emitEvent(this.adapter.createStatusEvent('no_next_stage', { stage: currentStageKey }));
311
- await this.progressReporter.noNextStage(currentStageKey);
312
- return;
389
+ else {
390
+ this.logger.warn('No changes to commit in build phase', { taskId: task.id });
313
391
  }
314
- throw error;
392
+ // Create PR
393
+ const branchName = await this.gitManager.getCurrentBranch();
394
+ const prUrl = await this.createPullRequest(task.id, branchName, task.title, task.description);
395
+ this.logger.info('Pull request created', { taskId: task.id, prUrl });
396
+ this.emitEvent(this.adapter.createStatusEvent('pr_created', { prUrl }));
397
+ // Attach PR to task run
398
+ try {
399
+ await this.attachPullRequestToTask(task.id, prUrl, branchName);
400
+ this.logger.info('PR attached to task successfully', { taskId: task.id, prUrl });
401
+ }
402
+ catch (error) {
403
+ this.logger.warn('Could not attach PR to task', { error: error instanceof Error ? error.message : String(error) });
404
+ }
405
+ }
406
+ else {
407
+ this.logger.info('PR already exists, skipping build phase', { taskId: task.id });
315
408
  }
409
+ // Phase 6: Complete
410
+ await this.progressReporter.complete();
411
+ this.logger.info('Task execution complete', { taskId: task.id });
412
+ this.emitEvent(this.adapter.createStatusEvent('task_complete', { taskId: task.id }));
316
413
  }
317
414
  // Direct prompt execution - still supported for low-level usage
318
415
  async run(prompt, options = {}) {
@@ -384,7 +481,23 @@ class Agent {
384
481
  this.logger.debug('Reading plan', { taskId });
385
482
  return await this.fileManager.readPlan(taskId);
386
483
  }
387
- // Git operations for task workflow
484
+ async extractQuestionsFromResearch(taskId, includeAnswers = false) {
485
+ this.logger.info('Extracting questions from research.md', { taskId, includeAnswers });
486
+ if (!this.extractor) {
487
+ throw new Error('OpenAI extractor not initialized. Set OPENAI_API_KEY environment variable.');
488
+ }
489
+ const researchContent = await this.fileManager.readResearch(taskId);
490
+ if (!researchContent) {
491
+ throw new Error('research.md not found for task ' + taskId);
492
+ }
493
+ if (includeAnswers) {
494
+ return await this.extractor.extractQuestionsWithAnswers(researchContent);
495
+ }
496
+ else {
497
+ return await this.extractor.extractQuestions(researchContent);
498
+ }
499
+ }
500
+ // Git operations for task execution
388
501
  async createPlanningBranch(taskId) {
389
502
  this.logger.info('Creating planning branch', { taskId });
390
503
  const branchName = await this.gitManager.createTaskPlanningBranch(taskId);