@renseiai/agentfactory-nextjs 0.8.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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/src/__tests__/middleware-edge-safety.test.d.ts +2 -0
  4. package/dist/src/__tests__/middleware-edge-safety.test.d.ts.map +1 -0
  5. package/dist/src/__tests__/middleware-edge-safety.test.js +74 -0
  6. package/dist/src/__tests__/poll-project-filter.test.d.ts +2 -0
  7. package/dist/src/__tests__/poll-project-filter.test.d.ts.map +1 -0
  8. package/dist/src/__tests__/poll-project-filter.test.js +83 -0
  9. package/dist/src/__tests__/subpath-exports.test.d.ts +2 -0
  10. package/dist/src/__tests__/subpath-exports.test.d.ts.map +1 -0
  11. package/dist/src/__tests__/subpath-exports.test.js +35 -0
  12. package/dist/src/__tests__/webhook-project-filter.test.d.ts +2 -0
  13. package/dist/src/__tests__/webhook-project-filter.test.d.ts.map +1 -0
  14. package/dist/src/__tests__/webhook-project-filter.test.js +48 -0
  15. package/dist/src/factory.d.ts +140 -0
  16. package/dist/src/factory.d.ts.map +1 -0
  17. package/dist/src/factory.js +127 -0
  18. package/dist/src/handlers/cleanup.d.ts +44 -0
  19. package/dist/src/handlers/cleanup.d.ts.map +1 -0
  20. package/dist/src/handlers/cleanup.js +34 -0
  21. package/dist/src/handlers/config.d.ts +11 -0
  22. package/dist/src/handlers/config.d.ts.map +1 -0
  23. package/dist/src/handlers/config.js +20 -0
  24. package/dist/src/handlers/issue-tracker-proxy/index.d.ts +34 -0
  25. package/dist/src/handlers/issue-tracker-proxy/index.d.ts.map +1 -0
  26. package/dist/src/handlers/issue-tracker-proxy/index.js +230 -0
  27. package/dist/src/handlers/issue-tracker-proxy/serializer.d.ts +28 -0
  28. package/dist/src/handlers/issue-tracker-proxy/serializer.d.ts.map +1 -0
  29. package/dist/src/handlers/issue-tracker-proxy/serializer.js +95 -0
  30. package/dist/src/handlers/issue-tracker-proxy/types.d.ts +9 -0
  31. package/dist/src/handlers/issue-tracker-proxy/types.d.ts.map +1 -0
  32. package/dist/src/handlers/issue-tracker-proxy/types.js +4 -0
  33. package/dist/src/handlers/oauth/callback.d.ts +36 -0
  34. package/dist/src/handlers/oauth/callback.d.ts.map +1 -0
  35. package/dist/src/handlers/oauth/callback.js +96 -0
  36. package/dist/src/handlers/public/session-detail.d.ts +31 -0
  37. package/dist/src/handlers/public/session-detail.d.ts.map +1 -0
  38. package/dist/src/handlers/public/session-detail.js +91 -0
  39. package/dist/src/handlers/public/sessions-list.d.ts +22 -0
  40. package/dist/src/handlers/public/sessions-list.d.ts.map +1 -0
  41. package/dist/src/handlers/public/sessions-list.js +75 -0
  42. package/dist/src/handlers/public/stats.d.ts +28 -0
  43. package/dist/src/handlers/public/stats.d.ts.map +1 -0
  44. package/dist/src/handlers/public/stats.js +66 -0
  45. package/dist/src/handlers/sessions/activity.d.ts +15 -0
  46. package/dist/src/handlers/sessions/activity.d.ts.map +1 -0
  47. package/dist/src/handlers/sessions/activity.js +93 -0
  48. package/dist/src/handlers/sessions/claim.d.ts +15 -0
  49. package/dist/src/handlers/sessions/claim.d.ts.map +1 -0
  50. package/dist/src/handlers/sessions/claim.js +139 -0
  51. package/dist/src/handlers/sessions/completion.d.ts +16 -0
  52. package/dist/src/handlers/sessions/completion.d.ts.map +1 -0
  53. package/dist/src/handlers/sessions/completion.js +82 -0
  54. package/dist/src/handlers/sessions/external-urls.d.ts +15 -0
  55. package/dist/src/handlers/sessions/external-urls.d.ts.map +1 -0
  56. package/dist/src/handlers/sessions/external-urls.js +70 -0
  57. package/dist/src/handlers/sessions/get.d.ts +19 -0
  58. package/dist/src/handlers/sessions/get.d.ts.map +1 -0
  59. package/dist/src/handlers/sessions/get.js +47 -0
  60. package/dist/src/handlers/sessions/list.d.ts +27 -0
  61. package/dist/src/handlers/sessions/list.d.ts.map +1 -0
  62. package/dist/src/handlers/sessions/list.js +51 -0
  63. package/dist/src/handlers/sessions/lock-refresh.d.ts +14 -0
  64. package/dist/src/handlers/sessions/lock-refresh.d.ts.map +1 -0
  65. package/dist/src/handlers/sessions/lock-refresh.js +38 -0
  66. package/dist/src/handlers/sessions/progress.d.ts +15 -0
  67. package/dist/src/handlers/sessions/progress.d.ts.map +1 -0
  68. package/dist/src/handlers/sessions/progress.js +94 -0
  69. package/dist/src/handlers/sessions/prompts.d.ts +15 -0
  70. package/dist/src/handlers/sessions/prompts.d.ts.map +1 -0
  71. package/dist/src/handlers/sessions/prompts.js +91 -0
  72. package/dist/src/handlers/sessions/status.d.ts +19 -0
  73. package/dist/src/handlers/sessions/status.d.ts.map +1 -0
  74. package/dist/src/handlers/sessions/status.js +187 -0
  75. package/dist/src/handlers/sessions/tool-error.d.ts +15 -0
  76. package/dist/src/handlers/sessions/tool-error.d.ts.map +1 -0
  77. package/dist/src/handlers/sessions/tool-error.js +103 -0
  78. package/dist/src/handlers/sessions/transfer-ownership.d.ts +14 -0
  79. package/dist/src/handlers/sessions/transfer-ownership.d.ts.map +1 -0
  80. package/dist/src/handlers/sessions/transfer-ownership.js +56 -0
  81. package/dist/src/handlers/workers/get-delete.d.ts +15 -0
  82. package/dist/src/handlers/workers/get-delete.d.ts.map +1 -0
  83. package/dist/src/handlers/workers/get-delete.js +58 -0
  84. package/dist/src/handlers/workers/heartbeat.d.ts +14 -0
  85. package/dist/src/handlers/workers/heartbeat.d.ts.map +1 -0
  86. package/dist/src/handlers/workers/heartbeat.js +42 -0
  87. package/dist/src/handlers/workers/list.d.ts +22 -0
  88. package/dist/src/handlers/workers/list.d.ts.map +1 -0
  89. package/dist/src/handlers/workers/list.js +33 -0
  90. package/dist/src/handlers/workers/poll.d.ts +14 -0
  91. package/dist/src/handlers/workers/poll.d.ts.map +1 -0
  92. package/dist/src/handlers/workers/poll.js +96 -0
  93. package/dist/src/handlers/workers/register.d.ts +9 -0
  94. package/dist/src/handlers/workers/register.d.ts.map +1 -0
  95. package/dist/src/handlers/workers/register.js +45 -0
  96. package/dist/src/index.d.ts +52 -0
  97. package/dist/src/index.d.ts.map +1 -0
  98. package/dist/src/index.js +56 -0
  99. package/dist/src/linear-client-resolver.d.ts +59 -0
  100. package/dist/src/linear-client-resolver.d.ts.map +1 -0
  101. package/dist/src/linear-client-resolver.js +104 -0
  102. package/dist/src/middleware/cron-auth.d.ts +21 -0
  103. package/dist/src/middleware/cron-auth.d.ts.map +1 -0
  104. package/dist/src/middleware/cron-auth.js +46 -0
  105. package/dist/src/middleware/factory.d.ts +33 -0
  106. package/dist/src/middleware/factory.d.ts.map +1 -0
  107. package/dist/src/middleware/factory.js +185 -0
  108. package/dist/src/middleware/index.d.ts +16 -0
  109. package/dist/src/middleware/index.d.ts.map +1 -0
  110. package/dist/src/middleware/index.js +14 -0
  111. package/dist/src/middleware/types.d.ts +35 -0
  112. package/dist/src/middleware/types.d.ts.map +1 -0
  113. package/dist/src/middleware/types.js +4 -0
  114. package/dist/src/middleware/worker-auth.d.ts +25 -0
  115. package/dist/src/middleware/worker-auth.d.ts.map +1 -0
  116. package/dist/src/middleware/worker-auth.js +43 -0
  117. package/dist/src/orchestrator/error-formatting.d.ts +8 -0
  118. package/dist/src/orchestrator/error-formatting.d.ts.map +1 -0
  119. package/dist/src/orchestrator/error-formatting.js +35 -0
  120. package/dist/src/orchestrator/index.d.ts +4 -0
  121. package/dist/src/orchestrator/index.d.ts.map +1 -0
  122. package/dist/src/orchestrator/index.js +2 -0
  123. package/dist/src/orchestrator/types.d.ts +53 -0
  124. package/dist/src/orchestrator/types.d.ts.map +1 -0
  125. package/dist/src/orchestrator/types.js +4 -0
  126. package/dist/src/orchestrator/webhook-orchestrator.d.ts +32 -0
  127. package/dist/src/orchestrator/webhook-orchestrator.d.ts.map +1 -0
  128. package/dist/src/orchestrator/webhook-orchestrator.js +373 -0
  129. package/dist/src/types.d.ts +101 -0
  130. package/dist/src/types.d.ts.map +1 -0
  131. package/dist/src/types.js +7 -0
  132. package/dist/src/webhook/governor-bridge.d.ts +23 -0
  133. package/dist/src/webhook/governor-bridge.d.ts.map +1 -0
  134. package/dist/src/webhook/governor-bridge.js +36 -0
  135. package/dist/src/webhook/handlers/issue-updated.d.ts +15 -0
  136. package/dist/src/webhook/handlers/issue-updated.d.ts.map +1 -0
  137. package/dist/src/webhook/handlers/issue-updated.js +771 -0
  138. package/dist/src/webhook/handlers/session-created.d.ts +9 -0
  139. package/dist/src/webhook/handlers/session-created.d.ts.map +1 -0
  140. package/dist/src/webhook/handlers/session-created.js +337 -0
  141. package/dist/src/webhook/handlers/session-prompted.d.ts +9 -0
  142. package/dist/src/webhook/handlers/session-prompted.d.ts.map +1 -0
  143. package/dist/src/webhook/handlers/session-prompted.js +199 -0
  144. package/dist/src/webhook/handlers/session-updated.d.ts +9 -0
  145. package/dist/src/webhook/handlers/session-updated.d.ts.map +1 -0
  146. package/dist/src/webhook/handlers/session-updated.js +29 -0
  147. package/dist/src/webhook/processor.d.ts +22 -0
  148. package/dist/src/webhook/processor.d.ts.map +1 -0
  149. package/dist/src/webhook/processor.js +98 -0
  150. package/dist/src/webhook/signature.d.ts +16 -0
  151. package/dist/src/webhook/signature.d.ts.map +1 -0
  152. package/dist/src/webhook/signature.js +23 -0
  153. package/dist/src/webhook/utils.d.ts +61 -0
  154. package/dist/src/webhook/utils.d.ts.map +1 -0
  155. package/dist/src/webhook/utils.js +166 -0
  156. package/package.json +86 -0
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Webhook Orchestrator Factory
3
+ *
4
+ * Creates a singleton orchestrator instance configured for webhook-triggered
5
+ * agent spawning. Includes retry logic, idempotency, session state persistence,
6
+ * and error activity emission to Linear.
7
+ *
8
+ * Consumers provide lifecycle hooks (e.g., onAgentComplete) for custom behavior
9
+ * like marking issues as "agent-worked" for automated QA.
10
+ */
11
+ import { createOrchestrator, } from '@renseiai/agentfactory';
12
+ import { withRetry, AgentSpawnError, isRetryableError, createAgentSession, createLinearAgentClient, } from '@renseiai/agentfactory-linear';
13
+ import { createLogger, generateIdempotencyKey, isWebhookProcessed, markWebhookProcessed, unmarkWebhookProcessed, storeSessionState, getSessionState, updateProviderSessionId, updateSessionStatus, updateSessionCostData, updateWorkflowState, recordPhaseAttempt, incrementCycleCount, appendFailureSummary, clearWorkflowState, extractFailureReason, markAcceptanceCompleted, } from '@renseiai/agentfactory-server';
14
+ import { formatErrorForComment } from './error-formatting.js';
15
+ const log = createLogger('webhook-orchestrator');
16
+ const DEFAULT_RETRY_CONFIG = {
17
+ maxRetries: 2,
18
+ initialDelayMs: 1000,
19
+ backoffMultiplier: 2,
20
+ maxDelayMs: 5000,
21
+ retryableStatusCodes: [429, 500, 502, 503, 504],
22
+ };
23
+ /**
24
+ * Resolve the Linear client lazily from environment.
25
+ * Used for error activity emission.
26
+ */
27
+ function getLinearClientFromEnv() {
28
+ const apiKey = process.env.LINEAR_ACCESS_TOKEN;
29
+ if (!apiKey)
30
+ return null;
31
+ return createLinearAgentClient({ apiKey });
32
+ }
33
+ /**
34
+ * Determine if a spawn error is retryable.
35
+ */
36
+ function isSpawnErrorRetryable(error) {
37
+ if (isRetryableError(error))
38
+ return true;
39
+ if (error instanceof Error) {
40
+ const message = error.message.toLowerCase();
41
+ if (message.includes('lock') || message.includes('busy') || message.includes('temporary')) {
42
+ return true;
43
+ }
44
+ if (message.includes('not found') || message.includes('enoent')) {
45
+ return false;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ /**
51
+ * Clean up resources after a failed spawn attempt.
52
+ */
53
+ async function cleanupFailedSpawn(issueId, idempotencyKey) {
54
+ try {
55
+ await unmarkWebhookProcessed(idempotencyKey);
56
+ log.debug('Cleaned up resources for failed spawn', { issueId, idempotencyKey });
57
+ }
58
+ catch (cleanupError) {
59
+ log.error('Cleanup error', { issueId, idempotencyKey, error: cleanupError });
60
+ }
61
+ }
62
+ /**
63
+ * Emit an error activity to Linear for tracking agent failures.
64
+ */
65
+ async function emitAgentErrorActivity(issueId, error, sessionId) {
66
+ try {
67
+ const client = getLinearClientFromEnv();
68
+ if (!client) {
69
+ log.warn('Cannot emit error activity: LINEAR_ACCESS_TOKEN not set');
70
+ return;
71
+ }
72
+ if (sessionId) {
73
+ const session = createAgentSession({
74
+ client: client.linearClient,
75
+ issueId,
76
+ sessionId,
77
+ autoTransition: false,
78
+ });
79
+ await session.emitError(error);
80
+ }
81
+ else {
82
+ const errorMessage = formatErrorForComment(error);
83
+ await client.createComment(issueId, errorMessage);
84
+ }
85
+ log.debug('Error activity emitted to Linear', { issueId, sessionId });
86
+ }
87
+ catch (emitError) {
88
+ log.error('Failed to emit error activity', { issueId, sessionId, error: emitError });
89
+ }
90
+ }
91
+ /**
92
+ * Create a webhook orchestrator instance.
93
+ *
94
+ * @param config - Orchestrator configuration
95
+ * @param hooks - Lifecycle hooks for custom behavior
96
+ * @returns A webhook orchestrator instance
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const orchestrator = createWebhookOrchestrator(
101
+ * { maxConcurrent: 10 },
102
+ * {
103
+ * onAgentComplete: async (agent) => {
104
+ * await markAgentWorked(agent.issueId, { ... })
105
+ * },
106
+ * }
107
+ * )
108
+ * ```
109
+ */
110
+ export function createWebhookOrchestrator(config, hooks) {
111
+ const retryConfig = config?.retryConfig ?? DEFAULT_RETRY_CONFIG;
112
+ let _orchestrator = null;
113
+ function getOrchestrator() {
114
+ if (!_orchestrator) {
115
+ const apiKey = process.env.LINEAR_ACCESS_TOKEN;
116
+ if (!apiKey) {
117
+ throw new Error('LINEAR_ACCESS_TOKEN not set - orchestrator initialization failed');
118
+ }
119
+ _orchestrator = createOrchestrator({
120
+ linearApiKey: apiKey,
121
+ maxConcurrent: config?.maxConcurrent ?? 10,
122
+ autoTransition: config?.autoTransition ?? true,
123
+ }, {
124
+ onAgentStart: (agent) => {
125
+ log.info('Agent started', {
126
+ agentIdentifier: agent.identifier,
127
+ agentPid: agent.pid,
128
+ issueId: agent.issueId,
129
+ sessionId: agent.sessionId,
130
+ });
131
+ },
132
+ onAgentComplete: async (agent) => {
133
+ log.info('Agent completed', {
134
+ agentIdentifier: agent.identifier,
135
+ issueId: agent.issueId,
136
+ sessionId: agent.sessionId,
137
+ totalCostUsd: agent.totalCostUsd,
138
+ });
139
+ if (agent.sessionId && (agent.totalCostUsd != null || agent.inputTokens != null || agent.outputTokens != null)) {
140
+ await updateSessionCostData(agent.sessionId, {
141
+ totalCostUsd: agent.totalCostUsd,
142
+ inputTokens: agent.inputTokens,
143
+ outputTokens: agent.outputTokens,
144
+ }).catch((err) => log.error('Failed to persist cost data', { error: err }));
145
+ }
146
+ // Track workflow state for result-sensitive work types
147
+ try {
148
+ const workType = agent.workType ?? 'development';
149
+ const phaseMap = {
150
+ development: 'development',
151
+ qa: 'qa',
152
+ 'qa-coordination': 'qa',
153
+ acceptance: 'acceptance',
154
+ 'acceptance-coordination': 'acceptance',
155
+ refinement: 'refinement',
156
+ };
157
+ const phase = phaseMap[workType];
158
+ if (phase) {
159
+ // Ensure workflow state exists
160
+ await updateWorkflowState(agent.issueId, {
161
+ issueIdentifier: agent.identifier,
162
+ });
163
+ // Record the phase attempt
164
+ await recordPhaseAttempt(agent.issueId, phase, {
165
+ attempt: 1, // Will be refined by phase-specific logic
166
+ sessionId: agent.sessionId,
167
+ startedAt: agent.startedAt.getTime(),
168
+ completedAt: agent.completedAt?.getTime(),
169
+ result: agent.workResult ?? (phase === 'development' || phase === 'refinement' ? 'passed' : undefined),
170
+ costUsd: agent.totalCostUsd,
171
+ });
172
+ // On QA/acceptance failure: increment cycle count and append failure summary
173
+ const isResultSensitive = phase === 'qa' || phase === 'acceptance';
174
+ if (isResultSensitive && (agent.workResult === 'failed' || agent.workResult === 'unknown')) {
175
+ const state = await incrementCycleCount(agent.issueId);
176
+ const failureReason = agent.workResult === 'unknown'
177
+ ? 'Agent completed without a structured WORK_RESULT marker (treated as failure)'
178
+ : extractFailureReason(agent.resultMessage);
179
+ const formattedFailure = `--- Cycle ${state.cycleCount}, ${phase} (${new Date().toISOString()}) ---\n${failureReason}`;
180
+ await appendFailureSummary(agent.issueId, formattedFailure);
181
+ log.info('Workflow state updated after failure', {
182
+ issueId: agent.issueId,
183
+ cycleCount: state.cycleCount,
184
+ strategy: state.strategy,
185
+ phase,
186
+ workResult: agent.workResult,
187
+ });
188
+ }
189
+ // On acceptance pass: clear workflow state (issue is done)
190
+ if (phase === 'acceptance' && agent.workResult === 'passed') {
191
+ await clearWorkflowState(agent.issueId);
192
+ await markAcceptanceCompleted(agent.issueId);
193
+ log.info('Workflow state cleared after acceptance pass', {
194
+ issueId: agent.issueId,
195
+ });
196
+ }
197
+ // On acceptance failure/unknown: mark completed to prevent re-trigger loop.
198
+ // The issue stays in Delivered but won't auto-fire another acceptance agent.
199
+ if (phase === 'acceptance' && (agent.workResult === 'failed' || agent.workResult === 'unknown')) {
200
+ await markAcceptanceCompleted(agent.issueId);
201
+ log.info('Marked acceptance completed to prevent re-trigger loop', {
202
+ issueId: agent.issueId,
203
+ workResult: agent.workResult,
204
+ });
205
+ }
206
+ }
207
+ }
208
+ catch (err) {
209
+ log.error('Failed to update workflow state', { error: err, issueId: agent.issueId });
210
+ }
211
+ try {
212
+ await hooks?.onAgentComplete?.(agent);
213
+ }
214
+ catch (err) {
215
+ log.error('Hook onAgentComplete failed', { error: err });
216
+ }
217
+ },
218
+ onAgentError: (agent, error) => {
219
+ log.error('Agent failed', {
220
+ agentIdentifier: agent.identifier,
221
+ issueId: agent.issueId,
222
+ sessionId: agent.sessionId,
223
+ error,
224
+ });
225
+ emitAgentErrorActivity(agent.issueId, error, agent.sessionId).catch((err) => log.error('Failed to emit error activity', { error: err }));
226
+ try {
227
+ hooks?.onAgentError?.(agent, error);
228
+ }
229
+ catch (err) {
230
+ log.error('Hook onAgentError failed', { error: err });
231
+ }
232
+ },
233
+ onAgentStopped: (agent) => {
234
+ log.info('Agent stopped', {
235
+ agentIdentifier: agent.identifier,
236
+ issueId: agent.issueId,
237
+ sessionId: agent.sessionId,
238
+ });
239
+ if (agent.sessionId) {
240
+ updateSessionStatus(agent.sessionId, 'stopped').catch((err) => log.error('Failed to update session status', { error: err }));
241
+ }
242
+ hooks?.onAgentStopped?.(agent);
243
+ },
244
+ onProviderSessionId: async (linearSessionId, providerSessionId) => {
245
+ log.info('Provider session ID captured', { linearSessionId, providerSessionId });
246
+ await updateProviderSessionId(linearSessionId, providerSessionId);
247
+ },
248
+ });
249
+ }
250
+ return _orchestrator;
251
+ }
252
+ return {
253
+ async spawnAgentAsync(issueId, sessionId, webhookId) {
254
+ const idempotencyKey = generateIdempotencyKey(webhookId, sessionId);
255
+ if (await isWebhookProcessed(idempotencyKey)) {
256
+ return { spawned: false, reason: 'duplicate_webhook' };
257
+ }
258
+ const orch = getOrchestrator();
259
+ if (orch.getActiveAgents().some((a) => a.issueId === issueId)) {
260
+ return { spawned: false, reason: 'agent_already_running' };
261
+ }
262
+ await markWebhookProcessed(idempotencyKey);
263
+ const spawnLog = log.child({ issueId, sessionId });
264
+ try {
265
+ const agent = await withRetry(async () => {
266
+ return getOrchestrator().spawnAgentForIssue(issueId, sessionId);
267
+ }, {
268
+ config: retryConfig,
269
+ shouldRetry: isSpawnErrorRetryable,
270
+ onRetry: ({ attempt, delay, lastError }) => {
271
+ spawnLog.warn('Spawn retry attempt', {
272
+ attempt: attempt + 1,
273
+ maxRetries: retryConfig.maxRetries,
274
+ delayMs: delay,
275
+ lastErrorMessage: lastError?.message,
276
+ });
277
+ },
278
+ });
279
+ spawnLog.info('Agent spawn successful', {
280
+ agentIdentifier: agent.identifier,
281
+ agentPid: agent.pid,
282
+ });
283
+ await storeSessionState(sessionId, {
284
+ issueId,
285
+ providerSessionId: agent.providerSessionId ?? null,
286
+ worktreePath: agent.worktreePath ?? '',
287
+ status: 'running',
288
+ });
289
+ return { spawned: true, agent };
290
+ }
291
+ catch (error) {
292
+ const spawnError = error instanceof Error ? error : new Error(String(error));
293
+ const typedError = new AgentSpawnError(`Failed to spawn agent: ${spawnError.message}`, issueId, sessionId, isSpawnErrorRetryable(error), spawnError);
294
+ spawnLog.error('Failed to spawn agent after retries', {
295
+ error: typedError,
296
+ isRetryable: typedError.isRetryable,
297
+ });
298
+ await emitAgentErrorActivity(issueId, typedError, sessionId);
299
+ await cleanupFailedSpawn(issueId, idempotencyKey);
300
+ return { spawned: false, reason: 'spawn_failed', error: typedError };
301
+ }
302
+ },
303
+ async stopAgentBySession(sessionId, cleanupWorktree = true) {
304
+ const stopLog = log.child({ sessionId });
305
+ try {
306
+ const orch = getOrchestrator();
307
+ const result = await orch.stopAgentBySession(sessionId, cleanupWorktree);
308
+ if (result.stopped) {
309
+ stopLog.info('Agent stopped by session', {
310
+ agentIdentifier: result.agent?.identifier,
311
+ cleanedWorktree: cleanupWorktree,
312
+ });
313
+ }
314
+ else {
315
+ stopLog.info('Could not stop agent', { reason: result.reason });
316
+ }
317
+ return result;
318
+ }
319
+ catch (error) {
320
+ stopLog.error('Failed to stop agent', { error });
321
+ throw error;
322
+ }
323
+ },
324
+ getAgentBySession(sessionId) {
325
+ return getOrchestrator().getAgentBySession(sessionId);
326
+ },
327
+ isAgentRunningForIssue(issueId) {
328
+ return getOrchestrator().getActiveAgents().some((a) => a.issueId === issueId);
329
+ },
330
+ async forwardPromptAsync(issueId, sessionId, promptText) {
331
+ const promptLog = log.child({ issueId, sessionId });
332
+ try {
333
+ const sessionState = await getSessionState(sessionId);
334
+ promptLog.info('Forwarding prompt to agent', {
335
+ hasSessionState: !!sessionState,
336
+ hasProviderSessionId: !!sessionState?.providerSessionId,
337
+ promptLength: promptText.length,
338
+ workType: sessionState?.workType ?? 'development',
339
+ });
340
+ const orch = getOrchestrator();
341
+ const result = await orch.forwardPrompt(issueId, sessionId, promptText, sessionState?.providerSessionId ?? undefined, sessionState?.workType);
342
+ if (result.forwarded) {
343
+ promptLog.info('Prompt forwarded successfully', {
344
+ resumed: result.resumed,
345
+ agentIdentifier: result.agent?.identifier,
346
+ agentPid: result.agent?.pid,
347
+ });
348
+ if (result.agent) {
349
+ await storeSessionState(sessionId, {
350
+ issueId,
351
+ providerSessionId: result.agent.providerSessionId ?? null,
352
+ worktreePath: result.agent.worktreePath ?? '',
353
+ status: 'running',
354
+ workType: sessionState?.workType,
355
+ });
356
+ }
357
+ }
358
+ else {
359
+ promptLog.warn('Prompt not forwarded', {
360
+ reason: result.reason,
361
+ error: result.error?.message,
362
+ });
363
+ }
364
+ return result;
365
+ }
366
+ catch (error) {
367
+ const errorMsg = error instanceof Error ? error : new Error(String(error));
368
+ promptLog.error('Failed to forward prompt', { error: errorMsg });
369
+ return { forwarded: false, resumed: false, reason: 'spawn_failed', error: errorMsg };
370
+ }
371
+ },
372
+ };
373
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Core types for @renseiai/agentfactory-nextjs
3
+ *
4
+ * These types define the configuration interfaces that consumers
5
+ * must implement to use the extracted route handlers.
6
+ */
7
+ import type { NextResponse } from 'next/server';
8
+ import type { LinearAgentClient, AgentWorkType, SubIssueStatus, WorkflowContext } from '@renseiai/agentfactory-linear';
9
+ /**
10
+ * Resolves a Linear client for a given organization.
11
+ * Consumers implement this to handle workspace-specific OAuth tokens.
12
+ */
13
+ export interface LinearClientResolver {
14
+ getClient(organizationId?: string): Promise<LinearAgentClient> | LinearAgentClient;
15
+ }
16
+ /**
17
+ * Base configuration for routes that need Linear API access.
18
+ */
19
+ export interface RouteConfig {
20
+ linearClient: LinearClientResolver;
21
+ appUrl?: string;
22
+ }
23
+ /**
24
+ * Auto-trigger configuration for webhook processing.
25
+ */
26
+ export interface AutoTriggerConfig {
27
+ enableAutoQA: boolean;
28
+ enableAutoAcceptance: boolean;
29
+ autoQARequireAgentWorked: boolean;
30
+ autoAcceptanceRequireAgentWorked: boolean;
31
+ autoQAProjects: string[];
32
+ autoAcceptanceProjects: string[];
33
+ autoQAExcludeLabels: string[];
34
+ autoAcceptanceExcludeLabels: string[];
35
+ }
36
+ /**
37
+ * Configuration for the webhook processor.
38
+ *
39
+ * `generatePrompt` is optional — if not provided, falls back to
40
+ * `defaultGeneratePrompt` from @renseiai/agentfactory-linear.
41
+ */
42
+ export interface WebhookConfig extends RouteConfig {
43
+ webhookSecret?: string;
44
+ generatePrompt?: (identifier: string, workType: AgentWorkType, mentionContext?: string, workflowContext?: WorkflowContext) => string;
45
+ detectWorkTypeFromPrompt?: (prompt: string, validWorkTypes: AgentWorkType[]) => AgentWorkType | undefined;
46
+ getPriority?: (workType: AgentWorkType) => number;
47
+ autoTrigger?: AutoTriggerConfig;
48
+ buildParentQAContext?: (identifier: string, subIssues: SubIssueStatus[]) => string;
49
+ buildParentAcceptanceContext?: (identifier: string, subIssues: SubIssueStatus[]) => string;
50
+ /** Linear project names this server handles. Empty/undefined = all projects. */
51
+ projects?: string[];
52
+ /**
53
+ * Path to a directory containing custom workflow template YAML files.
54
+ * Templates in this directory override built-in defaults per work type.
55
+ */
56
+ templateDir?: string;
57
+ /**
58
+ * Governor integration mode:
59
+ * - 'direct' (default): Today's behavior -- webhooks dispatch work directly
60
+ * - 'event-bridge': Dual-write -- webhooks dispatch AND publish governor events
61
+ * - 'governor-only': Events only -- webhooks publish to governor, no direct dispatch
62
+ */
63
+ governorMode?: 'direct' | 'event-bridge' | 'governor-only';
64
+ }
65
+ /**
66
+ * Resolved webhook config with all defaults applied.
67
+ * Used internally by webhook handlers — generatePrompt is guaranteed to be set.
68
+ */
69
+ export interface ResolvedWebhookConfig extends RouteConfig {
70
+ webhookSecret?: string;
71
+ generatePrompt: (identifier: string, workType: AgentWorkType, mentionContext?: string, workflowContext?: WorkflowContext) => string;
72
+ detectWorkTypeFromPrompt?: (prompt: string, validWorkTypes: AgentWorkType[]) => AgentWorkType | undefined;
73
+ getPriority?: (workType: AgentWorkType) => number;
74
+ autoTrigger?: AutoTriggerConfig;
75
+ buildParentQAContext?: (identifier: string, subIssues: SubIssueStatus[]) => string;
76
+ buildParentAcceptanceContext?: (identifier: string, subIssues: SubIssueStatus[]) => string;
77
+ /** Linear project names this server handles. Empty/undefined = all projects. */
78
+ projects?: string[];
79
+ /**
80
+ * Governor integration mode:
81
+ * - 'direct' (default): Today's behavior -- webhooks dispatch work directly
82
+ * - 'event-bridge': Dual-write -- webhooks dispatch AND publish governor events
83
+ * - 'governor-only': Events only -- webhooks publish to governor, no direct dispatch
84
+ */
85
+ governorMode?: 'direct' | 'event-bridge' | 'governor-only';
86
+ }
87
+ /**
88
+ * Configuration for cron-authenticated routes.
89
+ */
90
+ export interface CronConfig {
91
+ cronSecret?: string;
92
+ }
93
+ /**
94
+ * Standard Next.js route handler signature.
95
+ *
96
+ * Uses `any` for the context parameter because Next.js App Router
97
+ * provides different shapes depending on the route segment (static
98
+ * routes get no context, dynamic `[id]` routes get `{ params }`).
99
+ */
100
+ export type RouteHandler = (...args: any[]) => Promise<NextResponse>;
101
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Core types for @renseiai/agentfactory-nextjs
3
+ *
4
+ * These types define the configuration interfaces that consumers
5
+ * must implement to use the extracted route handlers.
6
+ */
7
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Governor Bridge
3
+ *
4
+ * Connects webhook handlers to the GovernorEventBus. When configured,
5
+ * webhook handlers publish events to the bus in addition to (or instead of)
6
+ * their normal direct-dispatch behavior.
7
+ */
8
+ import type { GovernorEventBus, GovernorEvent } from '@renseiai/agentfactory';
9
+ /**
10
+ * Configure the governor event bus for webhook bridging.
11
+ * Call this during server initialization when governorMode != 'direct'.
12
+ */
13
+ export declare function setGovernorEventBus(bus: GovernorEventBus): void;
14
+ /**
15
+ * Get the configured event bus, or null if not configured.
16
+ */
17
+ export declare function getGovernorEventBus(): GovernorEventBus | null;
18
+ /**
19
+ * Publish a GovernorEvent if a bus is configured.
20
+ * Returns the event ID if published, null if no bus is configured.
21
+ */
22
+ export declare function publishGovernorEvent(event: GovernorEvent): Promise<string | null>;
23
+ //# sourceMappingURL=governor-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governor-bridge.d.ts","sourceRoot":"","sources":["../../../src/webhook/governor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAI7E;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAE/D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAE7D;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQvF"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Governor Bridge
3
+ *
4
+ * Connects webhook handlers to the GovernorEventBus. When configured,
5
+ * webhook handlers publish events to the bus in addition to (or instead of)
6
+ * their normal direct-dispatch behavior.
7
+ */
8
+ let _eventBus = null;
9
+ /**
10
+ * Configure the governor event bus for webhook bridging.
11
+ * Call this during server initialization when governorMode != 'direct'.
12
+ */
13
+ export function setGovernorEventBus(bus) {
14
+ _eventBus = bus;
15
+ }
16
+ /**
17
+ * Get the configured event bus, or null if not configured.
18
+ */
19
+ export function getGovernorEventBus() {
20
+ return _eventBus;
21
+ }
22
+ /**
23
+ * Publish a GovernorEvent if a bus is configured.
24
+ * Returns the event ID if published, null if no bus is configured.
25
+ */
26
+ export async function publishGovernorEvent(event) {
27
+ if (!_eventBus)
28
+ return null;
29
+ try {
30
+ return await _eventBus.publish(event);
31
+ }
32
+ catch (err) {
33
+ console.error('[governor-bridge] Failed to publish event:', err);
34
+ return null;
35
+ }
36
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Handle Issue update events — status transition triggers.
3
+ *
4
+ * Handles:
5
+ * - Finished → auto-QA trigger (with circuit breaker)
6
+ * - → Rejected → escalation ladder (circuit breaker, decomposition, human escalation)
7
+ * - (Icebox|Rejected|Canceled) → Backlog → auto-development trigger (with circuit breaker)
8
+ * - Finished → Delivered → auto-acceptance trigger
9
+ */
10
+ import { NextResponse } from 'next/server';
11
+ import type { LinearWebhookPayload } from '@renseiai/agentfactory-linear';
12
+ import type { ResolvedWebhookConfig } from '../../types.js';
13
+ import type { createLogger } from '@renseiai/agentfactory-server';
14
+ export declare function handleIssueUpdated(config: ResolvedWebhookConfig, payload: LinearWebhookPayload, log: ReturnType<typeof createLogger>): Promise<NextResponse | null>;
15
+ //# sourceMappingURL=issue-updated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-updated.d.ts","sourceRoot":"","sources":["../../../../src/webhook/handlers/issue-updated.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C,OAAO,KAAK,EAAE,oBAAoB,EAAiB,MAAM,+BAA+B,CAAA;AA6BxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAQ3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAEjE,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA81B9B"}