vibeman 0.0.3 → 0.0.5

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 (184) hide show
  1. package/dist/index.js +3 -3
  2. package/dist/runtime/api/.tsbuildinfo +1 -1
  3. package/dist/runtime/api/agent/agent-service.d.ts +4 -0
  4. package/dist/runtime/api/agent/agent-service.js +62 -3
  5. package/dist/runtime/api/agent/ai-providers/amp-cli-provider.d.ts +38 -0
  6. package/dist/runtime/api/agent/ai-providers/amp-cli-provider.js +268 -0
  7. package/dist/runtime/api/agent/ai-providers/codex-cli-provider.js +40 -12
  8. package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.d.ts +24 -0
  9. package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.js +291 -0
  10. package/dist/runtime/api/agent/ai-providers/index.d.ts +3 -3
  11. package/dist/runtime/api/agent/ai-providers/index.js +3 -1
  12. package/dist/runtime/api/agent/ai-providers/types.d.ts +5 -2
  13. package/dist/runtime/api/agent/amp-cli-provider.test.d.ts +1 -0
  14. package/dist/runtime/api/agent/amp-cli-provider.test.js +99 -0
  15. package/dist/runtime/api/agent/codex-cli-provider.test.js +10 -8
  16. package/dist/runtime/api/agent/prompt-service.js +108 -105
  17. package/dist/runtime/api/agent/prompt-service.test.js +35 -0
  18. package/dist/runtime/api/agent/routing-policy.d.ts +2 -2
  19. package/dist/runtime/api/agent/routing-policy.test.js +4 -4
  20. package/dist/runtime/api/api/routers/ai.d.ts +3 -3
  21. package/dist/runtime/api/api/routers/executions.d.ts +2 -7
  22. package/dist/runtime/api/api/routers/executions.js +2 -2
  23. package/dist/runtime/api/api/routers/provider-config.d.ts +34 -0
  24. package/dist/runtime/api/api/routers/settings.d.ts +19 -0
  25. package/dist/runtime/api/api/routers/settings.js +16 -0
  26. package/dist/runtime/api/api/routers/tasks.d.ts +9 -9
  27. package/dist/runtime/api/api/routers/workflows.d.ts +12 -12
  28. package/dist/runtime/api/api/routers/worktrees.d.ts +2 -2
  29. package/dist/runtime/api/api/trpc.d.ts +16 -16
  30. package/dist/runtime/api/lib/local-config.d.ts +94 -4
  31. package/dist/runtime/api/lib/local-config.js +16 -0
  32. package/dist/runtime/api/lib/provider-detection.d.ts +2 -0
  33. package/dist/runtime/api/lib/provider-detection.js +83 -1
  34. package/dist/runtime/api/lib/server/vibeman-info.d.ts +5 -0
  35. package/dist/runtime/api/lib/server/vibeman-info.js +85 -0
  36. package/dist/runtime/api/lib/trpc/server.d.ts +63 -33
  37. package/dist/runtime/api/persistence/execution-log-persistence.d.ts +1 -1
  38. package/dist/runtime/api/persistence/execution-log-persistence.js +19 -3
  39. package/dist/runtime/api/router.d.ts +63 -33
  40. package/dist/runtime/api/settings-service.js +31 -14
  41. package/dist/runtime/api/tasks/task-file-parser.d.ts +1 -0
  42. package/dist/runtime/api/tasks/task-file-parser.js +20 -1
  43. package/dist/runtime/api/tasks/task-updater.d.ts +62 -0
  44. package/dist/runtime/api/tasks/task-updater.js +260 -0
  45. package/dist/runtime/api/tasks/task-updater.test.d.ts +1 -0
  46. package/dist/runtime/api/tasks/task-updater.test.js +303 -0
  47. package/dist/runtime/api/types/index.d.ts +1 -1
  48. package/dist/runtime/api/types/settings.d.ts +17 -6
  49. package/dist/runtime/api/vcs/git-service.d.ts +9 -0
  50. package/dist/runtime/api/vcs/git-service.js +23 -0
  51. package/dist/runtime/api/vcs/worktree-service.d.ts +1 -1
  52. package/dist/runtime/api/vcs/worktree-service.js +22 -10
  53. package/dist/runtime/api/workflows/quality-pipeline.js +2 -1
  54. package/dist/runtime/api/workflows/vibing-orchestrator.d.ts +93 -5
  55. package/dist/runtime/api/workflows/vibing-orchestrator.js +774 -203
  56. package/dist/runtime/api/workflows/workflow-effects.d.ts +45 -0
  57. package/dist/runtime/api/workflows/workflow-effects.js +49 -0
  58. package/dist/runtime/api/workflows/workflow-reconciler.d.ts +65 -0
  59. package/dist/runtime/api/workflows/workflow-reconciler.js +226 -0
  60. package/dist/runtime/api/workflows/workflow-reducer.d.ts +26 -0
  61. package/dist/runtime/api/workflows/workflow-reducer.js +288 -0
  62. package/dist/runtime/api/workflows/workflow-reducer.test.d.ts +1 -0
  63. package/dist/runtime/api/workflows/workflow-reducer.test.js +247 -0
  64. package/dist/runtime/api/workflows/workflow-schema.d.ts +546 -0
  65. package/dist/runtime/api/workflows/workflow-schema.js +256 -0
  66. package/dist/runtime/web/.next/BUILD_ID +1 -1
  67. package/dist/runtime/web/.next/app-build-manifest.json +50 -50
  68. package/dist/runtime/web/.next/app-path-routes-manifest.json +1 -1
  69. package/dist/runtime/web/.next/build-manifest.json +14 -14
  70. package/dist/runtime/web/.next/prerender-manifest.json +3 -3
  71. package/dist/runtime/web/.next/react-loadable-manifest.json +2 -33
  72. package/dist/runtime/web/.next/required-server-files.json +5 -5
  73. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js +1 -1
  74. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +1 -1
  75. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +1 -1
  76. package/dist/runtime/web/.next/server/app/_not-found/page.js +2 -2
  77. package/dist/runtime/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  78. package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  79. package/dist/runtime/web/.next/server/app/_not-found.html +2 -2
  80. package/dist/runtime/web/.next/server/app/_not-found.rsc +12 -12
  81. package/dist/runtime/web/.next/server/app/api/health/route.js +1 -1
  82. package/dist/runtime/web/.next/server/app/api/health/route.js.nft.json +1 -1
  83. package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  84. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js +1 -1
  85. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js.nft.json +1 -1
  86. package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +1 -1
  87. package/dist/runtime/web/.next/server/app/api/upload/route.js +1 -1
  88. package/dist/runtime/web/.next/server/app/api/upload/route.js.nft.json +1 -1
  89. package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +1 -1
  90. package/dist/runtime/web/.next/server/app/index.html +2 -2
  91. package/dist/runtime/web/.next/server/app/index.rsc +15 -15
  92. package/dist/runtime/web/.next/server/app/page.js +27 -62
  93. package/dist/runtime/web/.next/server/app/page.js.nft.json +1 -1
  94. package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/dist/runtime/web/.next/server/app-paths-manifest.json +1 -1
  96. package/dist/runtime/web/.next/server/chunks/210.js +1 -0
  97. package/dist/runtime/web/.next/server/chunks/291.js +18 -0
  98. package/dist/runtime/web/.next/server/chunks/552.js +22 -0
  99. package/dist/runtime/web/.next/server/chunks/780.js +1 -0
  100. package/dist/runtime/web/.next/server/chunks/905.js +6 -0
  101. package/dist/runtime/web/.next/server/chunks/98.js +1 -0
  102. package/dist/runtime/web/.next/server/middleware-build-manifest.js +1 -1
  103. package/dist/runtime/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  104. package/dist/runtime/web/.next/server/pages/404.html +2 -2
  105. package/dist/runtime/web/.next/server/pages/500.html +1 -1
  106. package/dist/runtime/web/.next/server/pages/_app.js +1 -1
  107. package/dist/runtime/web/.next/server/pages/_app.js.nft.json +1 -1
  108. package/dist/runtime/web/.next/server/pages/_document.js +1 -1
  109. package/dist/runtime/web/.next/server/pages/_document.js.nft.json +1 -1
  110. package/dist/runtime/web/.next/server/pages/_error.js +9 -9
  111. package/dist/runtime/web/.next/server/pages/_error.js.nft.json +1 -1
  112. package/dist/runtime/web/.next/server/pages-manifest.json +1 -1
  113. package/dist/runtime/web/.next/server/server-reference-manifest.json +1 -1
  114. package/dist/runtime/web/.next/server/webpack-runtime.js +1 -1
  115. package/dist/runtime/web/.next/static/{5_15u1WQCxN1_eHZpldCv → LJFZk_8tvKFN_Ee4HqUuM}/_buildManifest.js +1 -1
  116. package/dist/runtime/web/.next/static/chunks/05c91ade-7d09b2b280adffd1.js +1 -0
  117. package/dist/runtime/web/.next/static/chunks/201-51bef3fa8c832e2e.js +1 -0
  118. package/dist/runtime/web/.next/static/chunks/524-89747ed9b0294f8a.js +1 -0
  119. package/dist/runtime/web/.next/static/chunks/554-8bec6e9cca6acc67.js +1 -0
  120. package/dist/runtime/web/.next/static/chunks/764.86e9503a69d45a85.js +1 -0
  121. package/dist/runtime/web/.next/static/chunks/{87c73c54-09e1ba5c70e60a51.js → 7ab4dc20-239138e0ae7af24a.js} +1 -1
  122. package/dist/runtime/web/.next/static/chunks/905-342391e3d3a3678f.js +20 -0
  123. package/dist/runtime/web/.next/static/chunks/a8a5ce16-4edea7df2d9b544a.js +79 -0
  124. package/dist/runtime/web/.next/static/chunks/{8bb4d8db-3e2aa02b0a2384b9.js → ad74d572-4c1b162e2c15acaa.js} +1 -1
  125. package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-7b752a8641f96c1f.js +1 -0
  126. package/dist/runtime/web/.next/static/chunks/app/_not-found/page-34e66b251c2b5044.js +1 -0
  127. package/dist/runtime/web/.next/static/chunks/app/api/health/route-7b752a8641f96c1f.js +1 -0
  128. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-7b752a8641f96c1f.js +1 -0
  129. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-7b752a8641f96c1f.js +1 -0
  130. package/dist/runtime/web/.next/static/chunks/app/layout-df9ac93cb02b2385.js +1 -0
  131. package/dist/runtime/web/.next/static/chunks/app/page-6610743f7de5f92a.js +1 -0
  132. package/dist/runtime/web/.next/static/chunks/c25e0690-e9b798b8de667da1.js +1 -0
  133. package/dist/runtime/web/.next/static/chunks/framework-57157ec4d37f64aa.js +1 -0
  134. package/dist/runtime/web/.next/static/chunks/main-app-156cc0c60371bd78.js +1 -0
  135. package/dist/runtime/web/.next/static/chunks/main-df25d367c47b1fec.js +1 -0
  136. package/dist/runtime/web/.next/static/chunks/pages/_app-9f629a5e1131d19f.js +1 -0
  137. package/dist/runtime/web/.next/static/chunks/pages/_error-9238238274c7efcd.js +1 -0
  138. package/dist/runtime/web/.next/static/chunks/webpack-cd50e39b423d1808.js +1 -0
  139. package/dist/runtime/web/.next/static/css/4fbf378a264bd4ea.css +1 -0
  140. package/dist/runtime/web/package.json +8 -8
  141. package/dist/runtime/web/server.js +1 -1
  142. package/dist/tsconfig.tsbuildinfo +1 -1
  143. package/package.json +3 -37
  144. package/dist/runtime/api/lib/trpc/client.d.ts +0 -1
  145. package/dist/runtime/api/lib/trpc/client.js +0 -5
  146. package/dist/runtime/web/.next/server/chunks/217.js +0 -1
  147. package/dist/runtime/web/.next/server/chunks/383.js +0 -6
  148. package/dist/runtime/web/.next/server/chunks/458.js +0 -1
  149. package/dist/runtime/web/.next/server/chunks/576.js +0 -18
  150. package/dist/runtime/web/.next/server/chunks/635.js +0 -22
  151. package/dist/runtime/web/.next/server/chunks/761.js +0 -1
  152. package/dist/runtime/web/.next/server/chunks/777.js +0 -3
  153. package/dist/runtime/web/.next/server/chunks/825.js +0 -1
  154. package/dist/runtime/web/.next/server/chunks/838.js +0 -1
  155. package/dist/runtime/web/.next/server/chunks/973.js +0 -15
  156. package/dist/runtime/web/.next/static/chunks/18-15c10d3288afef2e.js +0 -1
  157. package/dist/runtime/web/.next/static/chunks/1c0ca389.537bbe362e3ffbd9.js +0 -3
  158. package/dist/runtime/web/.next/static/chunks/22747d63-ad5da0c19f4cfe41.js +0 -71
  159. package/dist/runtime/web/.next/static/chunks/355.056c2645878a799a.js +0 -1
  160. package/dist/runtime/web/.next/static/chunks/420.a5ccf151c9e2b2f1.js +0 -1
  161. package/dist/runtime/web/.next/static/chunks/439.1be0c6242fd248d5.js +0 -15
  162. package/dist/runtime/web/.next/static/chunks/440.c52e7c0f797e22b2.js +0 -1
  163. package/dist/runtime/web/.next/static/chunks/575-e2478287c27da87b.js +0 -1
  164. package/dist/runtime/web/.next/static/chunks/691.920d88c115087314.js +0 -1
  165. package/dist/runtime/web/.next/static/chunks/765-e838910065b50c3d.js +0 -1
  166. package/dist/runtime/web/.next/static/chunks/823-6f371a6e829adbba.js +0 -63
  167. package/dist/runtime/web/.next/static/chunks/891cff7f.0f71fc028f87e683.js +0 -1
  168. package/dist/runtime/web/.next/static/chunks/9af238c7-271a911d4e99ab18.js +0 -1
  169. package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-751c9265a65409e5.js +0 -1
  170. package/dist/runtime/web/.next/static/chunks/app/_not-found/page-1cb74d1cba27d0ab.js +0 -1
  171. package/dist/runtime/web/.next/static/chunks/app/api/health/route-751c9265a65409e5.js +0 -1
  172. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-751c9265a65409e5.js +0 -1
  173. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-751c9265a65409e5.js +0 -1
  174. package/dist/runtime/web/.next/static/chunks/app/layout-8435322f09fd0975.js +0 -1
  175. package/dist/runtime/web/.next/static/chunks/app/page-9fe7d75095b4ccec.js +0 -1
  176. package/dist/runtime/web/.next/static/chunks/cac567b0-5b77dd12911823cd.js +0 -1
  177. package/dist/runtime/web/.next/static/chunks/framework-2518f1345b5b2806.js +0 -1
  178. package/dist/runtime/web/.next/static/chunks/main-17665e5e39de9a8a.js +0 -1
  179. package/dist/runtime/web/.next/static/chunks/main-app-c0b0f5ba4f7f9d75.js +0 -1
  180. package/dist/runtime/web/.next/static/chunks/pages/_app-d6f6b3bbc3d81ee1.js +0 -1
  181. package/dist/runtime/web/.next/static/chunks/pages/_error-75a96cf1997cc3b9.js +0 -1
  182. package/dist/runtime/web/.next/static/chunks/webpack-c8de37305b4635cf.js +0 -1
  183. package/dist/runtime/web/.next/static/css/08c950681f1a9a92.css +0 -1
  184. /package/dist/runtime/web/.next/static/{5_15u1WQCxN1_eHZpldCv → LJFZk_8tvKFN_Ee4HqUuM}/_ssgManifest.js +0 -0
@@ -0,0 +1,45 @@
1
+ import type { WorkflowState } from './workflow-schema.js';
2
+ export type WorkflowEffect = {
3
+ type: 'EXECUTE_IMPLEMENTATION';
4
+ feedback?: string;
5
+ } | {
6
+ type: 'EXECUTE_VALIDATION';
7
+ } | {
8
+ type: 'EXECUTE_AI_REVIEW';
9
+ } | {
10
+ type: 'EXECUTE_MERGE';
11
+ } | {
12
+ type: 'EXECUTE_CLEANUP';
13
+ } | {
14
+ type: 'STOP_EXECUTION';
15
+ } | {
16
+ type: 'SAVE_WORKFLOW';
17
+ } | {
18
+ type: 'UPDATE_TASK';
19
+ } | {
20
+ type: 'EMIT_EVENT';
21
+ name: string;
22
+ payload: Record<string, unknown>;
23
+ };
24
+ export interface EffectContext {
25
+ workflowId: string;
26
+ taskId: string;
27
+ state: WorkflowState;
28
+ executeImplementation: (feedback?: string) => Promise<void>;
29
+ executeValidation: () => Promise<void>;
30
+ executeAiReview: () => Promise<void>;
31
+ executeMerge: () => Promise<void>;
32
+ executeCleanup: () => Promise<void>;
33
+ stopExecution: () => Promise<void>;
34
+ saveWorkflow: (state: WorkflowState) => Promise<void>;
35
+ updateTask: () => Promise<void>;
36
+ emit: (name: string, payload: Record<string, unknown>) => void;
37
+ }
38
+ export declare function executeEffects(effects: WorkflowEffect[], context: EffectContext): Promise<void>;
39
+ export interface EffectLogEntry {
40
+ timestamp: string;
41
+ effect: WorkflowEffect;
42
+ workflowId: string;
43
+ phase: string;
44
+ }
45
+ export declare function createEffectLog(effects: WorkflowEffect[], state: WorkflowState): EffectLogEntry[];
@@ -0,0 +1,49 @@
1
+ // ============================================================================
2
+ // EFFECT EXECUTOR
3
+ // Executes a list of effects using the provided context
4
+ // ============================================================================
5
+ export async function executeEffects(effects, context) {
6
+ for (const effect of effects) {
7
+ await executeEffect(effect, context);
8
+ }
9
+ }
10
+ async function executeEffect(effect, context) {
11
+ switch (effect.type) {
12
+ case 'EXECUTE_IMPLEMENTATION':
13
+ await context.executeImplementation(effect.feedback);
14
+ break;
15
+ case 'EXECUTE_VALIDATION':
16
+ await context.executeValidation();
17
+ break;
18
+ case 'EXECUTE_AI_REVIEW':
19
+ await context.executeAiReview();
20
+ break;
21
+ case 'EXECUTE_MERGE':
22
+ await context.executeMerge();
23
+ break;
24
+ case 'EXECUTE_CLEANUP':
25
+ await context.executeCleanup();
26
+ break;
27
+ case 'STOP_EXECUTION':
28
+ await context.stopExecution();
29
+ break;
30
+ case 'SAVE_WORKFLOW':
31
+ await context.saveWorkflow(context.state);
32
+ break;
33
+ case 'UPDATE_TASK':
34
+ await context.updateTask();
35
+ break;
36
+ case 'EMIT_EVENT':
37
+ context.emit(effect.name, effect.payload);
38
+ break;
39
+ }
40
+ }
41
+ export function createEffectLog(effects, state) {
42
+ const now = new Date().toISOString();
43
+ return effects.map((effect) => ({
44
+ timestamp: now,
45
+ effect,
46
+ workflowId: state.id,
47
+ phase: state.phase,
48
+ }));
49
+ }
@@ -0,0 +1,65 @@
1
+ import { EventEmitter } from 'events';
2
+ export interface ReconcilerConfig {
3
+ checkIntervalMs: number;
4
+ stuckThresholdMs: number;
5
+ maxReconcileAttempts: number;
6
+ }
7
+ export interface WorkflowSnapshot {
8
+ id: string;
9
+ taskId: string;
10
+ phase: string;
11
+ lastUpdatedAt: string;
12
+ terminalStatus: string | null;
13
+ lastHeartbeat?: string;
14
+ reconcileAttempts?: number;
15
+ }
16
+ export interface ReconcileAction {
17
+ workflowId: string;
18
+ action: 'timeout' | 'retry' | 'fail' | 'skip';
19
+ reason: string;
20
+ timestamp: string;
21
+ }
22
+ export declare class WorkflowReconciler extends EventEmitter {
23
+ private config;
24
+ private intervalId;
25
+ private running;
26
+ private reconcileHistory;
27
+ private getWorkflowsFn;
28
+ private onStuckWorkflow;
29
+ constructor(config: Partial<ReconcilerConfig>, getWorkflows: () => WorkflowSnapshot[], onStuckWorkflow: (workflowId: string, action: ReconcileAction) => Promise<void>);
30
+ start(): void;
31
+ stop(): void;
32
+ isRunning(): boolean;
33
+ getReconcileHistory(): ReconcileAction[];
34
+ reconcile(): Promise<ReconcileAction[]>;
35
+ private checkWorkflow;
36
+ }
37
+ export declare class HeartbeatTracker {
38
+ private heartbeats;
39
+ private staleThresholdMs;
40
+ constructor(staleThresholdMs?: number);
41
+ recordHeartbeat(workflowId: string, phase: string): void;
42
+ getLastHeartbeat(workflowId: string): string | undefined;
43
+ isStale(workflowId: string): boolean;
44
+ remove(workflowId: string): void;
45
+ getStaleWorkflows(): string[];
46
+ clear(): void;
47
+ }
48
+ export declare class IdempotencyGuard {
49
+ private processedEvents;
50
+ private ttlMs;
51
+ private cleanupIntervalId;
52
+ constructor(ttlMs?: number);
53
+ start(): void;
54
+ stop(): void;
55
+ generateEventKey(workflowId: string, eventType: string, payload?: Record<string, unknown>): string;
56
+ isProcessed(eventKey: string): boolean;
57
+ markProcessed(eventKey: string, result: 'success' | 'failure'): void;
58
+ getProcessedResult(eventKey: string): 'success' | 'failure' | null;
59
+ withIdempotency<T>(eventKey: string, operation: () => Promise<T>): Promise<{
60
+ result: T;
61
+ skipped: boolean;
62
+ }>;
63
+ private cleanup;
64
+ clear(): void;
65
+ }
@@ -0,0 +1,226 @@
1
+ import { EventEmitter } from 'events';
2
+ import { log } from '../lib/logger.js';
3
+ const DEFAULT_CONFIG = {
4
+ checkIntervalMs: 60 * 1000,
5
+ stuckThresholdMs: 30 * 60 * 1000,
6
+ maxReconcileAttempts: 3,
7
+ };
8
+ export class WorkflowReconciler extends EventEmitter {
9
+ constructor(config, getWorkflows, onStuckWorkflow) {
10
+ super();
11
+ this.intervalId = null;
12
+ this.running = false;
13
+ this.reconcileHistory = [];
14
+ this.config = { ...DEFAULT_CONFIG, ...config };
15
+ this.getWorkflowsFn = getWorkflows;
16
+ this.onStuckWorkflow = onStuckWorkflow;
17
+ }
18
+ start() {
19
+ if (this.running)
20
+ return;
21
+ this.running = true;
22
+ log.info('Workflow reconciler started', {
23
+ checkIntervalMs: this.config.checkIntervalMs,
24
+ stuckThresholdMs: this.config.stuckThresholdMs,
25
+ }, 'workflow-reconciler');
26
+ this.intervalId = setInterval(() => {
27
+ this.reconcile().catch((err) => {
28
+ log.error('Reconciler tick failed', err, 'workflow-reconciler');
29
+ });
30
+ }, this.config.checkIntervalMs);
31
+ this.reconcile().catch((err) => {
32
+ log.error('Initial reconcile failed', err, 'workflow-reconciler');
33
+ });
34
+ }
35
+ stop() {
36
+ if (!this.running)
37
+ return;
38
+ this.running = false;
39
+ if (this.intervalId) {
40
+ clearInterval(this.intervalId);
41
+ this.intervalId = null;
42
+ }
43
+ log.info('Workflow reconciler stopped', {}, 'workflow-reconciler');
44
+ }
45
+ isRunning() {
46
+ return this.running;
47
+ }
48
+ getReconcileHistory() {
49
+ return [...this.reconcileHistory];
50
+ }
51
+ async reconcile() {
52
+ const workflows = this.getWorkflowsFn();
53
+ const now = Date.now();
54
+ const actions = [];
55
+ for (const wf of workflows) {
56
+ const action = this.checkWorkflow(wf, now);
57
+ if (action) {
58
+ actions.push(action);
59
+ this.reconcileHistory.push(action);
60
+ if (this.reconcileHistory.length > 100) {
61
+ this.reconcileHistory = this.reconcileHistory.slice(-100);
62
+ }
63
+ try {
64
+ await this.onStuckWorkflow(wf.id, action);
65
+ this.emit('workflowReconciled', action);
66
+ }
67
+ catch (err) {
68
+ log.error('Failed to handle stuck workflow', { workflowId: wf.id, err }, 'workflow-reconciler');
69
+ }
70
+ }
71
+ }
72
+ if (actions.length > 0) {
73
+ log.info('Reconciler found stuck workflows', { count: actions.length }, 'workflow-reconciler');
74
+ }
75
+ return actions;
76
+ }
77
+ checkWorkflow(wf, now) {
78
+ if (wf.terminalStatus === 'paused' || wf.terminalStatus === 'failed') {
79
+ return null;
80
+ }
81
+ if (wf.phase === 'done') {
82
+ return null;
83
+ }
84
+ const activePhases = ['implementing', 'validating', 'merging'];
85
+ if (!activePhases.includes(wf.phase)) {
86
+ return null;
87
+ }
88
+ const lastUpdate = new Date(wf.lastHeartbeat || wf.lastUpdatedAt).getTime();
89
+ const elapsed = now - lastUpdate;
90
+ if (elapsed < this.config.stuckThresholdMs) {
91
+ return null;
92
+ }
93
+ const attempts = wf.reconcileAttempts || 0;
94
+ const timestamp = new Date().toISOString();
95
+ if (attempts >= this.config.maxReconcileAttempts) {
96
+ return {
97
+ workflowId: wf.id,
98
+ action: 'fail',
99
+ reason: `Exceeded max reconcile attempts (${this.config.maxReconcileAttempts}) after ${Math.round(elapsed / 60000)}min stuck`,
100
+ timestamp,
101
+ };
102
+ }
103
+ return {
104
+ workflowId: wf.id,
105
+ action: 'timeout',
106
+ reason: `No progress for ${Math.round(elapsed / 60000)}min in phase ${wf.phase}`,
107
+ timestamp,
108
+ };
109
+ }
110
+ }
111
+ // ============================================================================
112
+ // HEARTBEAT TRACKER
113
+ // Tracks heartbeats for active workflow executions
114
+ // ============================================================================
115
+ export class HeartbeatTracker {
116
+ constructor(staleThresholdMs = 5 * 60 * 1000) {
117
+ this.heartbeats = new Map();
118
+ this.staleThresholdMs = staleThresholdMs;
119
+ }
120
+ recordHeartbeat(workflowId, phase) {
121
+ this.heartbeats.set(workflowId, {
122
+ lastBeat: new Date().toISOString(),
123
+ phase,
124
+ });
125
+ }
126
+ getLastHeartbeat(workflowId) {
127
+ return this.heartbeats.get(workflowId)?.lastBeat;
128
+ }
129
+ isStale(workflowId) {
130
+ const entry = this.heartbeats.get(workflowId);
131
+ if (!entry)
132
+ return true;
133
+ const elapsed = Date.now() - new Date(entry.lastBeat).getTime();
134
+ return elapsed > this.staleThresholdMs;
135
+ }
136
+ remove(workflowId) {
137
+ this.heartbeats.delete(workflowId);
138
+ }
139
+ getStaleWorkflows() {
140
+ const stale = [];
141
+ const now = Date.now();
142
+ for (const [id, entry] of this.heartbeats) {
143
+ const elapsed = now - new Date(entry.lastBeat).getTime();
144
+ if (elapsed > this.staleThresholdMs) {
145
+ stale.push(id);
146
+ }
147
+ }
148
+ return stale;
149
+ }
150
+ clear() {
151
+ this.heartbeats.clear();
152
+ }
153
+ }
154
+ // ============================================================================
155
+ // IDEMPOTENCY GUARD
156
+ // Ensures workflow events are processed only once
157
+ // ============================================================================
158
+ export class IdempotencyGuard {
159
+ constructor(ttlMs = 60 * 60 * 1000) {
160
+ this.processedEvents = new Map();
161
+ this.cleanupIntervalId = null;
162
+ this.ttlMs = ttlMs;
163
+ }
164
+ start() {
165
+ this.cleanupIntervalId = setInterval(() => {
166
+ this.cleanup();
167
+ }, this.ttlMs / 2);
168
+ }
169
+ stop() {
170
+ if (this.cleanupIntervalId) {
171
+ clearInterval(this.cleanupIntervalId);
172
+ this.cleanupIntervalId = null;
173
+ }
174
+ }
175
+ generateEventKey(workflowId, eventType, payload) {
176
+ const payloadHash = payload ? JSON.stringify(payload).slice(0, 100) : '';
177
+ return `${workflowId}:${eventType}:${payloadHash}`;
178
+ }
179
+ isProcessed(eventKey) {
180
+ return this.processedEvents.has(eventKey);
181
+ }
182
+ markProcessed(eventKey, result) {
183
+ this.processedEvents.set(eventKey, {
184
+ timestamp: new Date().toISOString(),
185
+ result,
186
+ });
187
+ }
188
+ getProcessedResult(eventKey) {
189
+ return this.processedEvents.get(eventKey)?.result ?? null;
190
+ }
191
+ async withIdempotency(eventKey, operation) {
192
+ if (this.isProcessed(eventKey)) {
193
+ const prevResult = this.getProcessedResult(eventKey);
194
+ log.debug('Skipping duplicate event', { eventKey, previousResult: prevResult }, 'idempotency-guard');
195
+ return { result: undefined, skipped: true };
196
+ }
197
+ try {
198
+ const result = await operation();
199
+ this.markProcessed(eventKey, 'success');
200
+ return { result, skipped: false };
201
+ }
202
+ catch (err) {
203
+ this.markProcessed(eventKey, 'failure');
204
+ throw err;
205
+ }
206
+ }
207
+ cleanup() {
208
+ const now = Date.now();
209
+ const toRemove = [];
210
+ for (const [key, entry] of this.processedEvents) {
211
+ const elapsed = now - new Date(entry.timestamp).getTime();
212
+ if (elapsed > this.ttlMs) {
213
+ toRemove.push(key);
214
+ }
215
+ }
216
+ for (const key of toRemove) {
217
+ this.processedEvents.delete(key);
218
+ }
219
+ if (toRemove.length > 0) {
220
+ log.debug('Cleaned up expired idempotency keys', { count: toRemove.length }, 'idempotency-guard');
221
+ }
222
+ }
223
+ clear() {
224
+ this.processedEvents.clear();
225
+ }
226
+ }
@@ -0,0 +1,26 @@
1
+ import type { WorkflowState, WorkflowEvent, WorkflowConfig } from './workflow-schema.js';
2
+ import type { WorkflowEffect } from './workflow-effects.js';
3
+ export interface ReducerResult {
4
+ state: WorkflowState;
5
+ effects: WorkflowEffect[];
6
+ }
7
+ export declare function reduce(state: WorkflowState, event: WorkflowEvent): ReducerResult;
8
+ export type NextAction = {
9
+ action: 'implementing';
10
+ feedback?: string;
11
+ } | {
12
+ action: 'validating';
13
+ } | {
14
+ action: 'ai-reviewing';
15
+ } | {
16
+ action: 'awaiting-approval';
17
+ } | {
18
+ action: 'merging';
19
+ } | {
20
+ action: 'cleanup';
21
+ } | {
22
+ action: 'none';
23
+ reason: string;
24
+ };
25
+ export declare function computeNextAction(state: WorkflowState): NextAction;
26
+ export declare function createInitialState(workflowId: string, taskId: string, config: WorkflowConfig): WorkflowState;
@@ -0,0 +1,288 @@
1
+ import { getNextPhase } from './workflow-schema.js';
2
+ export function reduce(state, event) {
3
+ const now = new Date().toISOString();
4
+ const nextPhase = getNextPhase(state, event);
5
+ // Invalid transition - no change
6
+ if (nextPhase === null) {
7
+ return { state, effects: [] };
8
+ }
9
+ // Compute new state based on event type
10
+ let newState = {
11
+ ...state,
12
+ lastUpdatedAt: now,
13
+ };
14
+ const effects = [];
15
+ // Handle terminal statuses
16
+ if (nextPhase === 'paused') {
17
+ newState = {
18
+ ...newState,
19
+ terminalStatus: 'paused',
20
+ error: 'Paused by user',
21
+ };
22
+ effects.push({ type: 'STOP_EXECUTION' });
23
+ effects.push({ type: 'EMIT_EVENT', name: 'workflowPaused', payload: { workflowId: state.id } });
24
+ effects.push({ type: 'SAVE_WORKFLOW' });
25
+ return { state: newState, effects };
26
+ }
27
+ if (nextPhase === 'failed') {
28
+ const errorMsg = 'error' in event ? event.error : 'Unknown error';
29
+ newState = {
30
+ ...newState,
31
+ terminalStatus: 'failed',
32
+ error: errorMsg,
33
+ failureContext: {
34
+ atPhase: state.phase,
35
+ error: errorMsg,
36
+ timestamp: now,
37
+ },
38
+ };
39
+ effects.push({
40
+ type: 'EMIT_EVENT',
41
+ name: 'workflowFailed',
42
+ payload: { workflowId: state.id, error: errorMsg },
43
+ });
44
+ effects.push({ type: 'SAVE_WORKFLOW' });
45
+ return { state: newState, effects };
46
+ }
47
+ // Clear terminal status if resuming
48
+ if (event.type === 'RESUME' || event.type === 'RETRY') {
49
+ newState = {
50
+ ...newState,
51
+ terminalStatus: null,
52
+ error: undefined,
53
+ };
54
+ }
55
+ // Phase transition
56
+ if (nextPhase !== state.phase) {
57
+ newState = {
58
+ ...newState,
59
+ phase: nextPhase,
60
+ };
61
+ effects.push({
62
+ type: 'EMIT_EVENT',
63
+ name: 'workflowPhaseChanged',
64
+ payload: { workflowId: state.id, oldPhase: state.phase, newPhase: nextPhase },
65
+ });
66
+ }
67
+ // Event-specific state updates and effects
68
+ switch (event.type) {
69
+ case 'START':
70
+ effects.push({ type: 'EXECUTE_IMPLEMENTATION' });
71
+ break;
72
+ case 'IMPL_SUCCESS':
73
+ if (event.executionId) {
74
+ newState = updateExecutionIds(newState, 'implementing', event.executionId);
75
+ }
76
+ if (!state.config.stepByStepMode) {
77
+ effects.push({ type: 'EXECUTE_VALIDATION' });
78
+ }
79
+ break;
80
+ case 'IMPL_FAIL':
81
+ newState = incrementAttempts(newState, 'implementing');
82
+ if (nextPhase === 'implementing' && event.canRetry) {
83
+ newState = {
84
+ ...newState,
85
+ rerunContext: {
86
+ reason: event.error,
87
+ previousExecutionId: getLastExecutionId(state, 'implementing'),
88
+ },
89
+ };
90
+ if (!state.config.stepByStepMode) {
91
+ effects.push({ type: 'EXECUTE_IMPLEMENTATION', feedback: event.error });
92
+ }
93
+ }
94
+ break;
95
+ case 'VALID_SUCCESS':
96
+ newState = {
97
+ ...newState,
98
+ qualityResults: event.results,
99
+ };
100
+ if (nextPhase === 'approved' && state.config.aiCodeReview && !state.config.stepByStepMode) {
101
+ effects.push({ type: 'EXECUTE_AI_REVIEW' });
102
+ }
103
+ if (nextPhase === 'merging' && !state.config.stepByStepMode) {
104
+ effects.push({ type: 'EXECUTE_MERGE' });
105
+ }
106
+ break;
107
+ case 'VALID_FAIL':
108
+ if (event.results) {
109
+ newState = { ...newState, qualityResults: event.results };
110
+ }
111
+ newState = {
112
+ ...newState,
113
+ rerunContext: {
114
+ reason: `Validation failed: ${event.error}`,
115
+ previousExecutionId: getLastExecutionId(state, 'implementing'),
116
+ },
117
+ };
118
+ if (!state.config.stepByStepMode) {
119
+ effects.push({ type: 'EXECUTE_IMPLEMENTATION', feedback: event.error });
120
+ }
121
+ break;
122
+ case 'REVIEW_DONE':
123
+ newState = {
124
+ ...newState,
125
+ aiReviewResult: event.result,
126
+ };
127
+ if (event.result.executionId) {
128
+ newState = updateExecutionIds(newState, 'approved', event.result.executionId);
129
+ }
130
+ effects.push({ type: 'UPDATE_TASK' });
131
+ break;
132
+ case 'REVIEW_SKIP':
133
+ effects.push({ type: 'UPDATE_TASK' });
134
+ break;
135
+ case 'APPROVE':
136
+ if (!state.config.stepByStepMode) {
137
+ effects.push({ type: 'EXECUTE_MERGE' });
138
+ }
139
+ break;
140
+ case 'REJECT':
141
+ newState = {
142
+ ...newState,
143
+ rerunContext: {
144
+ reason: 'Rejected by reviewer',
145
+ feedback: event.feedback,
146
+ previousExecutionId: getLastExecutionId(state, 'implementing'),
147
+ },
148
+ };
149
+ if (!state.config.stepByStepMode) {
150
+ effects.push({ type: 'EXECUTE_IMPLEMENTATION', feedback: event.feedback });
151
+ }
152
+ break;
153
+ case 'MERGE_SUCCESS':
154
+ newState = {
155
+ ...newState,
156
+ links: {
157
+ ...newState.links,
158
+ prUrl: event.prUrl,
159
+ prNumber: event.prNumber,
160
+ },
161
+ endTime: now,
162
+ };
163
+ effects.push({ type: 'EXECUTE_CLEANUP' });
164
+ effects.push({
165
+ type: 'EMIT_EVENT',
166
+ name: 'workflowCompleted',
167
+ payload: { workflowId: state.id },
168
+ });
169
+ break;
170
+ case 'MERGE_FAIL':
171
+ // Stay in approved, let user retry
172
+ break;
173
+ case 'CLEAN_SUCCESS':
174
+ newState = { ...newState, endTime: now };
175
+ break;
176
+ case 'COMPLETE_MANUAL':
177
+ newState = {
178
+ ...newState,
179
+ phase: 'done',
180
+ endTime: now,
181
+ };
182
+ effects.push({
183
+ type: 'EMIT_EVENT',
184
+ name: 'workflowCompleted',
185
+ payload: { workflowId: state.id, manual: true },
186
+ });
187
+ break;
188
+ case 'RETRY':
189
+ newState = {
190
+ ...newState,
191
+ rerunContext: {
192
+ reason: 'Manual retry',
193
+ previousExecutionId: getLastExecutionId(state, 'implementing'),
194
+ },
195
+ };
196
+ effects.push({ type: 'EXECUTE_IMPLEMENTATION' });
197
+ break;
198
+ }
199
+ effects.push({ type: 'SAVE_WORKFLOW' });
200
+ return { state: newState, effects };
201
+ }
202
+ export function computeNextAction(state) {
203
+ const { phase, terminalStatus, config } = state;
204
+ // If paused, suggest resume to implementing (or last phase)
205
+ if (terminalStatus === 'paused') {
206
+ return { action: 'implementing', feedback: state.rerunContext?.feedback };
207
+ }
208
+ // If failed, suggest retry
209
+ if (terminalStatus === 'failed') {
210
+ return { action: 'implementing', feedback: state.failureContext?.error };
211
+ }
212
+ switch (phase) {
213
+ case 'draft':
214
+ return { action: 'implementing' };
215
+ case 'implementing':
216
+ // In step-by-step mode, this phase means we're waiting for user to continue
217
+ // The phase mapping already moved 'implemented' -> 'validating'
218
+ // So if we're still in 'implementing', the execution is in progress
219
+ return { action: 'none', reason: 'Implementation in progress' };
220
+ case 'validating':
221
+ // Validation phase: run the quality checks
222
+ return { action: 'validating' };
223
+ case 'approved':
224
+ // Check if AI review is needed and not done yet
225
+ if (config.aiCodeReview && !state.aiReviewResult) {
226
+ return { action: 'ai-reviewing' };
227
+ }
228
+ // If human approval required, wait for user to click approve
229
+ if (config.requireHumanApproval) {
230
+ return { action: 'awaiting-approval' };
231
+ }
232
+ // Otherwise proceed to merge
233
+ return { action: 'merging' };
234
+ case 'merging':
235
+ // Execute merge
236
+ return { action: 'merging' };
237
+ case 'done':
238
+ return { action: 'none', reason: 'Workflow completed' };
239
+ default:
240
+ return { action: 'none', reason: 'Unknown phase' };
241
+ }
242
+ }
243
+ // ============================================================================
244
+ // HELPER FUNCTIONS
245
+ // ============================================================================
246
+ function incrementAttempts(state, phase) {
247
+ const current = state.attempts[phase] ?? 0;
248
+ return {
249
+ ...state,
250
+ attempts: {
251
+ ...state.attempts,
252
+ [phase]: current + 1,
253
+ },
254
+ };
255
+ }
256
+ function updateExecutionIds(state, phase, executionId) {
257
+ const current = state.executionIds[phase] ?? [];
258
+ return {
259
+ ...state,
260
+ executionIds: {
261
+ ...state.executionIds,
262
+ [phase]: [...current, executionId],
263
+ },
264
+ };
265
+ }
266
+ function getLastExecutionId(state, phase) {
267
+ const ids = state.executionIds[phase] ?? [];
268
+ return ids[ids.length - 1];
269
+ }
270
+ // ============================================================================
271
+ // STATE FACTORY
272
+ // Creates initial workflow state from task and config
273
+ // ============================================================================
274
+ export function createInitialState(workflowId, taskId, config) {
275
+ const now = new Date().toISOString();
276
+ return {
277
+ id: workflowId,
278
+ taskId,
279
+ phase: 'draft',
280
+ terminalStatus: null,
281
+ startTime: now,
282
+ lastUpdatedAt: now,
283
+ attempts: {},
284
+ executionIds: {},
285
+ links: {},
286
+ config,
287
+ };
288
+ }