@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,9 @@
1
+ /**
2
+ * Handle agent session 'create' events — new session initiated.
3
+ */
4
+ import { NextResponse } from 'next/server';
5
+ import type { LinearWebhookPayload } from '@renseiai/agentfactory-linear';
6
+ import type { ResolvedWebhookConfig } from '../../types.js';
7
+ import type { createLogger } from '@renseiai/agentfactory-server';
8
+ export declare function handleSessionCreated(config: ResolvedWebhookConfig, payload: LinearWebhookPayload, rawPayload: Record<string, unknown>, log: ReturnType<typeof createLogger>): Promise<NextResponse | null>;
9
+ //# sourceMappingURL=session-created.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-created.d.ts","sourceRoot":"","sources":["../../../../src/webhook/handlers/session-created.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAiB,MAAM,+BAA+B,CAAA;AAoBxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAS3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAEjE,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,oBAAoB,EAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAuY9B"}
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Handle agent session 'create' events — new session initiated.
3
+ */
4
+ import { NextResponse } from 'next/server';
5
+ import { TERMINAL_STATUSES, validateWorkTypeForStatus, WORK_TYPE_ALLOWED_STATUSES, STATUS_WORK_TYPE_MAP, getValidWorkTypesForStatus, buildFailureContextBlock, } from '@renseiai/agentfactory-linear';
6
+ import { generateIdempotencyKey, isWebhookProcessed, storeSessionState, getSessionState, updateSessionStatus, dispatchWork, getWorkflowState, } from '@renseiai/agentfactory-server';
7
+ import { emitActivity, determineWorkType, isProjectAllowed, getAppUrl, getPriority, WORK_TYPE_MESSAGES, } from '../utils.js';
8
+ export async function handleSessionCreated(config, payload, rawPayload, log) {
9
+ const agentSession = rawPayload.agentSession;
10
+ log.debug('AgentSessionEvent payload structure', {
11
+ payloadKeys: Object.keys(rawPayload),
12
+ agentSessionKeys: agentSession ? Object.keys(agentSession) : [],
13
+ hasAgentSession: !!agentSession,
14
+ });
15
+ if (!agentSession) {
16
+ log.error('AgentSessionEvent missing agentSession field', {
17
+ payloadKeys: Object.keys(rawPayload),
18
+ });
19
+ return NextResponse.json({ error: 'Missing agentSession in webhook payload', success: false }, { status: 400 });
20
+ }
21
+ const sessionId = agentSession.id;
22
+ const issue = agentSession.issue;
23
+ const issueId = agentSession.issueId || issue?.id;
24
+ const webhookId = rawPayload.webhookId;
25
+ const agentId = agentSession.agentId;
26
+ if (!sessionId || !issueId) {
27
+ log.error('Missing sessionId or issueId in webhook payload', {
28
+ sessionId,
29
+ issueId,
30
+ agentSessionKeys: Object.keys(agentSession),
31
+ });
32
+ return NextResponse.json({ error: 'Invalid payload structure', success: false }, { status: 400 });
33
+ }
34
+ const sessionLog = log.child({ sessionId, issueId });
35
+ const promptContext = rawPayload.promptContext;
36
+ const user = rawPayload.user;
37
+ const comment = rawPayload.comment;
38
+ const isMention = !!comment;
39
+ const initiationType = isMention ? 'mention' : 'delegation';
40
+ sessionLog.info('Agent session created', {
41
+ initiationType,
42
+ isMention,
43
+ hasPromptContext: !!promptContext,
44
+ promptContextLength: promptContext?.length,
45
+ userName: user?.name,
46
+ });
47
+ // Idempotency check
48
+ const idempotencyKey = generateIdempotencyKey(webhookId, sessionId);
49
+ if (await isWebhookProcessed(idempotencyKey)) {
50
+ sessionLog.info('Duplicate webhook ignored', { idempotencyKey });
51
+ return NextResponse.json({ success: true, skipped: true, reason: 'duplicate_webhook' });
52
+ }
53
+ const existingSession = await getSessionState(sessionId);
54
+ if (existingSession) {
55
+ sessionLog.info('Session already exists', { status: existingSession.status });
56
+ return NextResponse.json({ success: true, skipped: true, reason: 'session_already_exists' });
57
+ }
58
+ const issueIdentifier = issue?.identifier || issueId.slice(0, 8);
59
+ // Determine work type
60
+ let workType = 'development';
61
+ let currentStatus;
62
+ let projectName;
63
+ let workTypeSource = 'status';
64
+ // Fetch current issue status
65
+ try {
66
+ const linearClient = await config.linearClient.getClient(payload.organizationId);
67
+ const issueDetails = await linearClient.getIssue(issueId);
68
+ const currentState = await issueDetails.state;
69
+ currentStatus = currentState?.name;
70
+ sessionLog.debug('Fetched current issue status', { currentStatus });
71
+ // Extract project name for routing
72
+ const project = await issueDetails.project;
73
+ projectName = project?.name;
74
+ }
75
+ catch (err) {
76
+ sessionLog.warn('Failed to fetch issue status', { error: err });
77
+ }
78
+ // Server-level project filter
79
+ if (!isProjectAllowed(projectName, config.projects ?? [])) {
80
+ sessionLog.debug('Project not handled by this server, skipping', { projectName });
81
+ return NextResponse.json({ success: true, skipped: true, reason: 'project_not_allowed' });
82
+ }
83
+ // Phase 1: Mention-based routing
84
+ if (isMention && promptContext && config.detectWorkTypeFromPrompt) {
85
+ // For mentions: unconstrained detection (pass all work types)
86
+ const allWorkTypes = [
87
+ 'coordination', 'backlog-creation', 'research', 'qa', 'inflight',
88
+ 'acceptance', 'refinement', 'development', 'qa-coordination', 'acceptance-coordination',
89
+ ];
90
+ const mentionWorkType = config.detectWorkTypeFromPrompt(promptContext, allWorkTypes);
91
+ if (mentionWorkType) {
92
+ workType = mentionWorkType;
93
+ workTypeSource = 'mention';
94
+ sessionLog.info('Detected work type from mention prompt', {
95
+ workType,
96
+ currentStatus,
97
+ promptPreview: promptContext.substring(0, 50),
98
+ });
99
+ }
100
+ }
101
+ // For non-mentions or when mention detection failed: constrained detection
102
+ if (workTypeSource === 'status' && promptContext && currentStatus && config.detectWorkTypeFromPrompt) {
103
+ const validWorkTypes = getValidWorkTypesForStatus(currentStatus);
104
+ const constrainedWorkType = config.detectWorkTypeFromPrompt(promptContext, validWorkTypes);
105
+ if (constrainedWorkType) {
106
+ workType = constrainedWorkType;
107
+ workTypeSource = 'mention';
108
+ sessionLog.info('Detected work type from promptContext (constrained)', {
109
+ workType,
110
+ currentStatus,
111
+ validWorkTypes,
112
+ });
113
+ }
114
+ }
115
+ // Phase 2: Fall back to status-based routing
116
+ if (workTypeSource === 'status') {
117
+ workType = determineWorkType(currentStatus);
118
+ sessionLog.info('Detected work type from issue status', {
119
+ currentStatus,
120
+ workType,
121
+ });
122
+ }
123
+ // Phase 2.5: Auto-detect parent/child issues for coordination routing
124
+ // Apply to all status-derived work types that have coordination variants
125
+ const coordinationUpgradeable = workTypeSource === 'status' && (workType === 'development' || workType === 'refinement');
126
+ if (coordinationUpgradeable) {
127
+ try {
128
+ const linearClient = await config.linearClient.getClient(payload.organizationId);
129
+ // Skip child/sub-issues for non-mention sessions — these are managed
130
+ // by the parent issue's coordinator agent, not dispatched independently.
131
+ if (workType === 'development') {
132
+ const isChild = await linearClient.isChildIssue(issueId);
133
+ if (isChild) {
134
+ sessionLog.info('Sub-issue detected, skipping independent agent dispatch', {
135
+ issueIdentifier,
136
+ });
137
+ try {
138
+ await emitActivity(linearClient, sessionId, 'response', `This is a sub-issue. Work will be coordinated by the parent issue's agent.`);
139
+ }
140
+ catch (err) {
141
+ sessionLog.warn('Failed to emit sub-issue skip activity', { error: err });
142
+ }
143
+ return NextResponse.json({
144
+ success: true,
145
+ skipped: true,
146
+ reason: 'sub_issue_managed_by_parent',
147
+ });
148
+ }
149
+ }
150
+ const isParent = await linearClient.isParentIssue(issueId);
151
+ if (isParent) {
152
+ if (workType === 'development')
153
+ workType = 'coordination';
154
+ else if (workType === 'refinement')
155
+ workType = 'refinement-coordination';
156
+ sessionLog.info('Parent issue detected, switching to coordination work type', {
157
+ issueIdentifier,
158
+ workType,
159
+ });
160
+ }
161
+ }
162
+ catch (err) {
163
+ sessionLog.warn('Failed to check issue parent/child status', { error: err });
164
+ }
165
+ }
166
+ // Check terminal state
167
+ if (currentStatus && TERMINAL_STATUSES.includes(currentStatus)) {
168
+ sessionLog.info('Issue in terminal state, acknowledging mention', { currentStatus });
169
+ try {
170
+ const linearClient = await config.linearClient.getClient(payload.organizationId);
171
+ await emitActivity(linearClient, sessionId, 'response', `This issue is in **${currentStatus}** status and has been completed. No further agent work is needed.\n\n` +
172
+ `If you need additional help, please create a new issue or reopen this one by moving it back to an active status.`);
173
+ }
174
+ catch (err) {
175
+ sessionLog.error('Failed to emit terminal state response', { error: err });
176
+ }
177
+ return NextResponse.json({ success: true, skipped: true, reason: 'terminal_state', currentStatus });
178
+ }
179
+ // Validate work type for status
180
+ if (currentStatus) {
181
+ const validation = validateWorkTypeForStatus(workType, currentStatus);
182
+ if (!validation.valid) {
183
+ sessionLog.warn('Work type validation failed', { workType, currentStatus, error: validation.error });
184
+ try {
185
+ const linearClient = await config.linearClient.getClient(payload.organizationId);
186
+ const allowedStatuses = WORK_TYPE_ALLOWED_STATUSES[workType];
187
+ const expectedWorkType = STATUS_WORK_TYPE_MAP[currentStatus];
188
+ await emitActivity(linearClient, sessionId, 'error', `Cannot perform ${workType} work on this issue.\n\n` +
189
+ `**Current status:** ${currentStatus}\n` +
190
+ `**${workType} requires status:** ${allowedStatuses.join(' or ')}\n\n` +
191
+ (expectedWorkType
192
+ ? `For issues in ${currentStatus} status, use ${expectedWorkType} commands instead.`
193
+ : `This issue's status (${currentStatus}) is not handled by the agent.`));
194
+ }
195
+ catch (err) {
196
+ sessionLog.error('Failed to emit validation error activity', { error: err });
197
+ }
198
+ return NextResponse.json({
199
+ success: false,
200
+ error: 'work_type_invalid_for_status',
201
+ message: validation.error,
202
+ });
203
+ }
204
+ }
205
+ // Extract PR info for QA/acceptance agents so they know which PRs to validate
206
+ let prContext = '';
207
+ const needsPrContext = workType === 'qa' ||
208
+ workType === 'acceptance' ||
209
+ workType === 'qa-coordination' ||
210
+ workType === 'acceptance-coordination';
211
+ if (needsPrContext) {
212
+ try {
213
+ const linearClient = await config.linearClient.getClient(payload.organizationId);
214
+ const issueForAttachments = await linearClient.getIssue(issueId);
215
+ const attachments = await issueForAttachments.attachments();
216
+ const prLinks = attachments.nodes
217
+ .filter((a) => a.url?.includes('github.com') && a.url?.includes('/pull/'))
218
+ .map((a) => {
219
+ const match = a.url.match(/\/pull\/(\d+)/);
220
+ return match ? { url: a.url, number: parseInt(match[1], 10), title: a.title ?? '' } : null;
221
+ })
222
+ .filter((pr) => pr !== null);
223
+ if (prLinks.length > 0) {
224
+ prContext = '\n\nLinked PRs:\n';
225
+ prContext += prLinks.map((pr) => `- PR #${pr.number}: ${pr.title} (${pr.url})`).join('\n');
226
+ if (prLinks.length > 1) {
227
+ prContext +=
228
+ '\n\nNote: Multiple PRs are linked. Check each PR state (open/merged/closed) and validate the most recent OPEN one.';
229
+ }
230
+ }
231
+ sessionLog.debug('Extracted PR context for QA/acceptance', {
232
+ prLinksCount: prLinks.length,
233
+ hasPrContext: prContext.length > 0,
234
+ });
235
+ }
236
+ catch (err) {
237
+ sessionLog.warn('Failed to extract PR info from issue attachments', { error: err });
238
+ }
239
+ }
240
+ const enhancedPromptContext = prContext
241
+ ? promptContext
242
+ ? promptContext + prContext
243
+ : prContext
244
+ : promptContext;
245
+ const priority = getPriority(config, workType);
246
+ await storeSessionState(sessionId, {
247
+ issueId,
248
+ issueIdentifier,
249
+ providerSessionId: null,
250
+ worktreePath: '',
251
+ status: 'pending',
252
+ queuedAt: Date.now(),
253
+ promptContext: promptContext,
254
+ priority,
255
+ organizationId: payload.organizationId,
256
+ workType,
257
+ agentId,
258
+ projectName,
259
+ });
260
+ // Enrich prompt with workflow failure context for retries
261
+ // Applies to: refinement, development (rework from Backlog), coordination (rework from Started)
262
+ let workflowContextBlock = '';
263
+ let wfContext;
264
+ const needsFailureContext = workType === 'refinement' || workType === 'refinement-coordination' ||
265
+ (workType === 'development' && currentStatus === 'Backlog') ||
266
+ (workType === 'coordination' && currentStatus === 'Started');
267
+ if (needsFailureContext) {
268
+ try {
269
+ const workflowState = await getWorkflowState(issueId);
270
+ if (workflowState && workflowState.cycleCount > 0) {
271
+ wfContext = {
272
+ cycleCount: workflowState.cycleCount,
273
+ strategy: workflowState.strategy,
274
+ failureSummary: workflowState.failureSummary,
275
+ };
276
+ workflowContextBlock = buildFailureContextBlock(workType, wfContext);
277
+ if (workflowContextBlock) {
278
+ sessionLog.info('Prompt enriched with workflow failure context', {
279
+ workType,
280
+ cycleCount: workflowState.cycleCount,
281
+ strategy: workflowState.strategy,
282
+ });
283
+ }
284
+ }
285
+ }
286
+ catch (err) {
287
+ sessionLog.warn('Failed to enrich prompt with workflow context', { error: err });
288
+ }
289
+ }
290
+ // Queue work
291
+ const work = {
292
+ sessionId,
293
+ issueId,
294
+ issueIdentifier,
295
+ priority,
296
+ queuedAt: Date.now(),
297
+ workType,
298
+ prompt: config.generatePrompt(issueIdentifier, workType, enhancedPromptContext, wfContext) + workflowContextBlock,
299
+ projectName,
300
+ };
301
+ const result = await dispatchWork(work);
302
+ if (result.dispatched || result.parked) {
303
+ sessionLog.info('Work dispatched', {
304
+ sessionId,
305
+ issueIdentifier,
306
+ workType,
307
+ dispatched: result.dispatched,
308
+ parked: result.parked,
309
+ replaced: result.replaced,
310
+ });
311
+ }
312
+ else {
313
+ sessionLog.error('Failed to dispatch work');
314
+ await updateSessionStatus(sessionId, 'failed');
315
+ }
316
+ // Update session with externalUrl and acknowledge
317
+ try {
318
+ const linearClient = await config.linearClient.getClient(payload.organizationId);
319
+ const appUrl = getAppUrl(config);
320
+ await linearClient.updateAgentSession({
321
+ sessionId,
322
+ externalUrls: [
323
+ {
324
+ label: 'Agent Dashboard',
325
+ url: `${appUrl}/sessions/${sessionId}`,
326
+ },
327
+ ],
328
+ });
329
+ const activityText = WORK_TYPE_MESSAGES[workType];
330
+ await emitActivity(linearClient, sessionId, 'thought', activityText);
331
+ sessionLog.debug('Session updated and activity emitted');
332
+ }
333
+ catch (err) {
334
+ sessionLog.error('Failed to update session or create comment', { error: err });
335
+ }
336
+ return null; // Continue processing
337
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Handle agent session 'prompted' events — follow-up messages or stop/continue signals.
3
+ */
4
+ import { NextResponse } from 'next/server';
5
+ import type { LinearWebhookPayload } from '@renseiai/agentfactory-linear';
6
+ import type { ResolvedWebhookConfig } from '../../types.js';
7
+ import type { createLogger } from '@renseiai/agentfactory-server';
8
+ export declare function handleSessionPrompted(config: ResolvedWebhookConfig, payload: LinearWebhookPayload, rawPayload: Record<string, unknown>, log: ReturnType<typeof createLogger>): Promise<NextResponse | null>;
9
+ //# sourceMappingURL=session-prompted.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-prompted.d.ts","sourceRoot":"","sources":["../../../../src/webhook/handlers/session-prompted.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAiB,MAAM,+BAA+B,CAAA;AAaxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAEjE,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,oBAAoB,EAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAoP9B"}
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Handle agent session 'prompted' events — follow-up messages or stop/continue signals.
3
+ */
4
+ import { NextResponse } from 'next/server';
5
+ import { getSessionState, updateSessionStatus, storeSessionState, releaseClaim, removeWorkerSession, dispatchWork, storePendingPrompt, generateIdempotencyKey, isWebhookProcessed, } from '@renseiai/agentfactory-server';
6
+ import { handleStopSignal, emitActivity } from '../utils.js';
7
+ export async function handleSessionPrompted(config, payload, rawPayload, log) {
8
+ const agentSession = rawPayload.agentSession;
9
+ if (!agentSession) {
10
+ log.warn('AgentSessionEvent prompted missing agentSession field');
11
+ return NextResponse.json({ success: true, skipped: true, reason: 'missing_agent_session' });
12
+ }
13
+ const sessionId = agentSession.id;
14
+ const issue = agentSession.issue;
15
+ const issueId = agentSession.issueId || issue?.id;
16
+ const promptText = rawPayload.promptContext || '';
17
+ const webhookId = rawPayload.webhookId;
18
+ const user = rawPayload.user;
19
+ const agentActivity = rawPayload.agentActivity;
20
+ const comment = rawPayload.comment;
21
+ const commentBody = comment?.body || '';
22
+ const promptLog = log.child({ sessionId, issueId });
23
+ // Check for stop/continue signals
24
+ const activitySignal = agentActivity?.signal;
25
+ const isStopSignal = activitySignal === 'stop';
26
+ const isContinueSignal = activitySignal === 'continue';
27
+ promptLog.info('Agent session prompted', {
28
+ hasPromptContext: !!promptText,
29
+ promptContextLength: promptText?.length,
30
+ userName: user?.name,
31
+ hasAgentActivity: !!agentActivity,
32
+ activitySignal,
33
+ isStopSignal,
34
+ isContinueSignal,
35
+ hasComment: !!comment,
36
+ commentBodyLength: commentBody?.length,
37
+ });
38
+ // Handle stop signal
39
+ if (isStopSignal) {
40
+ promptLog.info('Stop signal received via prompted webhook');
41
+ await handleStopSignal(config, sessionId, issueId, payload.organizationId);
42
+ return NextResponse.json({ success: true, action: 'stopped', sessionId });
43
+ }
44
+ // Handle continue signal
45
+ if (isContinueSignal) {
46
+ promptLog.info('Continue signal received via prompted webhook');
47
+ const existingSession = await getSessionState(sessionId);
48
+ const workType = existingSession?.workType || 'inflight';
49
+ let resumePrompt = promptText || commentBody || '';
50
+ const issueIdentifier = existingSession?.issueIdentifier ||
51
+ issue?.identifier ||
52
+ issueId.slice(0, 8);
53
+ promptLog.info('Session state for continue', {
54
+ hasExistingSession: !!existingSession,
55
+ sessionStatus: existingSession?.status,
56
+ workType,
57
+ });
58
+ if (!resumePrompt.trim()) {
59
+ resumePrompt = config.generatePrompt(issueIdentifier, workType);
60
+ }
61
+ // Reset session status if in terminal state
62
+ if (existingSession && ['completed', 'failed', 'stopped'].includes(existingSession.status)) {
63
+ await releaseClaim(sessionId);
64
+ if (existingSession.workerId) {
65
+ await removeWorkerSession(existingSession.workerId, sessionId);
66
+ }
67
+ await updateSessionStatus(sessionId, 'pending');
68
+ }
69
+ // Update organizationId if missing
70
+ if (existingSession && !existingSession.organizationId && payload.organizationId) {
71
+ await storeSessionState(sessionId, {
72
+ ...existingSession,
73
+ organizationId: payload.organizationId,
74
+ status: 'pending',
75
+ });
76
+ }
77
+ // Queue work to resume
78
+ const work = {
79
+ sessionId,
80
+ issueId,
81
+ issueIdentifier,
82
+ priority: 2,
83
+ queuedAt: Date.now(),
84
+ prompt: resumePrompt,
85
+ providerSessionId: existingSession?.providerSessionId || undefined,
86
+ workType,
87
+ projectName: existingSession?.projectName,
88
+ };
89
+ await dispatchWork(work);
90
+ try {
91
+ const linearClient = await config.linearClient.getClient(payload.organizationId);
92
+ await emitActivity(linearClient, sessionId, 'thought', 'Session resume requested. Waiting for an available worker...');
93
+ }
94
+ catch (err) {
95
+ promptLog.error('Failed to emit continue acknowledgment activity', { error: err });
96
+ }
97
+ return NextResponse.json({ success: true, action: 'continue_queued', sessionId });
98
+ }
99
+ // Generate idempotency key for prompted events
100
+ const idempotencyKey = generateIdempotencyKey(webhookId, `${sessionId}:prompt:${payload.createdAt}`);
101
+ if (await isWebhookProcessed(idempotencyKey)) {
102
+ promptLog.info('Duplicate prompted webhook ignored', { idempotencyKey });
103
+ return NextResponse.json({ success: true, skipped: true, reason: 'duplicate_prompt' });
104
+ }
105
+ const existingSession = await getSessionState(sessionId);
106
+ const issueIdentifier = existingSession?.issueIdentifier ||
107
+ issue?.identifier ||
108
+ issueId.slice(0, 8);
109
+ // Determine effective prompt with cascading fallbacks
110
+ let effectivePrompt = promptText.trim();
111
+ if (!effectivePrompt && commentBody.trim()) {
112
+ effectivePrompt = commentBody.trim();
113
+ promptLog.info('Using comment body as prompt fallback');
114
+ }
115
+ if (!effectivePrompt) {
116
+ if (existingSession) {
117
+ effectivePrompt = config.generatePrompt(issueIdentifier, existingSession.workType || 'inflight');
118
+ promptLog.info('Generated continue prompt for empty prompt');
119
+ }
120
+ else {
121
+ promptLog.warn('Empty prompt with no existing session, skipping');
122
+ return NextResponse.json({ success: true, skipped: true, reason: 'empty_prompt_no_session' });
123
+ }
124
+ }
125
+ // If session is running, store as pending prompt
126
+ if (existingSession?.status === 'running' || existingSession?.status === 'claimed') {
127
+ const userInfo = user ? {
128
+ id: user.id,
129
+ name: user.name,
130
+ } : undefined;
131
+ const pendingPrompt = await storePendingPrompt(sessionId, issueId, effectivePrompt, userInfo);
132
+ if (pendingPrompt) {
133
+ promptLog.info('Pending prompt stored for running session', {
134
+ promptId: pendingPrompt.id,
135
+ sessionId,
136
+ issueIdentifier,
137
+ workerId: existingSession.workerId,
138
+ });
139
+ }
140
+ else {
141
+ promptLog.error('Failed to store pending prompt');
142
+ }
143
+ }
144
+ else {
145
+ // Session not running — queue as work
146
+ promptLog.info('Queuing follow-up for non-running session', {
147
+ sessionStatus: existingSession?.status,
148
+ });
149
+ if (existingSession && ['completed', 'failed', 'stopped'].includes(existingSession.status)) {
150
+ await releaseClaim(sessionId);
151
+ if (existingSession.workerId) {
152
+ await removeWorkerSession(existingSession.workerId, sessionId);
153
+ }
154
+ await updateSessionStatus(sessionId, 'pending');
155
+ }
156
+ if (existingSession && !existingSession.organizationId && payload.organizationId) {
157
+ await storeSessionState(sessionId, {
158
+ ...existingSession,
159
+ organizationId: payload.organizationId,
160
+ status: 'pending',
161
+ });
162
+ }
163
+ const work = {
164
+ sessionId,
165
+ issueId,
166
+ issueIdentifier,
167
+ priority: 2,
168
+ queuedAt: Date.now(),
169
+ prompt: effectivePrompt,
170
+ providerSessionId: existingSession?.providerSessionId || undefined,
171
+ workType: existingSession?.workType || 'inflight',
172
+ projectName: existingSession?.projectName,
173
+ };
174
+ const dispatchResult = await dispatchWork(work);
175
+ if (dispatchResult.dispatched || dispatchResult.parked) {
176
+ promptLog.info('Follow-up prompt dispatched', {
177
+ sessionId,
178
+ issueIdentifier,
179
+ dispatched: dispatchResult.dispatched,
180
+ parked: dispatchResult.parked,
181
+ });
182
+ }
183
+ else {
184
+ promptLog.error('Failed to dispatch follow-up prompt');
185
+ }
186
+ }
187
+ // Acknowledge prompt receipt
188
+ try {
189
+ const linearClient = await config.linearClient.getClient(payload.organizationId);
190
+ const truncatedPrompt = effectivePrompt.length > 100
191
+ ? `${effectivePrompt.substring(0, 100)}...`
192
+ : effectivePrompt;
193
+ await emitActivity(linearClient, sessionId, 'thought', `Follow-up received: "${truncatedPrompt}" - Processing...`);
194
+ }
195
+ catch (err) {
196
+ promptLog.error('Failed to emit prompt acknowledgment activity', { error: err });
197
+ }
198
+ return null; // Continue processing
199
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Handle agent session 'update' events — stop signal from user.
3
+ */
4
+ import { NextResponse } from 'next/server';
5
+ import type { LinearWebhookPayload } from '@renseiai/agentfactory-linear';
6
+ import type { ResolvedWebhookConfig } from '../../types.js';
7
+ import type { createLogger } from '@renseiai/agentfactory-server';
8
+ export declare function handleSessionUpdated(config: ResolvedWebhookConfig, payload: LinearWebhookPayload, rawPayload: Record<string, unknown>, log: ReturnType<typeof createLogger>): Promise<NextResponse | null>;
9
+ //# sourceMappingURL=session-updated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-updated.d.ts","sourceRoot":"","sources":["../../../../src/webhook/handlers/session-updated.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAEjE,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,oBAAoB,EAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA6B9B"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Handle agent session 'update' events — stop signal from user.
3
+ */
4
+ import { NextResponse } from 'next/server';
5
+ import { handleStopSignal } from '../utils.js';
6
+ export async function handleSessionUpdated(config, payload, rawPayload, log) {
7
+ const agentSession = rawPayload.agentSession;
8
+ if (!agentSession) {
9
+ log.warn('AgentSessionEvent updated missing agentSession field');
10
+ return NextResponse.json({ success: true, skipped: true, reason: 'missing_agent_session' });
11
+ }
12
+ const sessionId = agentSession.id;
13
+ const issue = agentSession.issue;
14
+ const issueId = agentSession.issueId || issue?.id;
15
+ const newState = agentSession.state;
16
+ const updatedFrom = rawPayload.updatedFrom;
17
+ const previousState = updatedFrom?.state;
18
+ const updateLog = log.child({ sessionId, issueId });
19
+ updateLog.info('Agent session updated', {
20
+ newState,
21
+ previousState,
22
+ });
23
+ // Check if this is a stop signal (state changed to completed/failed)
24
+ if (newState === 'completed' || newState === 'failed') {
25
+ updateLog.info('Stop signal received via updated webhook', { previousState });
26
+ await handleStopSignal(config, sessionId, issueId, payload.organizationId);
27
+ }
28
+ return null; // Continue processing other handlers
29
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Webhook Processor — Main Entry Point
3
+ *
4
+ * Receives Linear webhook events, verifies signatures, runs idempotency
5
+ * checks, and dispatches to sub-handlers.
6
+ */
7
+ import { NextRequest, NextResponse } from 'next/server';
8
+ import type { ResolvedWebhookConfig } from '../types.js';
9
+ /**
10
+ * Create webhook route handlers from config.
11
+ *
12
+ * Returns { POST, GET } for use as Next.js App Router exports.
13
+ */
14
+ export declare function createWebhookHandler(config: ResolvedWebhookConfig): {
15
+ POST: (request: NextRequest) => Promise<NextResponse<unknown>>;
16
+ GET: () => Promise<NextResponse<{
17
+ status: string;
18
+ endpoint: string;
19
+ description: string;
20
+ }>>;
21
+ };
22
+ //# sourceMappingURL=processor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processor.d.ts","sourceRoot":"","sources":["../../../src/webhook/processor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AASvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AASxD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,qBAAqB;oBACnC,WAAW;;;;;;EA2FzC"}