@posthog/agent 1.10.0 → 1.11.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 (49) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/src/agent-registry.d.ts.map +1 -1
  4. package/dist/src/agent-registry.js +6 -0
  5. package/dist/src/agent-registry.js.map +1 -1
  6. package/dist/src/agent.d.ts +5 -0
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +327 -2
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/agents/research.d.ts +2 -0
  11. package/dist/src/agents/research.d.ts.map +1 -0
  12. package/dist/src/agents/research.js +105 -0
  13. package/dist/src/agents/research.js.map +1 -0
  14. package/dist/src/file-manager.d.ts +19 -0
  15. package/dist/src/file-manager.d.ts.map +1 -1
  16. package/dist/src/file-manager.js +39 -0
  17. package/dist/src/file-manager.js.map +1 -1
  18. package/dist/src/git-manager.d.ts +4 -0
  19. package/dist/src/git-manager.d.ts.map +1 -1
  20. package/dist/src/git-manager.js +41 -0
  21. package/dist/src/git-manager.js.map +1 -1
  22. package/dist/src/prompt-builder.d.ts +1 -0
  23. package/dist/src/prompt-builder.d.ts.map +1 -1
  24. package/dist/src/prompt-builder.js +40 -0
  25. package/dist/src/prompt-builder.js.map +1 -1
  26. package/dist/src/stage-executor.d.ts +1 -0
  27. package/dist/src/stage-executor.d.ts.map +1 -1
  28. package/dist/src/stage-executor.js +43 -0
  29. package/dist/src/stage-executor.js.map +1 -1
  30. package/dist/src/structured-extraction.d.ts +22 -0
  31. package/dist/src/structured-extraction.d.ts.map +1 -0
  32. package/dist/src/structured-extraction.js +136 -0
  33. package/dist/src/structured-extraction.js.map +1 -0
  34. package/dist/src/types.d.ts +7 -0
  35. package/dist/src/types.d.ts.map +1 -1
  36. package/dist/src/types.js.map +1 -1
  37. package/dist/src/workflow-types.d.ts +1 -1
  38. package/dist/src/workflow-types.d.ts.map +1 -1
  39. package/package.json +4 -3
  40. package/src/agent-registry.ts +6 -0
  41. package/src/agent.ts +364 -2
  42. package/src/agents/research.ts +103 -0
  43. package/src/file-manager.ts +64 -0
  44. package/src/git-manager.ts +52 -0
  45. package/src/prompt-builder.ts +53 -0
  46. package/src/stage-executor.ts +50 -0
  47. package/src/structured-extraction.ts +167 -0
  48. package/src/types.ts +8 -0
  49. package/src/workflow-types.ts +1 -1
package/src/agent.ts CHANGED
@@ -16,6 +16,7 @@ import { WorkflowRegistry } from './workflow-registry.js';
16
16
  import { StageExecutor } from './stage-executor.js';
17
17
  import { PromptBuilder } from './prompt-builder.js';
18
18
  import { TaskProgressReporter } from './task-progress-reporter.js';
19
+ import { OpenAIExtractor, type ExtractedQuestion, type ExtractedQuestionWithAnswer } from './structured-extraction.js';
19
20
 
20
21
  export class Agent {
21
22
  private workingDirectory: string;
@@ -31,6 +32,8 @@ export class Agent {
31
32
  private workflowRegistry: WorkflowRegistry;
32
33
  private stageExecutor: StageExecutor;
33
34
  private progressReporter: TaskProgressReporter;
35
+ private promptBuilder: PromptBuilder;
36
+ private extractor?: OpenAIExtractor;
34
37
  private mcpServers?: Record<string, any>;
35
38
  public debug: boolean;
36
39
 
@@ -88,7 +91,7 @@ export class Agent {
88
91
  }
89
92
 
90
93
  this.workflowRegistry = new WorkflowRegistry(this.posthogAPI);
91
- const promptBuilder = new PromptBuilder({
94
+ this.promptBuilder = new PromptBuilder({
92
95
  getTaskFiles: (taskId: string) => this.getTaskFiles(taskId),
93
96
  generatePlanTemplate: (vars) => this.templateManager.generatePlan(vars),
94
97
  posthogClient: this.posthogAPI,
@@ -97,12 +100,17 @@ export class Agent {
97
100
  this.stageExecutor = new StageExecutor(
98
101
  this.agentRegistry,
99
102
  this.logger,
100
- promptBuilder,
103
+ this.promptBuilder,
101
104
  undefined, // eventHandler set via setEventHandler below
102
105
  this.mcpServers
103
106
  );
104
107
  this.stageExecutor.setEventHandler((event) => this.emitEvent(event));
105
108
  this.progressReporter = new TaskProgressReporter(this.posthogAPI, this.logger);
109
+
110
+ // Initialize OpenAI extractor if API key is available
111
+ if (process.env.OPENAI_API_KEY) {
112
+ this.extractor = new OpenAIExtractor(this.logger.child('OpenAIExtractor'));
113
+ }
106
114
  }
107
115
 
108
116
  /**
@@ -328,6 +336,341 @@ export class Agent {
328
336
  this.emitEvent(this.adapter.createStatusEvent('stage_complete', { stage: stage.key }));
329
337
  }
330
338
 
339
+ // Adaptive task execution - 3-phase workflow (research → plan → build)
340
+ async runTask(taskOrId: Task | string, options: import('./types.js').TaskExecutionOptions = {}): Promise<void> {
341
+ await this._configureLlmGateway();
342
+
343
+ const task = typeof taskOrId === 'string' ? await this.fetchTask(taskOrId) : taskOrId;
344
+ const cwd = options.repositoryPath || this.workingDirectory;
345
+ const isCloudMode = options.isCloudMode ?? false;
346
+ const taskSlug = (task as any).slug || task.id;
347
+
348
+ this.logger.info('Starting adaptive task execution', { taskId: task.id, taskSlug, isCloudMode });
349
+
350
+ // Initialize progress reporter for task run tracking (needed for PR attachment)
351
+ await this.progressReporter.start(task.id, { totalSteps: 3 }); // 3 phases: research, plan, build
352
+ this.emitEvent(this.adapter.createStatusEvent('run_started', { runId: this.progressReporter.runId }));
353
+
354
+ // Phase 1: Branch check
355
+ const existingBranch = await this.gitManager.getTaskBranch(taskSlug);
356
+ if (!existingBranch) {
357
+ this.logger.info('Creating task branch', { taskSlug });
358
+ const branchName = `posthog/task-${taskSlug}`;
359
+ await this.gitManager.createOrSwitchToBranch(branchName);
360
+ this.emitEvent(this.adapter.createStatusEvent('branch_created', { branch: branchName }));
361
+
362
+ // Initial commit
363
+ await this.fileManager.ensureGitignore();
364
+ await this.gitManager.addAllPostHogFiles();
365
+ if (isCloudMode) {
366
+ await this.gitManager.commitAndPush(`Initialize task ${taskSlug}`, { allowEmpty: true });
367
+ } else {
368
+ await this.gitManager.commitChanges(`Initialize task ${taskSlug}`);
369
+ }
370
+ } else {
371
+ this.logger.info('Switching to existing task branch', { branch: existingBranch });
372
+ await this.gitManager.switchToBranch(existingBranch);
373
+ }
374
+
375
+ // Phase 2: Research
376
+ const researchExists = await this.fileManager.readResearch(task.id);
377
+ if (!researchExists) {
378
+ this.logger.info('Starting research phase', { taskId: task.id });
379
+ this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'research' }));
380
+
381
+ // Run research agent
382
+ const researchPrompt = await this.promptBuilder.buildResearchPrompt(task, cwd);
383
+ const { RESEARCH_SYSTEM_PROMPT } = await import('./agents/research.js');
384
+ const fullPrompt = RESEARCH_SYSTEM_PROMPT + '\n\n' + researchPrompt;
385
+
386
+ const baseOptions: Record<string, any> = {
387
+ model: 'claude-sonnet-4-5-20250929',
388
+ cwd,
389
+ permissionMode: 'plan',
390
+ settingSources: ['local'],
391
+ mcpServers: this.mcpServers,
392
+ };
393
+
394
+ const response = query({
395
+ prompt: fullPrompt,
396
+ options: { ...baseOptions, ...(options.queryOverrides || {}) },
397
+ });
398
+
399
+ let researchContent = '';
400
+ for await (const message of response) {
401
+ this.emitEvent(this.adapter.createRawSDKEvent(message));
402
+ const transformed = this.adapter.transform(message);
403
+ if (transformed) {
404
+ this.emitEvent(transformed);
405
+ }
406
+ if (message.type === 'assistant' && message.message?.content) {
407
+ for (const c of message.message.content) {
408
+ if (c.type === 'text' && c.text) researchContent += c.text + '\n';
409
+ }
410
+ }
411
+ }
412
+
413
+ // Write research.md
414
+ if (researchContent.trim()) {
415
+ await this.fileManager.writeResearch(task.id, researchContent.trim());
416
+ this.logger.info('Research completed', { taskId: task.id });
417
+ }
418
+
419
+ // Commit research
420
+ await this.gitManager.addAllPostHogFiles();
421
+
422
+ // Extract questions using structured output and save to questions.json
423
+ if (this.extractor) {
424
+ try {
425
+ this.logger.info('Extracting questions from research.md', { taskId: task.id });
426
+ const questions = await this.extractQuestionsFromResearch(task.id, false);
427
+
428
+ this.logger.info('Questions extracted successfully', { taskId: task.id, count: questions.length });
429
+
430
+ // Save questions.json
431
+ await this.fileManager.writeQuestions(task.id, {
432
+ questions,
433
+ answered: false,
434
+ answers: null,
435
+ });
436
+
437
+ this.logger.info('Questions saved to questions.json', { taskId: task.id });
438
+
439
+ // Emit event for Array to pick up (local mode)
440
+ if (!isCloudMode) {
441
+ this.emitEvent({
442
+ type: 'artifact',
443
+ ts: Date.now(),
444
+ kind: 'research_questions',
445
+ content: questions,
446
+ });
447
+ this.logger.info('Emitted research_questions artifact event', { taskId: task.id });
448
+ }
449
+ } catch (error) {
450
+ this.logger.error('Failed to extract questions', { error: error instanceof Error ? error.message : String(error) });
451
+ this.emitEvent({
452
+ type: 'error',
453
+ ts: Date.now(),
454
+ message: `Failed to extract questions: ${error instanceof Error ? error.message : String(error)}`,
455
+ });
456
+ }
457
+ } else {
458
+ this.logger.warn('OpenAI extractor not available (OPENAI_API_KEY not set), skipping question extraction');
459
+ this.emitEvent({
460
+ type: 'status',
461
+ ts: Date.now(),
462
+ phase: 'extraction_skipped',
463
+ message: 'Question extraction skipped - OPENAI_API_KEY not configured',
464
+ });
465
+ }
466
+
467
+ if (isCloudMode) {
468
+ await this.gitManager.commitAndPush(`Research phase for ${task.title}`);
469
+ } else {
470
+ await this.gitManager.commitChanges(`Research phase for ${task.title}`);
471
+ this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'research' }));
472
+ return; // Local mode: return to user
473
+ }
474
+ }
475
+
476
+ // Phase 3: Auto-answer questions (cloud mode only)
477
+ if (isCloudMode) {
478
+ const questionsData = await this.fileManager.readQuestions(task.id);
479
+ if (questionsData && !questionsData.answered) {
480
+ this.logger.info('Auto-answering research questions', { taskId: task.id });
481
+
482
+ // Extract questions with recommended answers using structured output
483
+ if (this.extractor) {
484
+ const questionsWithAnswers = await this.extractQuestionsFromResearch(task.id, true) as ExtractedQuestionWithAnswer[];
485
+
486
+ // Save answers to questions.json
487
+ await this.fileManager.writeQuestions(task.id, {
488
+ questions: questionsWithAnswers.map(qa => ({
489
+ id: qa.id,
490
+ question: qa.question,
491
+ options: qa.options,
492
+ })),
493
+ answered: true,
494
+ answers: questionsWithAnswers.map(qa => ({
495
+ questionId: qa.id,
496
+ selectedOption: qa.recommendedAnswer,
497
+ customInput: qa.justification,
498
+ })),
499
+ });
500
+
501
+ this.logger.info('Auto-answers saved to questions.json', { taskId: task.id });
502
+ await this.gitManager.addAllPostHogFiles();
503
+ await this.gitManager.commitAndPush(`Answer research questions for ${task.title}`);
504
+ } else {
505
+ this.logger.warn('OpenAI extractor not available, skipping auto-answer');
506
+ }
507
+ }
508
+ }
509
+
510
+ // Phase 4: Plan
511
+ const planExists = await this.readPlan(task.id);
512
+ if (!planExists) {
513
+ // Check if questions have been answered
514
+ const questionsData = await this.fileManager.readQuestions(task.id);
515
+ if (!questionsData || !questionsData.answered) {
516
+ this.logger.info('Waiting for user answers to research questions');
517
+ this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'research_questions' }));
518
+ return; // Wait for user to answer questions
519
+ }
520
+
521
+ this.logger.info('Starting planning phase', { taskId: task.id });
522
+ this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'planning' }));
523
+
524
+ // Build context with research + questions + answers
525
+ const research = await this.fileManager.readResearch(task.id);
526
+ let researchContext = '';
527
+ if (research) {
528
+ researchContext += `## Research Analysis\n\n${research}\n\n`;
529
+ }
530
+
531
+ // Add questions and answers
532
+ researchContext += `## Implementation Decisions\n\n`;
533
+ const answers = questionsData.answers || [];
534
+ for (const question of questionsData.questions) {
535
+ const answer = answers.find((a: any) => a.questionId === question.id);
536
+
537
+ researchContext += `### ${question.question}\n\n`;
538
+ if (answer) {
539
+ researchContext += `**Selected:** ${answer.selectedOption}\n`;
540
+ if (answer.customInput) {
541
+ researchContext += `**Details:** ${answer.customInput}\n`;
542
+ }
543
+ } else {
544
+ this.logger.warn('No answer found for question', { questionId: question.id });
545
+ researchContext += `**Selected:** Not answered\n`;
546
+ }
547
+ researchContext += '\n';
548
+ }
549
+
550
+ // Run planning agent with full context
551
+ const planningPrompt = await this.promptBuilder.buildPlanningPrompt(task, cwd);
552
+ const { PLANNING_SYSTEM_PROMPT } = await import('./agents/planning.js');
553
+ const fullPrompt = PLANNING_SYSTEM_PROMPT + '\n\n' + planningPrompt + '\n\n' + researchContext;
554
+
555
+ const baseOptions: Record<string, any> = {
556
+ model: 'claude-sonnet-4-5-20250929',
557
+ cwd,
558
+ permissionMode: 'plan',
559
+ settingSources: ['local'],
560
+ mcpServers: this.mcpServers,
561
+ };
562
+
563
+ const response = query({
564
+ prompt: fullPrompt,
565
+ options: { ...baseOptions, ...(options.queryOverrides || {}) },
566
+ });
567
+
568
+ let planContent = '';
569
+ for await (const message of response) {
570
+ this.emitEvent(this.adapter.createRawSDKEvent(message));
571
+ const transformed = this.adapter.transform(message);
572
+ if (transformed) {
573
+ this.emitEvent(transformed);
574
+ }
575
+ if (message.type === 'assistant' && message.message?.content) {
576
+ for (const c of message.message.content) {
577
+ if (c.type === 'text' && c.text) planContent += c.text + '\n';
578
+ }
579
+ }
580
+ }
581
+
582
+ // Write plan.md
583
+ if (planContent.trim()) {
584
+ await this.writePlan(task.id, planContent.trim());
585
+ this.logger.info('Plan completed', { taskId: task.id });
586
+ }
587
+
588
+ // Commit plan
589
+ await this.gitManager.addAllPostHogFiles();
590
+ if (isCloudMode) {
591
+ await this.gitManager.commitAndPush(`Planning phase for ${task.title}`);
592
+ } else {
593
+ await this.gitManager.commitChanges(`Planning phase for ${task.title}`);
594
+ this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'planning' }));
595
+ return; // Local mode: return to user
596
+ }
597
+ }
598
+
599
+ // Phase 5: Build
600
+ const latestRun = task.latest_run;
601
+ const prExists = latestRun?.output && (latestRun.output as any).pr_url;
602
+
603
+ if (!prExists) {
604
+ this.logger.info('Starting build phase', { taskId: task.id });
605
+ this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'build' }));
606
+
607
+ // Run execution agent
608
+ const executionPrompt = await this.promptBuilder.buildExecutionPrompt(task, cwd);
609
+ const { EXECUTION_SYSTEM_PROMPT } = await import('./agents/execution.js');
610
+ const fullPrompt = EXECUTION_SYSTEM_PROMPT + '\n\n' + executionPrompt;
611
+
612
+ const { PermissionMode } = await import('./types.js');
613
+ const permissionMode = options.permissionMode || PermissionMode.ACCEPT_EDITS;
614
+ const baseOptions: Record<string, any> = {
615
+ model: 'claude-sonnet-4-5-20250929',
616
+ cwd,
617
+ permissionMode,
618
+ settingSources: ['local'],
619
+ mcpServers: this.mcpServers,
620
+ };
621
+
622
+ const response = query({
623
+ prompt: fullPrompt,
624
+ options: { ...baseOptions, ...(options.queryOverrides || {}) },
625
+ });
626
+
627
+ for await (const message of response) {
628
+ this.emitEvent(this.adapter.createRawSDKEvent(message));
629
+ const transformed = this.adapter.transform(message);
630
+ if (transformed) {
631
+ this.emitEvent(transformed);
632
+ }
633
+ }
634
+
635
+ // Commit and push implementation
636
+ // Stage ALL changes (not just .posthog/)
637
+ const hasChanges = await this.gitManager.hasChanges();
638
+ if (hasChanges) {
639
+ await this.gitManager.addFiles(['.']); // Stage all changes
640
+ await this.gitManager.commitChanges(`Implementation for ${task.title}`);
641
+
642
+ // Push to origin
643
+ const branchName = await this.gitManager.getCurrentBranch();
644
+ await this.gitManager.pushBranch(branchName);
645
+
646
+ this.logger.info('Implementation committed and pushed', { taskId: task.id });
647
+ } else {
648
+ this.logger.warn('No changes to commit in build phase', { taskId: task.id });
649
+ }
650
+
651
+ // Create PR
652
+ const branchName = await this.gitManager.getCurrentBranch();
653
+ const prUrl = await this.createPullRequest(task.id, branchName, task.title, task.description);
654
+ this.logger.info('Pull request created', { taskId: task.id, prUrl });
655
+ this.emitEvent(this.adapter.createStatusEvent('pr_created', { prUrl }));
656
+
657
+ // Attach PR to task run
658
+ try {
659
+ await this.attachPullRequestToTask(task.id, prUrl, branchName);
660
+ this.logger.info('PR attached to task successfully', { taskId: task.id, prUrl });
661
+ } catch (error) {
662
+ this.logger.warn('Could not attach PR to task', { error: error instanceof Error ? error.message : String(error) });
663
+ }
664
+ } else {
665
+ this.logger.info('PR already exists, skipping build phase', { taskId: task.id });
666
+ }
667
+
668
+ // Phase 6: Complete
669
+ await this.progressReporter.complete();
670
+ this.logger.info('Task execution complete', { taskId: task.id });
671
+ this.emitEvent(this.adapter.createStatusEvent('task_complete', { taskId: task.id }));
672
+ }
673
+
331
674
  async progressToNextStage(taskId: string, currentStageKey?: string): Promise<void> {
332
675
  if (!this.posthogAPI || !this.progressReporter.runId) {
333
676
  throw new Error('PostHog API not configured or no active run. Cannot progress stage.');
@@ -437,6 +780,25 @@ export class Agent {
437
780
  this.logger.debug('Reading plan', { taskId });
438
781
  return await this.fileManager.readPlan(taskId);
439
782
  }
783
+
784
+ async extractQuestionsFromResearch(taskId: string, includeAnswers: boolean = false): Promise<ExtractedQuestion[] | ExtractedQuestionWithAnswer[]> {
785
+ this.logger.info('Extracting questions from research.md', { taskId, includeAnswers });
786
+
787
+ if (!this.extractor) {
788
+ throw new Error('OpenAI extractor not initialized. Set OPENAI_API_KEY environment variable.');
789
+ }
790
+
791
+ const researchContent = await this.fileManager.readResearch(taskId);
792
+ if (!researchContent) {
793
+ throw new Error('research.md not found for task ' + taskId);
794
+ }
795
+
796
+ if (includeAnswers) {
797
+ return await this.extractor.extractQuestionsWithAnswers(researchContent);
798
+ } else {
799
+ return await this.extractor.extractQuestions(researchContent);
800
+ }
801
+ }
440
802
 
441
803
  // Git operations for task workflow
442
804
  async createPlanningBranch(taskId: string): Promise<string> {
@@ -0,0 +1,103 @@
1
+ export const RESEARCH_SYSTEM_PROMPT = `# PostHog AI Coding Agent - Research Mode
2
+
3
+ You are a PostHog AI Coding Agent operating in RESEARCH mode.
4
+
5
+ ## Your Role
6
+
7
+ You are a research agent that explores codebases to understand implementation context and generate clarifying questions for development tasks.
8
+
9
+ ## Important Constraints
10
+
11
+ - **Read-Only Mode**: You can only read files, search code, and analyze the codebase
12
+ - **No Modifications**: You cannot make any changes or edits to code files
13
+ - **Research Focus**: Your goal is understanding and asking the right questions
14
+
15
+ ## Available Tools
16
+
17
+ - File reading and exploration
18
+ - Code search and analysis
19
+ - Repository structure analysis
20
+ - Documentation review
21
+ - \`create_plan\` tool for creating your research artifact
22
+
23
+ ## Research Process
24
+
25
+ When given a task, follow this systematic approach:
26
+
27
+ 1. **Codebase Analysis**
28
+ - Explore the repository structure
29
+ - Identify relevant files and components
30
+ - Understand existing patterns and conventions
31
+ - Review related code and dependencies
32
+ - Look for similar implementations or patterns
33
+
34
+ 2. **Decision Point Identification**
35
+ - Identify areas where implementation decisions need to be made
36
+ - Find multiple viable approaches in the codebase
37
+ - Note where user preferences would affect the implementation
38
+ - Consider architectural or design pattern choices
39
+
40
+ 3. **Question Generation**
41
+ - Generate 3-5 clarifying questions
42
+ - Each question should offer 2-3 concrete options based on codebase analysis
43
+ - Options should reference actual patterns/approaches found in the code
44
+ - Always include option c) as "Something else (please specify)" for flexibility
45
+ - Focus on high-impact decisions that affect the implementation approach
46
+
47
+ ## Output Format
48
+
49
+ After completing your research, you MUST use the \`create_plan\` tool to create a research.md artifact with your questions.
50
+
51
+ The artifact MUST follow this EXACT markdown format (this is critical for parsing):
52
+
53
+ \`\`\`markdown
54
+ # Research Questions
55
+
56
+ Based on my analysis of the codebase, here are the key questions to guide implementation:
57
+
58
+ ## Question 1: [Question text - be specific and clear]
59
+
60
+ **Options:**
61
+ - a) [Concrete option based on existing pattern - reference specific files/components]
62
+ - b) [Alternative approach based on another pattern - reference specific files/components]
63
+ - c) Something else (please specify)
64
+
65
+ ## Question 2: [Next question - be specific and clear]
66
+
67
+ **Options:**
68
+ - a) [Option with specific code references]
69
+ - b) [Alternative with specific code references]
70
+ - c) Something else (please specify)
71
+
72
+ ## Question 3: [Continue with 3-5 questions total]
73
+
74
+ **Options:**
75
+ - a) [Option]
76
+ - b) [Alternative]
77
+ - c) Something else (please specify)
78
+ \`\`\`
79
+
80
+ ## CRITICAL FORMAT REQUIREMENTS
81
+
82
+ - Use EXACTLY "## Question N:" format for question headers (h2 level, not h3)
83
+ - Each question MUST be followed by "**Options:**" on its own line
84
+ - Each option MUST start with "- a)", "- b)", "- c)", etc.
85
+ - Always include "c) Something else (please specify)" as the last option
86
+ - Do NOT add extra sections between questions
87
+ - Keep context and analysis BEFORE the questions section, not mixed in
88
+
89
+ ## Important Requirements
90
+
91
+ - Generate 2-5 questions (no more, no less)
92
+ - Make options specific and reference actual code/patterns you find
93
+ - Each question must have at least 2 concrete options plus "Something else"
94
+ - Focus on architectural and implementation approach decisions
95
+ - Reference specific files, components, or patterns in your options
96
+ - Make sure the questions help guide a clear implementation path
97
+
98
+ ## Final Step
99
+
100
+ Once you have completed your research and identified the questions, use the \`create_plan\` tool to create the research.md artifact with the markdown content above. Do NOT use any other tools after creating the artifact.
101
+
102
+ Your research should be thorough enough that the questions help clarify the user's preferences and guide the planning phase effectively.`;
103
+
@@ -9,6 +9,24 @@ export interface TaskFile {
9
9
  type: 'plan' | 'context' | 'reference' | 'output' | 'artifact';
10
10
  }
11
11
 
12
+ export interface QuestionData {
13
+ id: string;
14
+ question: string;
15
+ options: string[];
16
+ }
17
+
18
+ export interface AnswerData {
19
+ questionId: string;
20
+ selectedOption: string;
21
+ customInput?: string;
22
+ }
23
+
24
+ export interface QuestionsFile {
25
+ questions: QuestionData[];
26
+ answered: boolean;
27
+ answers: AnswerData[] | null;
28
+ }
29
+
12
30
  export class PostHogFileManager {
13
31
  private repositoryPath: string;
14
32
  private logger: Logger;
@@ -152,6 +170,52 @@ export class PostHogFileManager {
152
170
  return await this.readTaskFile(taskId, 'requirements.md');
153
171
  }
154
172
 
173
+ async writeResearch(taskId: string, content: string): Promise<void> {
174
+ this.logger.debug('Writing research', {
175
+ taskId,
176
+ contentLength: content.length,
177
+ contentPreview: content.substring(0, 200)
178
+ });
179
+
180
+ await this.writeTaskFile(taskId, {
181
+ name: 'research.md',
182
+ content: content,
183
+ type: 'artifact'
184
+ });
185
+
186
+ this.logger.info('Research file written', { taskId });
187
+ }
188
+
189
+ async readResearch(taskId: string): Promise<string | null> {
190
+ return await this.readTaskFile(taskId, 'research.md');
191
+ }
192
+
193
+ async writeQuestions(taskId: string, data: QuestionsFile): Promise<void> {
194
+ this.logger.debug('Writing questions', {
195
+ taskId,
196
+ questionCount: data.questions.length,
197
+ answered: data.answered,
198
+ });
199
+
200
+ await this.writeTaskFile(taskId, {
201
+ name: 'questions.json',
202
+ content: JSON.stringify(data, null, 2),
203
+ type: 'artifact'
204
+ });
205
+
206
+ this.logger.info('Questions file written', { taskId });
207
+ }
208
+
209
+ async readQuestions(taskId: string): Promise<QuestionsFile | null> {
210
+ try {
211
+ const content = await this.readTaskFile(taskId, 'questions.json');
212
+ return content ? JSON.parse(content) as QuestionsFile : null;
213
+ } catch (error) {
214
+ this.logger.debug('Failed to parse questions.json', { error });
215
+ return null;
216
+ }
217
+ }
218
+
155
219
  async getTaskFiles(taskId: string): Promise<SupportingFile[]> {
156
220
  const fileNames = await this.listTaskFiles(taskId);
157
221
  const files: SupportingFile[] = [];
@@ -341,4 +341,56 @@ Generated by PostHog Agent`;
341
341
  throw new Error(`Failed to create PR: ${error}`);
342
342
  }
343
343
  }
344
+
345
+ async getTaskBranch(taskSlug: string): Promise<string | null> {
346
+ try {
347
+ // Get all branches matching the task slug pattern
348
+ const branches = await this.runGitCommand('branch --list --all');
349
+ const branchPattern = `posthog/task-${taskSlug}`;
350
+
351
+ // Look for exact match or with counter suffix
352
+ const lines = branches.split('\n').map(l => l.trim().replace(/^\*\s+/, ''));
353
+ for (const line of lines) {
354
+ const cleanBranch = line.replace('remotes/origin/', '');
355
+ if (cleanBranch.startsWith(branchPattern)) {
356
+ return cleanBranch;
357
+ }
358
+ }
359
+
360
+ return null;
361
+ } catch (error) {
362
+ this.logger.debug('Failed to get task branch', { taskSlug, error });
363
+ return null;
364
+ }
365
+ }
366
+
367
+ async commitAndPush(message: string, options?: { allowEmpty?: boolean }): Promise<void> {
368
+ const hasChanges = await this.hasStagedChanges();
369
+
370
+ if (!hasChanges && !options?.allowEmpty) {
371
+ this.logger.debug('No changes to commit, skipping');
372
+ return;
373
+ }
374
+
375
+ let command = `commit -m "${message.replace(/"/g, '\\"')}"`;
376
+
377
+ if (options?.allowEmpty) {
378
+ command += ' --allow-empty';
379
+ }
380
+
381
+ const authorName = this.authorName;
382
+ const authorEmail = this.authorEmail;
383
+
384
+ if (authorName && authorEmail) {
385
+ command += ` --author="${authorName} <${authorEmail}>"`;
386
+ }
387
+
388
+ await this.runGitCommand(command);
389
+
390
+ // Push to origin
391
+ const currentBranch = await this.getCurrentBranch();
392
+ await this.pushBranch(currentBranch);
393
+
394
+ this.logger.info('Committed and pushed changes', { branch: currentBranch, message });
395
+ }
344
396
  }