@supaku/agentfactory-nextjs 0.3.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.
- package/LICENSE +21 -0
- package/dist/src/factory.d.ts +105 -0
- package/dist/src/factory.d.ts.map +1 -0
- package/dist/src/factory.js +89 -0
- package/dist/src/handlers/cleanup.d.ts +44 -0
- package/dist/src/handlers/cleanup.d.ts.map +1 -0
- package/dist/src/handlers/cleanup.js +34 -0
- package/dist/src/handlers/public/session-detail.d.ts +31 -0
- package/dist/src/handlers/public/session-detail.d.ts.map +1 -0
- package/dist/src/handlers/public/session-detail.js +91 -0
- package/dist/src/handlers/public/sessions-list.d.ts +20 -0
- package/dist/src/handlers/public/sessions-list.d.ts.map +1 -0
- package/dist/src/handlers/public/sessions-list.js +73 -0
- package/dist/src/handlers/public/stats.d.ts +22 -0
- package/dist/src/handlers/public/stats.d.ts.map +1 -0
- package/dist/src/handlers/public/stats.js +53 -0
- package/dist/src/handlers/sessions/activity.d.ts +15 -0
- package/dist/src/handlers/sessions/activity.d.ts.map +1 -0
- package/dist/src/handlers/sessions/activity.js +77 -0
- package/dist/src/handlers/sessions/claim.d.ts +15 -0
- package/dist/src/handlers/sessions/claim.d.ts.map +1 -0
- package/dist/src/handlers/sessions/claim.js +87 -0
- package/dist/src/handlers/sessions/completion.d.ts +16 -0
- package/dist/src/handlers/sessions/completion.d.ts.map +1 -0
- package/dist/src/handlers/sessions/completion.js +82 -0
- package/dist/src/handlers/sessions/external-urls.d.ts +15 -0
- package/dist/src/handlers/sessions/external-urls.d.ts.map +1 -0
- package/dist/src/handlers/sessions/external-urls.js +59 -0
- package/dist/src/handlers/sessions/get.d.ts +19 -0
- package/dist/src/handlers/sessions/get.d.ts.map +1 -0
- package/dist/src/handlers/sessions/get.js +46 -0
- package/dist/src/handlers/sessions/list.d.ts +26 -0
- package/dist/src/handlers/sessions/list.d.ts.map +1 -0
- package/dist/src/handlers/sessions/list.js +50 -0
- package/dist/src/handlers/sessions/lock-refresh.d.ts +14 -0
- package/dist/src/handlers/sessions/lock-refresh.d.ts.map +1 -0
- package/dist/src/handlers/sessions/lock-refresh.js +38 -0
- package/dist/src/handlers/sessions/progress.d.ts +15 -0
- package/dist/src/handlers/sessions/progress.d.ts.map +1 -0
- package/dist/src/handlers/sessions/progress.js +82 -0
- package/dist/src/handlers/sessions/prompts.d.ts +15 -0
- package/dist/src/handlers/sessions/prompts.d.ts.map +1 -0
- package/dist/src/handlers/sessions/prompts.js +91 -0
- package/dist/src/handlers/sessions/status.d.ts +18 -0
- package/dist/src/handlers/sessions/status.d.ts.map +1 -0
- package/dist/src/handlers/sessions/status.js +131 -0
- package/dist/src/handlers/sessions/tool-error.d.ts +15 -0
- package/dist/src/handlers/sessions/tool-error.d.ts.map +1 -0
- package/dist/src/handlers/sessions/tool-error.js +91 -0
- package/dist/src/handlers/sessions/transfer-ownership.d.ts +14 -0
- package/dist/src/handlers/sessions/transfer-ownership.d.ts.map +1 -0
- package/dist/src/handlers/sessions/transfer-ownership.js +56 -0
- package/dist/src/handlers/workers/get-delete.d.ts +15 -0
- package/dist/src/handlers/workers/get-delete.d.ts.map +1 -0
- package/dist/src/handlers/workers/get-delete.js +58 -0
- package/dist/src/handlers/workers/heartbeat.d.ts +14 -0
- package/dist/src/handlers/workers/heartbeat.d.ts.map +1 -0
- package/dist/src/handlers/workers/heartbeat.js +42 -0
- package/dist/src/handlers/workers/list.d.ts +22 -0
- package/dist/src/handlers/workers/list.d.ts.map +1 -0
- package/dist/src/handlers/workers/list.js +33 -0
- package/dist/src/handlers/workers/poll.d.ts +14 -0
- package/dist/src/handlers/workers/poll.d.ts.map +1 -0
- package/dist/src/handlers/workers/poll.js +78 -0
- package/dist/src/handlers/workers/register.d.ts +9 -0
- package/dist/src/handlers/workers/register.d.ts.map +1 -0
- package/dist/src/handlers/workers/register.js +41 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +41 -0
- package/dist/src/middleware/cron-auth.d.ts +21 -0
- package/dist/src/middleware/cron-auth.d.ts.map +1 -0
- package/dist/src/middleware/cron-auth.js +46 -0
- package/dist/src/middleware/worker-auth.d.ts +25 -0
- package/dist/src/middleware/worker-auth.d.ts.map +1 -0
- package/dist/src/middleware/worker-auth.js +43 -0
- package/dist/src/types.d.ts +62 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +7 -0
- package/dist/src/webhook/handlers/issue-updated.d.ts +14 -0
- package/dist/src/webhook/handlers/issue-updated.d.ts.map +1 -0
- package/dist/src/webhook/handlers/issue-updated.js +462 -0
- package/dist/src/webhook/handlers/session-created.d.ts +9 -0
- package/dist/src/webhook/handlers/session-created.d.ts.map +1 -0
- package/dist/src/webhook/handlers/session-created.js +229 -0
- package/dist/src/webhook/handlers/session-prompted.d.ts +9 -0
- package/dist/src/webhook/handlers/session-prompted.d.ts.map +1 -0
- package/dist/src/webhook/handlers/session-prompted.js +197 -0
- package/dist/src/webhook/handlers/session-updated.d.ts +9 -0
- package/dist/src/webhook/handlers/session-updated.d.ts.map +1 -0
- package/dist/src/webhook/handlers/session-updated.js +29 -0
- package/dist/src/webhook/processor.d.ts +22 -0
- package/dist/src/webhook/processor.d.ts.map +1 -0
- package/dist/src/webhook/processor.js +98 -0
- package/dist/src/webhook/signature.d.ts +16 -0
- package/dist/src/webhook/signature.d.ts.map +1 -0
- package/dist/src/webhook/signature.js +23 -0
- package/dist/src/webhook/utils.d.ts +61 -0
- package/dist/src/webhook/utils.d.ts.map +1 -0
- package/dist/src/webhook/utils.js +159 -0
- package/package.json +66 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle Issue update events — status transition triggers.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Finished → auto-QA trigger
|
|
6
|
+
* - Icebox → Backlog → auto-development trigger
|
|
7
|
+
* - Finished → Delivered → auto-acceptance trigger
|
|
8
|
+
*/
|
|
9
|
+
import { NextResponse } from 'next/server';
|
|
10
|
+
import { checkIssueDeploymentStatus, formatFailedDeployments, } from '@supaku/agentfactory';
|
|
11
|
+
import { generateIdempotencyKey, isWebhookProcessed, storeSessionState, getSessionStateByIssue, dispatchWork, wasAgentWorked, didJustFailQA, getQAAttemptCount, recordQAAttempt, clearQAFailed, didJustQueueDevelopment, markDevelopmentQueued, didJustQueueAcceptance, markAcceptanceQueued, } from '@supaku/agentfactory-server';
|
|
12
|
+
import { emitActivity, resolveStateName, isProjectAllowed, hasExcludedLabel, getAppUrl, } from '../utils.js';
|
|
13
|
+
export async function handleIssueUpdated(config, payload, log) {
|
|
14
|
+
const { data, updatedFrom, actor } = payload;
|
|
15
|
+
const issueId = data.id;
|
|
16
|
+
const issueIdentifier = data.identifier;
|
|
17
|
+
const currentStateName = data.state?.name;
|
|
18
|
+
const webhookId = payload.webhookId;
|
|
19
|
+
const issueLog = log.child({ issueId, issueIdentifier });
|
|
20
|
+
const autoTrigger = config.autoTrigger;
|
|
21
|
+
// === Handle Finished transition (auto-QA) ===
|
|
22
|
+
if (currentStateName === 'Finished' && updatedFrom?.stateId) {
|
|
23
|
+
issueLog.info('Issue transitioned to Finished', {
|
|
24
|
+
previousStateId: updatedFrom.stateId,
|
|
25
|
+
currentState: currentStateName,
|
|
26
|
+
actorName: actor?.name,
|
|
27
|
+
});
|
|
28
|
+
// Skip QA for sub-issues
|
|
29
|
+
let isChild = !!(data.parent);
|
|
30
|
+
if (!isChild) {
|
|
31
|
+
try {
|
|
32
|
+
const checkClient = await config.linearClient.getClient(payload.organizationId);
|
|
33
|
+
isChild = await checkClient.isChildIssue(issueId);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
issueLog.warn('Failed to check if issue is a child', { error: err });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (isChild) {
|
|
40
|
+
issueLog.info('Sub-issue detected, skipping individual QA trigger');
|
|
41
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'sub_issue_skipped' });
|
|
42
|
+
}
|
|
43
|
+
if (!autoTrigger?.enableAutoQA) {
|
|
44
|
+
issueLog.debug('Auto-QA disabled, skipping QA trigger');
|
|
45
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'auto_qa_disabled' });
|
|
46
|
+
}
|
|
47
|
+
const projectName = data.project?.name;
|
|
48
|
+
if (!isProjectAllowed(projectName, autoTrigger.autoQAProjects)) {
|
|
49
|
+
issueLog.debug('Project not in auto-QA list, skipping', { projectName });
|
|
50
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'project_not_allowed' });
|
|
51
|
+
}
|
|
52
|
+
const labels = data.labels;
|
|
53
|
+
if (hasExcludedLabel(labels, autoTrigger.autoQAExcludeLabels)) {
|
|
54
|
+
issueLog.debug('Issue has excluded label, skipping QA trigger');
|
|
55
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'excluded_label' });
|
|
56
|
+
}
|
|
57
|
+
if (autoTrigger.autoQARequireAgentWorked) {
|
|
58
|
+
const workRecord = await wasAgentWorked(issueId);
|
|
59
|
+
if (!workRecord) {
|
|
60
|
+
issueLog.debug('Issue not worked by agent, skipping QA trigger');
|
|
61
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'not_agent_worked' });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const justFailed = await didJustFailQA(issueId);
|
|
65
|
+
if (justFailed) {
|
|
66
|
+
issueLog.info('Issue in QA cooldown period, skipping');
|
|
67
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'qa_cooldown' });
|
|
68
|
+
}
|
|
69
|
+
const attemptCount = await getQAAttemptCount(issueId);
|
|
70
|
+
if (attemptCount >= 3) {
|
|
71
|
+
issueLog.warn('QA attempt limit reached', { attemptCount });
|
|
72
|
+
try {
|
|
73
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
74
|
+
await linearClient.createComment(issueId, `## QA Limit Reached\n\nThis issue has failed automated QA ${attemptCount} times. Manual review is required.\n\nPlease review the previous QA failures and address the underlying issues before requesting another automated QA pass.`);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
issueLog.error('Failed to post QA limit comment', { error: err });
|
|
78
|
+
}
|
|
79
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'qa_limit_reached' });
|
|
80
|
+
}
|
|
81
|
+
const idempotencyKey = generateIdempotencyKey(webhookId, `qa:${issueId}:${Date.now()}`);
|
|
82
|
+
if (await isWebhookProcessed(idempotencyKey)) {
|
|
83
|
+
issueLog.info('Duplicate QA trigger ignored', { idempotencyKey });
|
|
84
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'duplicate_qa_trigger' });
|
|
85
|
+
}
|
|
86
|
+
// Check deployment status before QA
|
|
87
|
+
try {
|
|
88
|
+
const deploymentResult = await checkIssueDeploymentStatus(issueIdentifier);
|
|
89
|
+
if (deploymentResult?.anyFailed) {
|
|
90
|
+
issueLog.info('Deployment failed, blocking QA', {
|
|
91
|
+
commitSha: deploymentResult.commitSha,
|
|
92
|
+
});
|
|
93
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
94
|
+
await linearClient.createComment(issueId, `## QA Blocked: Deployment Failed\n\n` +
|
|
95
|
+
`Cannot proceed with QA until Vercel deployment succeeds.\n\n` +
|
|
96
|
+
formatFailedDeployments(deploymentResult) +
|
|
97
|
+
`\n\n**PR:** ${deploymentResult.pr.url}\n` +
|
|
98
|
+
`**Commit:** \`${deploymentResult.commitSha.slice(0, 7)}\`\n\n` +
|
|
99
|
+
`Please fix the deployment issues and move the issue back to Finished to retry QA.`);
|
|
100
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'deployment_failed' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
issueLog.warn('Deployment check failed, proceeding with QA', { error: err });
|
|
105
|
+
}
|
|
106
|
+
await clearQAFailed(issueId);
|
|
107
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
108
|
+
// Detect parent issues → qa-coordination
|
|
109
|
+
let qaWorkType = 'qa';
|
|
110
|
+
let qaPrompt = `QA ${issueIdentifier}`;
|
|
111
|
+
try {
|
|
112
|
+
const isParent = await linearClient.isParentIssue(issueId);
|
|
113
|
+
if (isParent) {
|
|
114
|
+
qaWorkType = 'qa-coordination';
|
|
115
|
+
qaPrompt = config.generatePrompt(issueIdentifier, 'qa-coordination');
|
|
116
|
+
issueLog.info('Parent issue detected, using qa-coordination work type');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
issueLog.warn('Failed to detect parent issue for QA routing', { error: err });
|
|
121
|
+
}
|
|
122
|
+
// Create Linear AgentSession for QA
|
|
123
|
+
let qaSessionId;
|
|
124
|
+
try {
|
|
125
|
+
const appUrl = getAppUrl(config);
|
|
126
|
+
const sessionResult = await linearClient.createAgentSessionOnIssue({
|
|
127
|
+
issueId,
|
|
128
|
+
externalUrls: [{ label: 'Agent Dashboard', url: `${appUrl}/sessions/pending` }],
|
|
129
|
+
});
|
|
130
|
+
if (!sessionResult.success || !sessionResult.sessionId) {
|
|
131
|
+
issueLog.error('Failed to create Linear AgentSession for QA', { sessionResult });
|
|
132
|
+
return NextResponse.json({ success: false, error: 'Failed to create agent session for QA' });
|
|
133
|
+
}
|
|
134
|
+
qaSessionId = sessionResult.sessionId;
|
|
135
|
+
issueLog.info('Linear AgentSession created for QA', { sessionId: qaSessionId });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
issueLog.error('Error creating Linear AgentSession for QA', { error: err });
|
|
139
|
+
return NextResponse.json({ success: false, error: 'Error creating agent session for QA' });
|
|
140
|
+
}
|
|
141
|
+
await recordQAAttempt(issueId, qaSessionId);
|
|
142
|
+
await storeSessionState(qaSessionId, {
|
|
143
|
+
issueId,
|
|
144
|
+
issueIdentifier,
|
|
145
|
+
claudeSessionId: null,
|
|
146
|
+
worktreePath: '',
|
|
147
|
+
status: 'pending',
|
|
148
|
+
queuedAt: Date.now(),
|
|
149
|
+
promptContext: qaPrompt,
|
|
150
|
+
priority: 2,
|
|
151
|
+
organizationId: payload.organizationId,
|
|
152
|
+
workType: qaWorkType,
|
|
153
|
+
});
|
|
154
|
+
const qaWork = {
|
|
155
|
+
sessionId: qaSessionId,
|
|
156
|
+
issueId,
|
|
157
|
+
issueIdentifier,
|
|
158
|
+
priority: 2,
|
|
159
|
+
queuedAt: Date.now(),
|
|
160
|
+
prompt: qaPrompt,
|
|
161
|
+
workType: qaWorkType,
|
|
162
|
+
};
|
|
163
|
+
const qaResult = await dispatchWork(qaWork);
|
|
164
|
+
if (qaResult.dispatched || qaResult.parked) {
|
|
165
|
+
issueLog.info('QA work dispatched', {
|
|
166
|
+
sessionId: qaSessionId,
|
|
167
|
+
attemptNumber: attemptCount + 1,
|
|
168
|
+
});
|
|
169
|
+
try {
|
|
170
|
+
const appUrl = getAppUrl(config);
|
|
171
|
+
await linearClient.updateAgentSession({
|
|
172
|
+
sessionId: qaSessionId,
|
|
173
|
+
externalUrls: [{ label: 'Agent Dashboard', url: `${appUrl}/sessions/${qaSessionId}` }],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
issueLog.warn('Failed to update QA session externalUrl', { error: err });
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
await emitActivity(linearClient, qaSessionId, 'thought', `QA work queued (attempt #${attemptCount + 1}). Waiting for an available worker...`);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
issueLog.warn('Failed to emit QA queued activity', { error: err });
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
await linearClient.createComment(issueId, `## Automated QA Started\n\nQA attempt #${attemptCount + 1} has been queued.\n\nThe QA agent will:\n1. Checkout the PR branch\n2. Run tests and validation\n3. Verify implementation against requirements\n4. Update status to Delivered (pass) or Backlog (fail)`);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
issueLog.error('Failed to post QA start comment', { error: err });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
issueLog.error('Failed to queue QA work');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// === Handle Icebox → Backlog transition (auto-development) ===
|
|
197
|
+
if (currentStateName === 'Backlog' && updatedFrom?.stateId) {
|
|
198
|
+
const previousStateName = await resolveStateName(config, payload.organizationId, issueId, updatedFrom.stateId);
|
|
199
|
+
if (previousStateName !== 'Icebox') {
|
|
200
|
+
issueLog.debug('Issue transitioned to Backlog but not from Icebox', { previousStateName });
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
issueLog.info('Issue transitioned from Icebox to Backlog', {
|
|
204
|
+
previousStateName,
|
|
205
|
+
actorName: actor?.name,
|
|
206
|
+
});
|
|
207
|
+
const existingSession = await getSessionStateByIssue(issueId);
|
|
208
|
+
if (existingSession && ['running', 'claimed', 'pending'].includes(existingSession.status)) {
|
|
209
|
+
issueLog.info('Session already active, skipping development trigger');
|
|
210
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'session_already_active' });
|
|
211
|
+
}
|
|
212
|
+
if (await didJustQueueDevelopment(issueId)) {
|
|
213
|
+
issueLog.info('Issue in development cooldown period, skipping');
|
|
214
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'development_cooldown' });
|
|
215
|
+
}
|
|
216
|
+
const idempotencyKey = generateIdempotencyKey(webhookId, `dev:${issueId}:${Date.now()}`);
|
|
217
|
+
if (await isWebhookProcessed(idempotencyKey)) {
|
|
218
|
+
issueLog.info('Duplicate development trigger ignored', { idempotencyKey });
|
|
219
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'duplicate_dev_trigger' });
|
|
220
|
+
}
|
|
221
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
222
|
+
let devSessionId;
|
|
223
|
+
try {
|
|
224
|
+
const appUrl = getAppUrl(config);
|
|
225
|
+
const sessionResult = await linearClient.createAgentSessionOnIssue({
|
|
226
|
+
issueId,
|
|
227
|
+
externalUrls: [{ label: 'Agent Dashboard', url: `${appUrl}/sessions/pending` }],
|
|
228
|
+
});
|
|
229
|
+
if (!sessionResult.success || !sessionResult.sessionId) {
|
|
230
|
+
issueLog.error('Failed to create Linear AgentSession', { sessionResult });
|
|
231
|
+
return NextResponse.json({ success: false, error: 'Failed to create agent session' });
|
|
232
|
+
}
|
|
233
|
+
devSessionId = sessionResult.sessionId;
|
|
234
|
+
issueLog.info('Linear AgentSession created', { sessionId: devSessionId });
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
issueLog.error('Error creating Linear AgentSession', { error: err });
|
|
238
|
+
return NextResponse.json({ success: false, error: 'Error creating agent session' });
|
|
239
|
+
}
|
|
240
|
+
await markDevelopmentQueued(issueId);
|
|
241
|
+
// Auto-detect parent for coordination
|
|
242
|
+
let workType = 'development';
|
|
243
|
+
try {
|
|
244
|
+
const isParent = await linearClient.isParentIssue(issueId);
|
|
245
|
+
if (isParent) {
|
|
246
|
+
workType = 'coordination';
|
|
247
|
+
issueLog.info('Parent issue detected, switching to coordination work type');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
issueLog.warn('Failed to check if issue is parent', { error: err });
|
|
252
|
+
}
|
|
253
|
+
const prompt = config.generatePrompt(issueIdentifier, workType);
|
|
254
|
+
await storeSessionState(devSessionId, {
|
|
255
|
+
issueId,
|
|
256
|
+
issueIdentifier,
|
|
257
|
+
claudeSessionId: null,
|
|
258
|
+
worktreePath: '',
|
|
259
|
+
status: 'pending',
|
|
260
|
+
queuedAt: Date.now(),
|
|
261
|
+
promptContext: prompt,
|
|
262
|
+
priority: 3,
|
|
263
|
+
organizationId: payload.organizationId,
|
|
264
|
+
workType,
|
|
265
|
+
});
|
|
266
|
+
const devWork = {
|
|
267
|
+
sessionId: devSessionId,
|
|
268
|
+
issueId,
|
|
269
|
+
issueIdentifier,
|
|
270
|
+
priority: 3,
|
|
271
|
+
queuedAt: Date.now(),
|
|
272
|
+
prompt,
|
|
273
|
+
workType,
|
|
274
|
+
};
|
|
275
|
+
const devResult = await dispatchWork(devWork);
|
|
276
|
+
if (devResult.dispatched || devResult.parked) {
|
|
277
|
+
issueLog.info('Development work dispatched', { sessionId: devSessionId });
|
|
278
|
+
try {
|
|
279
|
+
const appUrl = getAppUrl(config);
|
|
280
|
+
await linearClient.updateAgentSession({
|
|
281
|
+
sessionId: devSessionId,
|
|
282
|
+
externalUrls: [{ label: 'Agent Dashboard', url: `${appUrl}/sessions/${devSessionId}` }],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
issueLog.warn('Failed to update session externalUrl', { error: err });
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
await emitActivity(linearClient, devSessionId, 'thought', 'Development work queued. Waiting for an available worker...');
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
issueLog.warn('Failed to emit queued activity', { error: err });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
issueLog.error('Failed to queue development work');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// === Handle Finished → Delivered transition (auto-acceptance) ===
|
|
301
|
+
if (currentStateName === 'Delivered' && updatedFrom?.stateId) {
|
|
302
|
+
const previousStateName = await resolveStateName(config, payload.organizationId, issueId, updatedFrom.stateId);
|
|
303
|
+
if (previousStateName !== 'Finished') {
|
|
304
|
+
issueLog.debug('Issue transitioned to Delivered but not from Finished', { previousStateName });
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
issueLog.info('Issue transitioned from Finished to Delivered', {
|
|
308
|
+
previousStateName,
|
|
309
|
+
actorName: actor?.name,
|
|
310
|
+
});
|
|
311
|
+
// Skip acceptance for sub-issues
|
|
312
|
+
let isChildForAcceptance = !!(data.parent);
|
|
313
|
+
if (!isChildForAcceptance) {
|
|
314
|
+
try {
|
|
315
|
+
const checkClient = await config.linearClient.getClient(payload.organizationId);
|
|
316
|
+
isChildForAcceptance = await checkClient.isChildIssue(issueId);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
issueLog.warn('Failed to check if issue is a child', { error: err });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (isChildForAcceptance) {
|
|
323
|
+
issueLog.info('Sub-issue detected, skipping individual acceptance trigger');
|
|
324
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'sub_issue_skipped' });
|
|
325
|
+
}
|
|
326
|
+
if (!autoTrigger?.enableAutoAcceptance) {
|
|
327
|
+
issueLog.debug('Auto-acceptance disabled, skipping acceptance trigger');
|
|
328
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'auto_acceptance_disabled' });
|
|
329
|
+
}
|
|
330
|
+
const projectName = data.project?.name;
|
|
331
|
+
if (!isProjectAllowed(projectName, autoTrigger.autoAcceptanceProjects)) {
|
|
332
|
+
issueLog.debug('Project not in auto-acceptance list, skipping', { projectName });
|
|
333
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'project_not_allowed' });
|
|
334
|
+
}
|
|
335
|
+
const labels = data.labels;
|
|
336
|
+
if (hasExcludedLabel(labels, autoTrigger.autoAcceptanceExcludeLabels)) {
|
|
337
|
+
issueLog.debug('Issue has excluded label, skipping acceptance trigger');
|
|
338
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'excluded_label' });
|
|
339
|
+
}
|
|
340
|
+
if (autoTrigger.autoAcceptanceRequireAgentWorked) {
|
|
341
|
+
const workRecord = await wasAgentWorked(issueId);
|
|
342
|
+
if (!workRecord) {
|
|
343
|
+
issueLog.debug('Issue not worked by agent, skipping acceptance trigger');
|
|
344
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'not_agent_worked' });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (await didJustQueueAcceptance(issueId)) {
|
|
348
|
+
issueLog.info('Issue in acceptance cooldown period, skipping');
|
|
349
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'acceptance_cooldown' });
|
|
350
|
+
}
|
|
351
|
+
const idempotencyKey = generateIdempotencyKey(webhookId, `acceptance:${issueId}:${Date.now()}`);
|
|
352
|
+
if (await isWebhookProcessed(idempotencyKey)) {
|
|
353
|
+
issueLog.info('Duplicate acceptance trigger ignored', { idempotencyKey });
|
|
354
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'duplicate_acceptance_trigger' });
|
|
355
|
+
}
|
|
356
|
+
// Check deployment status
|
|
357
|
+
try {
|
|
358
|
+
const deploymentResult = await checkIssueDeploymentStatus(issueIdentifier);
|
|
359
|
+
if (deploymentResult?.anyFailed) {
|
|
360
|
+
issueLog.info('Deployment failed, blocking acceptance');
|
|
361
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
362
|
+
await linearClient.createComment(issueId, `## Acceptance Blocked: Deployment Failed\n\n` +
|
|
363
|
+
`Cannot proceed with acceptance testing until Vercel deployment succeeds.\n\n` +
|
|
364
|
+
formatFailedDeployments(deploymentResult) +
|
|
365
|
+
`\n\n**PR:** ${deploymentResult.pr.url}\n` +
|
|
366
|
+
`**Commit:** \`${deploymentResult.commitSha.slice(0, 7)}\`\n\n` +
|
|
367
|
+
`Please fix the deployment issues. The issue will remain in Delivered status.`);
|
|
368
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'deployment_failed' });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
issueLog.warn('Deployment check failed, proceeding with acceptance', { error: err });
|
|
373
|
+
}
|
|
374
|
+
await markAcceptanceQueued(issueId);
|
|
375
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
376
|
+
// Detect parent → acceptance-coordination
|
|
377
|
+
let acceptanceWorkType = 'acceptance';
|
|
378
|
+
let acceptancePrompt = config.generatePrompt(issueIdentifier, 'acceptance');
|
|
379
|
+
try {
|
|
380
|
+
const isParent = await linearClient.isParentIssue(issueId);
|
|
381
|
+
if (isParent) {
|
|
382
|
+
acceptanceWorkType = 'acceptance-coordination';
|
|
383
|
+
acceptancePrompt = config.generatePrompt(issueIdentifier, 'acceptance-coordination');
|
|
384
|
+
issueLog.info('Parent issue detected, using acceptance-coordination work type');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
issueLog.warn('Failed to detect parent issue for acceptance routing', { error: err });
|
|
389
|
+
}
|
|
390
|
+
// Create Linear AgentSession for acceptance
|
|
391
|
+
let acceptanceSessionId;
|
|
392
|
+
try {
|
|
393
|
+
const appUrl = getAppUrl(config);
|
|
394
|
+
const sessionResult = await linearClient.createAgentSessionOnIssue({
|
|
395
|
+
issueId,
|
|
396
|
+
externalUrls: [{ label: 'Agent Dashboard', url: `${appUrl}/sessions/pending` }],
|
|
397
|
+
});
|
|
398
|
+
if (!sessionResult.success || !sessionResult.sessionId) {
|
|
399
|
+
issueLog.error('Failed to create Linear AgentSession for acceptance', { sessionResult });
|
|
400
|
+
return NextResponse.json({ success: false, error: 'Failed to create agent session for acceptance' });
|
|
401
|
+
}
|
|
402
|
+
acceptanceSessionId = sessionResult.sessionId;
|
|
403
|
+
issueLog.info('Linear AgentSession created for acceptance', { sessionId: acceptanceSessionId });
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
issueLog.error('Error creating Linear AgentSession for acceptance', { error: err });
|
|
407
|
+
return NextResponse.json({ success: false, error: 'Error creating agent session for acceptance' });
|
|
408
|
+
}
|
|
409
|
+
await storeSessionState(acceptanceSessionId, {
|
|
410
|
+
issueId,
|
|
411
|
+
issueIdentifier,
|
|
412
|
+
claudeSessionId: null,
|
|
413
|
+
worktreePath: '',
|
|
414
|
+
status: 'pending',
|
|
415
|
+
queuedAt: Date.now(),
|
|
416
|
+
promptContext: acceptancePrompt,
|
|
417
|
+
priority: 2,
|
|
418
|
+
organizationId: payload.organizationId,
|
|
419
|
+
workType: acceptanceWorkType,
|
|
420
|
+
});
|
|
421
|
+
const acceptanceWork = {
|
|
422
|
+
sessionId: acceptanceSessionId,
|
|
423
|
+
issueId,
|
|
424
|
+
issueIdentifier,
|
|
425
|
+
priority: 2,
|
|
426
|
+
queuedAt: Date.now(),
|
|
427
|
+
prompt: acceptancePrompt,
|
|
428
|
+
workType: acceptanceWorkType,
|
|
429
|
+
};
|
|
430
|
+
const accResult = await dispatchWork(acceptanceWork);
|
|
431
|
+
if (accResult.dispatched || accResult.parked) {
|
|
432
|
+
issueLog.info('Acceptance work dispatched', { sessionId: acceptanceSessionId });
|
|
433
|
+
try {
|
|
434
|
+
const appUrl = getAppUrl(config);
|
|
435
|
+
await linearClient.updateAgentSession({
|
|
436
|
+
sessionId: acceptanceSessionId,
|
|
437
|
+
externalUrls: [{ label: 'Agent Dashboard', url: `${appUrl}/sessions/${acceptanceSessionId}` }],
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
issueLog.warn('Failed to update acceptance session externalUrl', { error: err });
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
await emitActivity(linearClient, acceptanceSessionId, 'thought', 'Acceptance work queued. Waiting for an available worker...');
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
issueLog.warn('Failed to emit acceptance queued activity', { error: err });
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
await linearClient.createComment(issueId, `## Acceptance Processing Started\n\nQA passed. Validating work completion and preparing to merge PR...\n\nThe acceptance handler will:\n1. Verify the preview deployment is working\n2. Check PR is ready to merge (CI passing, no conflicts)\n3. Merge the PR\n4. Clean up local resources\n5. Move issue to Accepted on success`);
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
issueLog.error('Failed to post acceptance start comment', { error: err });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
issueLog.error('Failed to queue acceptance work');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return null; // Continue processing
|
|
462
|
+
}
|
|
@@ -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 '@supaku/agentfactory-linear';
|
|
6
|
+
import type { WebhookConfig } from '../../types.js';
|
|
7
|
+
import type { createLogger } from '@supaku/agentfactory-server';
|
|
8
|
+
export declare function handleSessionCreated(config: WebhookConfig, 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,6BAA6B,CAAA;AAiBtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAQnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAE/D,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,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,CA0Q9B"}
|