vibeman 0.0.1 → 0.0.2

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 (71) hide show
  1. package/dist/index.js +5 -7
  2. package/dist/runtime/api/.tsbuildinfo +1 -1
  3. package/dist/runtime/api/agent/agent-service.d.ts +11 -13
  4. package/dist/runtime/api/agent/agent-service.js +25 -31
  5. package/dist/runtime/api/agent/ai-providers/claude-code-adapter.d.ts +2 -2
  6. package/dist/runtime/api/agent/ai-providers/claude-code-adapter.js +25 -36
  7. package/dist/runtime/api/agent/ai-providers/codex-cli-provider.js +48 -14
  8. package/dist/runtime/api/agent/ai-providers/types.d.ts +2 -0
  9. package/dist/runtime/api/agent/codex-cli-provider.test.js +37 -0
  10. package/dist/runtime/api/agent/parsers.d.ts +1 -0
  11. package/dist/runtime/api/agent/parsers.js +75 -8
  12. package/dist/runtime/api/agent/prompt-service.d.ts +14 -1
  13. package/dist/runtime/api/agent/prompt-service.js +123 -14
  14. package/dist/runtime/api/agent/prompt-service.test.d.ts +1 -0
  15. package/dist/runtime/api/agent/prompt-service.test.js +230 -0
  16. package/dist/runtime/api/agent/routing-policy.d.ts +14 -14
  17. package/dist/runtime/api/api/routers/ai.d.ts +6 -6
  18. package/dist/runtime/api/api/routers/ai.js +2 -17
  19. package/dist/runtime/api/api/routers/executions.d.ts +5 -5
  20. package/dist/runtime/api/api/routers/executions.js +12 -21
  21. package/dist/runtime/api/api/routers/provider-config.d.ts +165 -0
  22. package/dist/runtime/api/api/routers/provider-config.js +252 -0
  23. package/dist/runtime/api/api/routers/tasks.d.ts +10 -10
  24. package/dist/runtime/api/api/routers/workflows.d.ts +15 -16
  25. package/dist/runtime/api/api/routers/workflows.js +28 -26
  26. package/dist/runtime/api/api/routers/worktrees.d.ts +4 -5
  27. package/dist/runtime/api/api/routers/worktrees.js +11 -11
  28. package/dist/runtime/api/api/trpc.d.ts +18 -18
  29. package/dist/runtime/api/index.js +2 -10
  30. package/dist/runtime/api/lib/local-config.d.ts +245 -0
  31. package/dist/runtime/api/lib/local-config.js +288 -0
  32. package/dist/runtime/api/lib/provider-detection.d.ts +59 -0
  33. package/dist/runtime/api/lib/provider-detection.js +244 -0
  34. package/dist/runtime/api/lib/server/bootstrap.d.ts +38 -0
  35. package/dist/runtime/api/lib/server/bootstrap.js +197 -0
  36. package/dist/runtime/api/lib/server/project-root.js +24 -1
  37. package/dist/runtime/api/lib/trpc/server.d.ts +124 -31
  38. package/dist/runtime/api/lib/trpc/server.js +8 -8
  39. package/dist/runtime/api/lib/trpc/ws-server.js +2 -2
  40. package/dist/runtime/api/router.d.ts +125 -32
  41. package/dist/runtime/api/router.js +9 -31
  42. package/dist/runtime/api/settings-service.js +2 -0
  43. package/dist/runtime/api/workflows/vibing-orchestrator.d.ts +8 -3
  44. package/dist/runtime/api/workflows/vibing-orchestrator.js +182 -183
  45. package/dist/runtime/web/.next/BUILD_ID +1 -1
  46. package/dist/runtime/web/.next/app-build-manifest.json +2 -2
  47. package/dist/runtime/web/.next/build-manifest.json +2 -2
  48. package/dist/runtime/web/.next/prerender-manifest.json +3 -3
  49. package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  50. package/dist/runtime/web/.next/server/app/_not-found.html +2 -2
  51. package/dist/runtime/web/.next/server/app/_not-found.rsc +5 -5
  52. package/dist/runtime/web/.next/server/app/api/health/route.js +1 -1
  53. package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  54. package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +1 -1
  55. package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +1 -1
  56. package/dist/runtime/web/.next/server/app/index.html +2 -2
  57. package/dist/runtime/web/.next/server/app/index.rsc +6 -6
  58. package/dist/runtime/web/.next/server/app/page.js +3 -3
  59. package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +1 -1
  60. package/dist/runtime/web/.next/server/chunks/458.js +1 -1
  61. package/dist/runtime/web/.next/server/pages/404.html +2 -2
  62. package/dist/runtime/web/.next/server/pages/500.html +1 -1
  63. package/dist/runtime/web/.next/server/pages-manifest.json +1 -1
  64. package/dist/runtime/web/.next/server/server-reference-manifest.json +1 -1
  65. package/dist/runtime/web/.next/static/chunks/app/{layout-dc0cfd29075b2160.js → layout-8435322f09fd0975.js} +1 -1
  66. package/dist/runtime/web/.next/static/chunks/app/page-8c3ba579efc6f918.js +1 -0
  67. package/dist/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +5 -1
  69. package/dist/runtime/web/.next/static/chunks/app/page-f34a8b196b18850b.js +0 -1
  70. /package/dist/runtime/web/.next/static/{1HR8N0rJkCvFRtbTPJMyH → mRpNgPfbYR_0wrODzlg_4}/_buildManifest.js +0 -0
  71. /package/dist/runtime/web/.next/static/{1HR8N0rJkCvFRtbTPJMyH → mRpNgPfbYR_0wrODzlg_4}/_ssgManifest.js +0 -0
@@ -64,6 +64,7 @@ export declare class AgentService extends EventEmitter {
64
64
  attempt?: number;
65
65
  };
66
66
  providerOverride?: Partial<ResolvedProvider>;
67
+ workflowConfig?: import('../types/index.js').VibingConfig;
67
68
  }): Promise<string>;
68
69
  /**
69
70
  * Build an appended system prompt with rerun context (reason, logs, failed checks)
@@ -97,13 +98,18 @@ export declare class AgentService extends EventEmitter {
97
98
  type: string;
98
99
  priority: string;
99
100
  content: string;
101
+ title?: string;
100
102
  executionId: string;
101
103
  selectedModel?: string;
102
104
  }>;
103
105
  /**
104
106
  * Perform AI code review of changes in a worktree
105
107
  */
106
- aiReviewCode(taskId: string, reviewContext?: string, overrides?: Partial<ResolvedProvider>): Promise<{
108
+ aiReviewCode(taskId: string, reviewContext?: string, options?: {
109
+ overrides?: Partial<ResolvedProvider>;
110
+ executionId?: string;
111
+ workflowId?: string;
112
+ }): Promise<{
107
113
  executionId: string;
108
114
  reviewSummary: string;
109
115
  recommendations: string[];
@@ -114,19 +120,11 @@ export declare class AgentService extends EventEmitter {
114
120
  */
115
121
  aiMerge(taskId: string, options?: {
116
122
  baseBranch?: string;
117
- }, overrides?: Partial<ResolvedProvider>): Promise<{
118
- executionId: string;
119
- }>;
120
- /**
121
- * Compatibility alias for aiReviewCode
122
- * @deprecated Use aiReviewCode instead
123
- * TODO: migrate UI to use aiReviewCode directly and remove this
124
- */
125
- reviewCode(taskId: string, reviewContext?: string, overrides?: Partial<ResolvedProvider>): Promise<{
123
+ overrides?: Partial<ResolvedProvider>;
124
+ executionId?: string;
125
+ workflowId?: string;
126
+ }): Promise<{
126
127
  executionId: string;
127
- reviewSummary: string;
128
- recommendations: string[];
129
- qualityScore: number;
130
128
  }>;
131
129
  /**
132
130
  * Get persistent execution logs
@@ -325,8 +325,8 @@ export class AgentService extends EventEmitter {
325
325
  catch (error) {
326
326
  log.error('Failed to create worktree, using main directory', error, 'agent-service');
327
327
  }
328
- // Generate task prompt
329
- const prompt = await this.promptService.generateTaskPrompt(task);
328
+ // Generate task prompt with workflow configuration for dynamic instructions
329
+ const prompt = await this.promptService.generateTaskPrompt(task, options?.workflowConfig);
330
330
  // Build system prompt - use rerun context if available, otherwise base prompt
331
331
  let appendSystemPrompt;
332
332
  if (options?.rerunContext) {
@@ -349,6 +349,7 @@ export class AgentService extends EventEmitter {
349
349
  tools: this.defaultTools.execute_task,
350
350
  timeout: 30 * 60 * 1000,
351
351
  appendSystemPrompt,
352
+ dangerouslyBypassApprovalsAndSandbox: true,
352
353
  executionId,
353
354
  metadata: {
354
355
  operation: 'execute_task',
@@ -538,6 +539,7 @@ export class AgentService extends EventEmitter {
538
539
  appendSystemPrompt: this.systemPrompts.improve_task,
539
540
  tools: this.defaultTools.improve_task,
540
541
  timeout: 5 * 60 * 1000,
542
+ dangerouslyBypassApprovalsAndSandbox: false,
541
543
  executionId: finalExecutionId,
542
544
  metadata: {
543
545
  operation: 'improve_task',
@@ -559,7 +561,7 @@ export class AgentService extends EventEmitter {
559
561
  /**
560
562
  * Perform AI code review of changes in a worktree
561
563
  */
562
- async aiReviewCode(taskId, reviewContext, overrides) {
564
+ async aiReviewCode(taskId, reviewContext, options) {
563
565
  const task = this.taskService.getTask(taskId);
564
566
  if (!task) {
565
567
  throw new Error(`Task ${taskId} not found`);
@@ -568,7 +570,7 @@ export class AgentService extends EventEmitter {
568
570
  if (!worktree) {
569
571
  throw new Error(`No worktree found for task ${taskId}`);
570
572
  }
571
- const executionId = generateId('review');
573
+ const executionId = options?.executionId ?? generateId('review');
572
574
  try {
573
575
  // Pre-register execution for streaming/persistence
574
576
  if (!this.executionRegistry.has(executionId)) {
@@ -586,7 +588,7 @@ export class AgentService extends EventEmitter {
586
588
  logs: [],
587
589
  workingDirectory: this.projectRoot,
588
590
  };
589
- await this.logPersistence.startExecution(execRec);
591
+ await this.logPersistence.startExecution(execRec, options?.workflowId);
590
592
  }
591
593
  // Generate review prompt
592
594
  const reviewPrompt = await this.promptService.generateReviewPrompt(task, worktree, reviewContext);
@@ -606,7 +608,7 @@ export class AgentService extends EventEmitter {
606
608
  operation: 'ai_codereview',
607
609
  taskId,
608
610
  },
609
- }, overrides);
611
+ }, options?.overrides);
610
612
  // Parse review result
611
613
  const review = this.parseReviewResult(result);
612
614
  const payload = { executionId, ...review };
@@ -627,7 +629,7 @@ export class AgentService extends EventEmitter {
627
629
  /**
628
630
  * Perform AI-assisted merge of a task's worktree into the base project
629
631
  */
630
- async aiMerge(taskId, options, overrides) {
632
+ async aiMerge(taskId, options) {
631
633
  const task = this.taskService.getTask(taskId);
632
634
  if (!task) {
633
635
  throw new Error(`Task ${taskId} not found`);
@@ -636,14 +638,15 @@ export class AgentService extends EventEmitter {
636
638
  if (!worktree) {
637
639
  throw new Error(`No worktree found for task ${taskId}`);
638
640
  }
639
- const executionId = generateId('merge');
641
+ const executionId = options?.executionId ?? generateId('merge');
642
+ const workingDirectory = worktree.path || this.projectRoot;
640
643
  try {
641
644
  // Pre-register execution for streaming/persistence
642
645
  if (!this.executionRegistry.has(executionId)) {
643
646
  const startTime = new Date().toISOString();
644
647
  this.executionRegistry.set(executionId, {
645
648
  taskId,
646
- workingDirectory: this.projectRoot,
649
+ workingDirectory,
647
650
  startTime,
648
651
  });
649
652
  const execRec = {
@@ -652,21 +655,21 @@ export class AgentService extends EventEmitter {
652
655
  status: 'pending',
653
656
  startTime,
654
657
  logs: [],
655
- workingDirectory: this.projectRoot,
658
+ workingDirectory,
656
659
  };
657
- await this.logPersistence.startExecution(execRec);
660
+ await this.logPersistence.startExecution(execRec, options?.workflowId);
658
661
  }
659
662
  // Generate merge prompt
660
663
  const mergePrompt = await this.promptService.generateAIMergePrompt(task, worktree, options?.baseBranch);
661
664
  void this.logPersistence.logMessage(executionId, 'Merge prompt prepared', 'info', {
662
665
  prompt: mergePrompt,
663
- options: { workingDirectory: this.projectRoot, tools: this.defaultTools.ai_merge },
666
+ options: { workingDirectory, tools: this.defaultTools.ai_merge },
664
667
  taskId,
665
668
  baseBranch: options?.baseBranch,
666
669
  });
667
670
  // Execute merge with AI
668
671
  await this.executeWithRouting('ai_merge', mergePrompt, {
669
- workingDirectory: this.projectRoot,
672
+ workingDirectory,
670
673
  appendSystemPrompt: this.systemPrompts.ai_merge,
671
674
  tools: this.defaultTools.ai_merge,
672
675
  timeout: 15 * 60 * 1000,
@@ -676,29 +679,13 @@ export class AgentService extends EventEmitter {
676
679
  taskId,
677
680
  baseBranch: options?.baseBranch,
678
681
  },
679
- }, overrides);
680
- // Ensure registry exists for lookups (no-op otherwise)
681
- if (!this.executionRegistry.has(executionId)) {
682
- this.executionRegistry.set(executionId, {
683
- taskId,
684
- workingDirectory: this.projectRoot,
685
- startTime: new Date().toISOString(),
686
- });
687
- }
682
+ }, options?.overrides);
688
683
  return { executionId };
689
684
  }
690
685
  catch (error) {
691
686
  throw new Error(`AI merge failed: ${error instanceof Error ? error.message : String(error)}`);
692
687
  }
693
688
  }
694
- /**
695
- * Compatibility alias for aiReviewCode
696
- * @deprecated Use aiReviewCode instead
697
- * TODO: migrate UI to use aiReviewCode directly and remove this
698
- */
699
- async reviewCode(taskId, reviewContext, overrides) {
700
- return this.aiReviewCode(taskId, reviewContext, overrides);
701
- }
702
689
  /**
703
690
  * Get persistent execution logs
704
691
  */
@@ -843,8 +830,15 @@ export class AgentService extends EventEmitter {
843
830
  catch {
844
831
  // best effort; ignore if settings unavailable
845
832
  }
846
- let lastError = null;
847
833
  const providersToTry = [resolved.provider, ...(resolved.fallbacks || [])];
834
+ // Ensure Codex executes with write permissions when it is in the routing set.
835
+ // Some overrides may omit the flag, so default it on for task execution.
836
+ if (operation === 'execute_task' &&
837
+ providersToTry.includes('codex') &&
838
+ executionOptions.dangerouslyBypassApprovalsAndSandbox === undefined) {
839
+ executionOptions.dangerouslyBypassApprovalsAndSandbox = true;
840
+ }
841
+ let lastError = null;
848
842
  for (let i = 0; i < providersToTry.length; i++) {
849
843
  const provider = providersToTry[i];
850
844
  const isLastProvider = i === providersToTry.length - 1;
@@ -43,7 +43,7 @@ export declare class ClaudeCodeAdapter implements AIProvider {
43
43
  */
44
44
  detectAvailableModels(): Promise<ModelInfo[]>;
45
45
  /**
46
- * Validate Claude Code setup
46
+ * Validate Claude Code setup using enhanced detection
47
47
  */
48
48
  validateSetup(): Promise<ProviderStatus>;
49
49
  /**
@@ -55,7 +55,7 @@ export declare class ClaudeCodeAdapter implements AIProvider {
55
55
  */
56
56
  private mapToClaudeOptions;
57
57
  /**
58
- * Resolve Claude CLI executable path
58
+ * Resolve Claude CLI executable path using enhanced detection
59
59
  */
60
60
  private getClaudeExecutablePath;
61
61
  }
@@ -3,10 +3,9 @@
3
3
  * Implements AIProvider interface for Claude Code SDK
4
4
  */
5
5
  import { query } from '@anthropic-ai/claude-code';
6
- import path from 'path';
7
- import os from 'os';
8
6
  import { generateId } from '../../lib/id-generator.js';
9
7
  import { getSettingsService } from '../../settings-service.js';
8
+ import { getProviderDetectionService } from '../../lib/provider-detection.js';
10
9
  /**
11
10
  * Claude Code Adapter
12
11
  * Wraps the Claude Code SDK to implement the AIProvider interface
@@ -21,7 +20,7 @@ export class ClaudeCodeAdapter {
21
20
  * Execute a prompt using Claude Code SDK
22
21
  */
23
22
  async *execute(prompt, options) {
24
- const claudeOptions = this.mapToClaudeOptions(options);
23
+ const claudeOptions = await this.mapToClaudeOptions(options);
25
24
  const co = claudeOptions;
26
25
  const startTime = Date.now();
27
26
  let sessionId;
@@ -249,30 +248,19 @@ export class ClaudeCodeAdapter {
249
248
  ];
250
249
  }
251
250
  /**
252
- * Validate Claude Code setup
251
+ * Validate Claude Code setup using enhanced detection
253
252
  */
254
253
  async validateSetup() {
255
254
  try {
256
- const claudePath = this.getClaudeExecutablePath();
257
- const fs = await import('fs/promises');
258
- // Check if Claude executable exists
259
- try {
260
- await fs.access(claudePath);
261
- }
262
- catch {
263
- // Try to execute claude directly (might be in PATH)
264
- const { execSync } = await import('child_process');
265
- try {
266
- execSync('claude --version', { stdio: 'ignore' });
267
- }
268
- catch {
269
- return {
270
- available: false,
271
- error: 'Claude CLI not found. Please install Claude Code CLI.',
272
- models: [],
273
- capabilities: this.getCapabilities(),
274
- };
275
- }
255
+ const detectionService = getProviderDetectionService();
256
+ const result = await detectionService.detectProvider('claude-code');
257
+ if (!result.found) {
258
+ return {
259
+ available: false,
260
+ error: result.error || 'Claude CLI not found. Please install Claude Code CLI.',
261
+ models: [],
262
+ capabilities: this.getCapabilities(),
263
+ };
276
264
  }
277
265
  const models = await this.detectAvailableModels();
278
266
  return {
@@ -307,11 +295,11 @@ export class ClaudeCodeAdapter {
307
295
  /**
308
296
  * Map ExecutionOptions to Claude Code Options
309
297
  */
310
- mapToClaudeOptions(options) {
298
+ async mapToClaudeOptions(options) {
311
299
  const claudeOptions = {
312
300
  cwd: options?.workingDirectory || this.config.defaultWorkingDirectory || process.cwd(),
313
301
  model: options?.model || this.config.defaultModel,
314
- pathToClaudeCodeExecutable: this.getClaudeExecutablePath(),
302
+ pathToClaudeCodeExecutable: await this.getClaudeExecutablePath(),
315
303
  };
316
304
  // Map temperature
317
305
  if (options?.temperature !== undefined) {
@@ -342,18 +330,20 @@ export class ClaudeCodeAdapter {
342
330
  return claudeOptions;
343
331
  }
344
332
  /**
345
- * Resolve Claude CLI executable path
333
+ * Resolve Claude CLI executable path using enhanced detection
346
334
  */
347
- getClaudeExecutablePath() {
348
- // Highest precedence: explicit env var (documented in README)
349
- const envPath = process.env.VIBEMAN_CLAUDE_BIN;
350
- if (envPath && envPath.trim()) {
351
- return envPath.trim();
352
- }
335
+ async getClaudeExecutablePath() {
336
+ // Highest precedence: explicit config option
353
337
  if (this.config.claudeBinPath) {
354
338
  return this.config.claudeBinPath;
355
339
  }
356
- // Check settings first
340
+ // Use enhanced detection service
341
+ const detectionService = getProviderDetectionService();
342
+ const result = await detectionService.detectProvider('claude-code');
343
+ if (result.found && result.path) {
344
+ return result.path;
345
+ }
346
+ // Fallback: try old settings-based approach for backwards compatibility
357
347
  const settingsBinPath = (() => {
358
348
  try {
359
349
  const svc = getSettingsService();
@@ -367,7 +357,6 @@ export class ClaudeCodeAdapter {
367
357
  if (settingsBinPath?.trim()) {
368
358
  return settingsBinPath.trim();
369
359
  }
370
- // Default to local installation
371
- return path.join(os.homedir(), '.claude', 'local', 'claude');
360
+ return 'claude';
372
361
  }
373
362
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { spawn } from 'child_process';
6
6
  import { getSettingsService } from '../../settings-service.js';
7
+ import { getProviderDetectionService } from '../../lib/provider-detection.js';
7
8
  import { logger } from '../../lib/logger.js';
8
9
  export class CodexCliProvider {
9
10
  constructor(config = {}) {
@@ -11,10 +12,16 @@ export class CodexCliProvider {
11
12
  this.name = 'codex';
12
13
  this.displayName = 'Codex CLI';
13
14
  }
14
- resolveExecutable() {
15
+ async resolveExecutable() {
15
16
  if (this.config.codexBinPath)
16
17
  return this.config.codexBinPath;
17
- // Check settings first
18
+ // Use enhanced detection service
19
+ const detectionService = getProviderDetectionService();
20
+ const result = await detectionService.detectProvider('codex');
21
+ if (result.found && result.path) {
22
+ return result.path;
23
+ }
24
+ // Fallback: try old settings-based approach for backwards compatibility
18
25
  const settingsBinPath = (() => {
19
26
  try {
20
27
  const svc = getSettingsService();
@@ -51,8 +58,19 @@ export class CodexCliProvider {
51
58
  const images = (options?.images || []).filter(Boolean);
52
59
  const effort = options?.effort;
53
60
  const timeoutMs = options?.timeout ?? this.config.defaultTimeoutMs ?? 10 * 60 * 1000; // 10m
61
+ const systemPrompt = options?.systemPrompt?.trim();
62
+ const appendSystemPrompt = options?.appendSystemPrompt?.trim();
63
+ const promptSegments = [];
64
+ if (systemPrompt) {
65
+ promptSegments.push(systemPrompt);
66
+ }
67
+ if (appendSystemPrompt) {
68
+ promptSegments.push(appendSystemPrompt);
69
+ }
70
+ promptSegments.push(prompt);
71
+ const effectivePrompt = promptSegments.join('\n\n');
54
72
  // Build argv for `codex exec` (non-interactive automation mode)
55
- const argv = ['exec', prompt];
73
+ const argv = ['exec', effectivePrompt];
56
74
  if (model) {
57
75
  argv.push('--model', model);
58
76
  }
@@ -63,7 +81,10 @@ export class CodexCliProvider {
63
81
  if (images.length) {
64
82
  argv.push('--image', images.join(','));
65
83
  }
66
- const cmd = this.resolveExecutable();
84
+ if (options?.dangerouslyBypassApprovalsAndSandbox) {
85
+ argv.push('--dangerously-bypass-approvals-and-sandbox');
86
+ }
87
+ const cmd = await this.resolveExecutable();
67
88
  const child = spawn(cmd, argv, {
68
89
  cwd,
69
90
  env: { ...process.env },
@@ -81,8 +102,17 @@ export class CodexCliProvider {
81
102
  yield {
82
103
  type: 'system',
83
104
  timestamp: new Date().toISOString(),
84
- content: `Running: ${cmd} ${argv.map((a) => (a.includes(' ') ? '"' + a + '"' : a)).join(' ')} (cwd=${cwd})`,
85
- metadata: { provider: this.name, images, effort },
105
+ content: `Running: ${cmd} ${argv
106
+ .map((a) => (a.includes(' ') ? '"' + a + '"' : a))
107
+ .join(' ')} (cwd=${cwd})`,
108
+ metadata: {
109
+ provider: this.name,
110
+ images,
111
+ effort,
112
+ systemPrompt,
113
+ appendSystemPrompt,
114
+ dangerouslyBypassApprovalsAndSandbox: !!options?.dangerouslyBypassApprovalsAndSandbox,
115
+ },
86
116
  };
87
117
  let stdoutBuf = '';
88
118
  // let stderrBuf = '';
@@ -243,13 +273,17 @@ export class CodexCliProvider {
243
273
  }
244
274
  async validateSetup() {
245
275
  try {
246
- const bin = this.resolveExecutable();
247
- // Try to spawn `codex --help` quickly to validate presence
248
- const child = spawn(bin, ['--help'], { stdio: 'ignore' });
249
- await new Promise((resolve, reject) => {
250
- child.once('error', reject);
251
- child.once('close', () => resolve());
252
- });
276
+ const detectionService = getProviderDetectionService();
277
+ const result = await detectionService.detectProvider('codex');
278
+ if (!result.found) {
279
+ return {
280
+ available: false,
281
+ error: result.error ||
282
+ 'Codex CLI not found. Install from https://github.com/openai/codex (see Getting Started: CLI usage).',
283
+ models: [],
284
+ capabilities: this.getCapabilities(),
285
+ };
286
+ }
253
287
  return {
254
288
  available: true,
255
289
  models: await this.detectAvailableModels(),
@@ -260,7 +294,7 @@ export class CodexCliProvider {
260
294
  logger.error(error);
261
295
  return {
262
296
  available: false,
263
- error: 'Codex CLI not found. Install from https://github.com/openai/codex (see Getting Started: CLI usage).',
297
+ error: error instanceof Error ? error.message : String(error),
264
298
  models: [],
265
299
  capabilities: this.getCapabilities(),
266
300
  };
@@ -45,6 +45,8 @@ export interface ExecutionOptions {
45
45
  images?: string[];
46
46
  /** Optional effort hint for reasoning presets (e.g., 'minimal'|'low'|'medium'|'high'); informational only */
47
47
  effort?: string;
48
+ /** Enable Codex CLI sandbox bypass flag when running automation that must edit files */
49
+ dangerouslyBypassApprovalsAndSandbox?: boolean;
48
50
  }
49
51
  /**
50
52
  * Message types for streaming execution
@@ -55,6 +55,43 @@ describe('CodexCliProvider (mocked)', () => {
55
55
  expect(modelIdx).toBeGreaterThan(-1);
56
56
  expect(captured[1][modelIdx + 1]).toBe('gpt-5');
57
57
  });
58
+ it('prepends system prompts and toggles sandbox bypass flag', async () => {
59
+ vi.resetModules();
60
+ vi.doMock('child_process', () => {
61
+ let captured = [];
62
+ return {
63
+ __captured: () => captured,
64
+ spawn: (cmd, args, opts) => {
65
+ captured = [cmd, args, opts];
66
+ const { EventEmitter } = require('events');
67
+ const stdout = new EventEmitter();
68
+ const proc = new EventEmitter();
69
+ proc.stdout = stdout;
70
+ setTimeout(() => stdout.emit('data', Buffer.from('Done\n')), 5);
71
+ setTimeout(() => proc.emit('exit', 0, null), 10);
72
+ return proc;
73
+ },
74
+ };
75
+ });
76
+ const { CodexCliProvider } = await import('./ai-providers/codex-cli-provider.js');
77
+ const provider = new CodexCliProvider();
78
+ const iter = provider.execute('Implement feature', {
79
+ workingDirectory: '/tmp/project',
80
+ systemPrompt: 'Base system prompt',
81
+ appendSystemPrompt: 'Additional guidance',
82
+ dangerouslyBypassApprovalsAndSandbox: true,
83
+ });
84
+ for await (const _ of iter) {
85
+ // drain iterator
86
+ }
87
+ const mockSpawn = await import('child_process');
88
+ const captured = mockSpawn.__captured();
89
+ expect(captured[1][1]).toContain('Base system prompt');
90
+ expect(captured[1][1]).toContain('Additional guidance');
91
+ const bypassIdx = captured[1].indexOf('--dangerously-bypass-approvals-and-sandbox');
92
+ expect(bypassIdx).toBeGreaterThan(-1);
93
+ expect(captured[1][bypassIdx + 1]).toBeUndefined();
94
+ });
58
95
  });
59
96
  // Real integration (opt-in)
60
97
  (USE_REAL ? describe : describe.skip)('CodexCliProvider (real CLI)', () => {
@@ -7,6 +7,7 @@ export declare function parseImprovementResult(result: string, originalData: {
7
7
  type: string;
8
8
  priority: string;
9
9
  content: string;
10
+ title?: string;
10
11
  };
11
12
  export declare function parseReviewResult(result: string): {
12
13
  reviewSummary: string;
@@ -60,9 +60,10 @@ function extractLastValidJson(text) {
60
60
  */
61
61
  function parseJsonManually(jsonStr) {
62
62
  try {
63
- // Extract type and priority with simple regex (these are usually well-formed)
63
+ // Extract type, priority and title with simple regex (these are usually well-formed)
64
64
  const typeMatch = jsonStr.match(/"type":\s*"([^"]+)"/);
65
65
  const priorityMatch = jsonStr.match(/"priority":\s*"([^"]+)"/);
66
+ const titleMatch = jsonStr.match(/"title":\s*"([^"]+)"/);
66
67
  // For content, find the start and extract everything until the closing brace
67
68
  const contentStart = jsonStr.indexOf('"content":');
68
69
  if (contentStart === -1)
@@ -103,11 +104,16 @@ function parseJsonManually(jsonStr) {
103
104
  .replace(/\\t/g, '\t')
104
105
  .replace(/\\"/g, '"')
105
106
  .replace(/\\\\/g, '\\');
106
- return {
107
+ const result = {
107
108
  type: typeMatch[1],
108
109
  priority: priorityMatch[1],
109
110
  content: content,
110
111
  };
112
+ // Add title if found
113
+ if (titleMatch) {
114
+ result.title = titleMatch[1];
115
+ }
116
+ return result;
111
117
  }
112
118
  return null;
113
119
  }
@@ -117,6 +123,24 @@ function parseJsonManually(jsonStr) {
117
123
  return null;
118
124
  }
119
125
  }
126
+ /**
127
+ * Extract title from markdown content and remove it from content
128
+ * Returns { title: string | null, cleanedContent: string }
129
+ */
130
+ function extractTitleFromContent(content) {
131
+ if (!content || typeof content !== 'string') {
132
+ return { title: null, cleanedContent: content };
133
+ }
134
+ // Look for H1 heading at the start of content (with optional whitespace)
135
+ const h1Match = content.match(/^\s*#\s+([^\n\r]+)/);
136
+ if (h1Match) {
137
+ const title = h1Match[1].trim();
138
+ // Remove the H1 heading from content
139
+ const cleanedContent = content.replace(/^\s*#\s+[^\n\r]+\s*\n?/, '').trim();
140
+ return { title, cleanedContent };
141
+ }
142
+ return { title: null, cleanedContent: content };
143
+ }
120
144
  export function parseImprovementResult(result, originalData) {
121
145
  try {
122
146
  const jsonBlock = extractLastValidJson(result);
@@ -130,13 +154,31 @@ export function parseImprovementResult(result, originalData) {
130
154
  const validPriorities = ['high', 'medium', 'low'];
131
155
  const normalizedType = String(parsed.type).toLowerCase();
132
156
  const normalizedPriority = String(parsed.priority).toLowerCase();
133
- return {
157
+ // Extract title from JSON if present, otherwise from content
158
+ let finalTitle;
159
+ let finalContent = String(parsed.content).trim();
160
+ if (parsed.title && typeof parsed.title === 'string' && parsed.title.trim()) {
161
+ finalTitle = parsed.title.trim();
162
+ }
163
+ else {
164
+ // Try to extract title from content
165
+ const { title, cleanedContent } = extractTitleFromContent(finalContent);
166
+ if (title) {
167
+ finalTitle = title;
168
+ finalContent = cleanedContent;
169
+ }
170
+ }
171
+ const result = {
134
172
  type: validTypes.includes(normalizedType) ? normalizedType : originalData.type,
135
173
  priority: validPriorities.includes(normalizedPriority)
136
174
  ? normalizedPriority
137
175
  : originalData.priority,
138
- content: String(parsed.content).trim(),
176
+ content: finalContent,
139
177
  };
178
+ if (finalTitle) {
179
+ result.title = finalTitle;
180
+ }
181
+ return result;
140
182
  }
141
183
  }
142
184
  catch (parseError) {
@@ -151,13 +193,31 @@ export function parseImprovementResult(result, originalData) {
151
193
  const validPriorities = ['high', 'medium', 'low'];
152
194
  const normalizedType = String(parsed.type).toLowerCase();
153
195
  const normalizedPriority = String(parsed.priority).toLowerCase();
154
- return {
196
+ // Extract title from parsed JSON if present, otherwise from content
197
+ let finalTitle;
198
+ let finalContent = String(parsed.content).trim();
199
+ if (parsed.title && typeof parsed.title === 'string' && parsed.title.trim()) {
200
+ finalTitle = parsed.title.trim();
201
+ }
202
+ else {
203
+ // Try to extract title from content
204
+ const { title, cleanedContent } = extractTitleFromContent(finalContent);
205
+ if (title) {
206
+ finalTitle = title;
207
+ finalContent = cleanedContent;
208
+ }
209
+ }
210
+ const result = {
155
211
  type: validTypes.includes(normalizedType) ? normalizedType : originalData.type,
156
212
  priority: validPriorities.includes(normalizedPriority)
157
213
  ? normalizedPriority
158
214
  : originalData.priority,
159
- content: String(parsed.content).trim(),
215
+ content: finalContent,
160
216
  };
217
+ if (finalTitle) {
218
+ result.title = finalTitle;
219
+ }
220
+ return result;
161
221
  }
162
222
  }
163
223
  catch (secondError) {
@@ -171,11 +231,18 @@ export function parseImprovementResult(result, originalData) {
171
231
  const errMsg = error instanceof Error ? error.message : String(error);
172
232
  log.error('Failed to parse improvement result', { error: errMsg, result }, 'result-parsers');
173
233
  }
174
- return {
234
+ // Fallback: try to extract title from the raw result content
235
+ const fallbackContent = result.trim() || originalData.content;
236
+ const { title: extractedTitle, cleanedContent } = extractTitleFromContent(fallbackContent);
237
+ const fallbackResult = {
175
238
  type: originalData.type,
176
239
  priority: originalData.priority,
177
- content: result.trim() || originalData.content,
240
+ content: cleanedContent,
178
241
  };
242
+ if (extractedTitle) {
243
+ fallbackResult.title = extractedTitle;
244
+ }
245
+ return fallbackResult;
179
246
  }
180
247
  export function parseReviewResult(result) {
181
248
  try {