@supaku/agentfactory-nextjs 0.7.8 → 0.7.10
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/dist/src/orchestrator/webhook-orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/webhook-orchestrator.js +53 -1
- package/dist/src/types.d.ts +5 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/webhook/handlers/issue-updated.d.ts +3 -2
- package/dist/src/webhook/handlers/issue-updated.d.ts.map +1 -1
- package/dist/src/webhook/handlers/issue-updated.js +217 -10
- package/dist/src/webhook/handlers/session-created.d.ts.map +1 -1
- package/dist/src/webhook/handlers/session-created.js +68 -3
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-orchestrator.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/webhook-orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"webhook-orchestrator.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/webhook-orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoCH,OAAO,KAAK,EACV,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,EAC5B,MAAM,YAAY,CAAA;AAyFnB;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,CAAC,EAAE,yBAAyB,EAClC,KAAK,CAAC,EAAE,wBAAwB,GAC/B,2BAA2B,CAwS7B"}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createOrchestrator, } from '@supaku/agentfactory';
|
|
12
12
|
import { withRetry, AgentSpawnError, isRetryableError, createAgentSession, createLinearAgentClient, } from '@supaku/agentfactory-linear';
|
|
13
|
-
import { createLogger, generateIdempotencyKey, isWebhookProcessed, markWebhookProcessed, unmarkWebhookProcessed, storeSessionState, getSessionState, updateClaudeSessionId, updateSessionStatus, updateSessionCostData, } from '@supaku/agentfactory-server';
|
|
13
|
+
import { createLogger, generateIdempotencyKey, isWebhookProcessed, markWebhookProcessed, unmarkWebhookProcessed, storeSessionState, getSessionState, updateClaudeSessionId, updateSessionStatus, updateSessionCostData, updateWorkflowState, recordPhaseAttempt, incrementCycleCount, appendFailureSummary, clearWorkflowState, extractFailureReason, } from '@supaku/agentfactory-server';
|
|
14
14
|
import { formatErrorForComment } from './error-formatting.js';
|
|
15
15
|
const log = createLogger('webhook-orchestrator');
|
|
16
16
|
const DEFAULT_RETRY_CONFIG = {
|
|
@@ -143,6 +143,58 @@ export function createWebhookOrchestrator(config, hooks) {
|
|
|
143
143
|
outputTokens: agent.outputTokens,
|
|
144
144
|
}).catch((err) => log.error('Failed to persist cost data', { error: err }));
|
|
145
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') {
|
|
175
|
+
const state = await incrementCycleCount(agent.issueId);
|
|
176
|
+
const failureReason = extractFailureReason(agent.resultMessage);
|
|
177
|
+
const formattedFailure = `--- Cycle ${state.cycleCount}, ${phase} (${new Date().toISOString()}) ---\n${failureReason}`;
|
|
178
|
+
await appendFailureSummary(agent.issueId, formattedFailure);
|
|
179
|
+
log.info('Workflow state updated after failure', {
|
|
180
|
+
issueId: agent.issueId,
|
|
181
|
+
cycleCount: state.cycleCount,
|
|
182
|
+
strategy: state.strategy,
|
|
183
|
+
phase,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// On acceptance pass: clear workflow state (issue is done)
|
|
187
|
+
if (phase === 'acceptance' && agent.workResult === 'passed') {
|
|
188
|
+
await clearWorkflowState(agent.issueId);
|
|
189
|
+
log.info('Workflow state cleared after acceptance pass', {
|
|
190
|
+
issueId: agent.issueId,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
log.error('Failed to update workflow state', { error: err, issueId: agent.issueId });
|
|
197
|
+
}
|
|
146
198
|
try {
|
|
147
199
|
await hooks?.onAgentComplete?.(agent);
|
|
148
200
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -49,6 +49,11 @@ export interface WebhookConfig extends RouteConfig {
|
|
|
49
49
|
buildParentAcceptanceContext?: (identifier: string, subIssues: SubIssueStatus[]) => string;
|
|
50
50
|
/** Linear project names this server handles. Empty/undefined = all projects. */
|
|
51
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;
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
59
|
* Resolved webhook config with all defaults applied.
|
package/dist/src/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAEnG;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB,CAAA;CACnF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,oBAAoB,CAAA;IAClC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,OAAO,CAAA;IACrB,oBAAoB,EAAE,OAAO,CAAA;IAC7B,wBAAwB,EAAE,OAAO,CAAA;IACjC,gCAAgC,EAAE,OAAO,CAAA;IACzC,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,sBAAsB,EAAE,MAAM,EAAE,CAAA;IAChC,mBAAmB,EAAE,MAAM,EAAE,CAAA;IAC7B,2BAA2B,EAAE,MAAM,EAAE,CAAA;CACtC;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IACjG,wBAAwB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,aAAa,GAAG,SAAS,CAAA;IACzG,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,MAAM,CAAA;IACjD,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,MAAM,CAAA;IAClF,4BAA4B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,MAAM,CAAA;IAC1F,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAEnG;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB,CAAA;CACnF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,oBAAoB,CAAA;IAClC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,OAAO,CAAA;IACrB,oBAAoB,EAAE,OAAO,CAAA;IAC7B,wBAAwB,EAAE,OAAO,CAAA;IACjC,gCAAgC,EAAE,OAAO,CAAA;IACzC,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,sBAAsB,EAAE,MAAM,EAAE,CAAA;IAChC,mBAAmB,EAAE,MAAM,EAAE,CAAA;IAC7B,2BAA2B,EAAE,MAAM,EAAE,CAAA;CACtC;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IACjG,wBAAwB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,aAAa,GAAG,SAAS,CAAA;IACzG,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,MAAM,CAAA;IACjD,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,MAAM,CAAA;IAClF,4BAA4B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,MAAM,CAAA;IAC1F,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAsB,SAAQ,WAAW;IACxD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IAChG,wBAAwB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,aAAa,GAAG,SAAS,CAAA;IACzG,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,MAAM,CAAA;IACjD,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,MAAM,CAAA;IAClF,4BAA4B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,MAAM,CAAA;IAC1F,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;GAMG;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA"}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Handle Issue update events — status transition triggers.
|
|
3
3
|
*
|
|
4
4
|
* Handles:
|
|
5
|
-
* - Finished → auto-QA trigger
|
|
6
|
-
* -
|
|
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)
|
|
7
8
|
* - Finished → Delivered → auto-acceptance trigger
|
|
8
9
|
*/
|
|
9
10
|
import { NextResponse } from 'next/server';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issue-updated.d.ts","sourceRoot":"","sources":["../../../../src/webhook/handlers/issue-updated.ts"],"names":[],"mappings":"AAAA
|
|
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,6BAA6B,CAAA;AAuBtF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAQ3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAE/D,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,CA2vB9B"}
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
* Handle Issue update events — status transition triggers.
|
|
3
3
|
*
|
|
4
4
|
* Handles:
|
|
5
|
-
* - Finished → auto-QA trigger
|
|
6
|
-
* -
|
|
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)
|
|
7
8
|
* - Finished → Delivered → auto-acceptance trigger
|
|
8
9
|
*/
|
|
9
10
|
import { NextResponse } from 'next/server';
|
|
11
|
+
import { buildFailureContextBlock } from '@supaku/agentfactory-linear';
|
|
10
12
|
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';
|
|
13
|
+
import { generateIdempotencyKey, isWebhookProcessed, storeSessionState, getSessionStateByIssue, dispatchWork, wasAgentWorked, didJustFailQA, getQAAttemptCount, recordQAAttempt, clearQAFailed, didJustQueueDevelopment, markDevelopmentQueued, didJustQueueAcceptance, markAcceptanceQueued, getWorkflowState, } from '@supaku/agentfactory-server';
|
|
12
14
|
import { emitActivity, resolveStateName, isProjectAllowed, hasExcludedLabel, getAppUrl, } from '../utils.js';
|
|
13
15
|
export async function handleIssueUpdated(config, payload, log) {
|
|
14
16
|
const { data, updatedFrom, actor } = payload;
|
|
@@ -71,6 +73,20 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
71
73
|
issueLog.info('Issue in QA cooldown period, skipping');
|
|
72
74
|
return NextResponse.json({ success: true, skipped: true, reason: 'qa_cooldown' });
|
|
73
75
|
}
|
|
76
|
+
// Check workflow state for circuit breaker before QA
|
|
77
|
+
try {
|
|
78
|
+
const workflowState = await getWorkflowState(issueId);
|
|
79
|
+
if (workflowState?.strategy === 'escalate-human') {
|
|
80
|
+
issueLog.warn('Circuit breaker: escalate-human strategy, blocking QA', {
|
|
81
|
+
cycleCount: workflowState.cycleCount,
|
|
82
|
+
strategy: workflowState.strategy,
|
|
83
|
+
});
|
|
84
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'circuit_breaker_escalate_human' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
issueLog.warn('Failed to check workflow state for circuit breaker', { error: err });
|
|
89
|
+
}
|
|
74
90
|
const attemptCount = await getQAAttemptCount(issueId);
|
|
75
91
|
if (attemptCount >= 3) {
|
|
76
92
|
issueLog.warn('QA attempt limit reached', { attemptCount });
|
|
@@ -124,6 +140,31 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
124
140
|
catch (err) {
|
|
125
141
|
issueLog.warn('Failed to detect parent issue for QA routing', { error: err });
|
|
126
142
|
}
|
|
143
|
+
// Enrich QA prompt with previous failure context
|
|
144
|
+
if (attemptCount > 0) {
|
|
145
|
+
try {
|
|
146
|
+
const workflowState = await getWorkflowState(issueId);
|
|
147
|
+
if (workflowState && workflowState.failureSummary) {
|
|
148
|
+
const wfContext = {
|
|
149
|
+
cycleCount: workflowState.cycleCount,
|
|
150
|
+
strategy: workflowState.strategy,
|
|
151
|
+
failureSummary: workflowState.failureSummary,
|
|
152
|
+
qaAttemptCount: attemptCount,
|
|
153
|
+
};
|
|
154
|
+
const contextBlock = buildFailureContextBlock(qaWorkType, wfContext);
|
|
155
|
+
if (contextBlock) {
|
|
156
|
+
qaPrompt += contextBlock;
|
|
157
|
+
issueLog.info('QA prompt enriched with failure context', {
|
|
158
|
+
cycleCount: workflowState.cycleCount,
|
|
159
|
+
attemptCount,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
issueLog.warn('Failed to enrich QA prompt with failure context', { error: err });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
127
168
|
// Create Linear AgentSession for QA
|
|
128
169
|
let qaSessionId;
|
|
129
170
|
try {
|
|
@@ -200,17 +241,155 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
200
241
|
issueLog.error('Failed to queue QA work');
|
|
201
242
|
}
|
|
202
243
|
}
|
|
203
|
-
// === Handle
|
|
244
|
+
// === Handle → Rejected transition (escalation ladder) ===
|
|
245
|
+
// When QA/acceptance fails, the orchestrator transitions the issue to Rejected.
|
|
246
|
+
// Check the escalation strategy and act accordingly.
|
|
247
|
+
if (currentStateName === 'Rejected' && updatedFrom?.stateId) {
|
|
248
|
+
try {
|
|
249
|
+
const workflowState = await getWorkflowState(issueId);
|
|
250
|
+
if (workflowState) {
|
|
251
|
+
const { strategy, cycleCount, failureSummary } = workflowState;
|
|
252
|
+
if (strategy === 'escalate-human') {
|
|
253
|
+
issueLog.warn('Escalation ladder: escalate-human — creating blocker and stopping loop', {
|
|
254
|
+
cycleCount,
|
|
255
|
+
strategy,
|
|
256
|
+
});
|
|
257
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
258
|
+
// Post escalation summary comment
|
|
259
|
+
const totalCostLine = workflowState.phases
|
|
260
|
+
? (() => {
|
|
261
|
+
const allPhases = [
|
|
262
|
+
...workflowState.phases.development,
|
|
263
|
+
...workflowState.phases.qa,
|
|
264
|
+
...workflowState.phases.refinement,
|
|
265
|
+
...workflowState.phases.acceptance,
|
|
266
|
+
];
|
|
267
|
+
const totalCost = allPhases.reduce((sum, p) => sum + (p.costUsd ?? 0), 0);
|
|
268
|
+
return totalCost > 0 ? `\n**Total cost across all attempts:** $${totalCost.toFixed(2)}` : '';
|
|
269
|
+
})()
|
|
270
|
+
: '';
|
|
271
|
+
try {
|
|
272
|
+
await linearClient.createComment(issueId, `## Circuit Breaker: Human Intervention Required\n\n` +
|
|
273
|
+
`This issue has gone through **${cycleCount} dev-QA-rejected cycles** without passing.\n` +
|
|
274
|
+
`The automated system is stopping further attempts.\n` +
|
|
275
|
+
totalCostLine +
|
|
276
|
+
`\n\n### Failure History\n\n${failureSummary ?? 'No failure details recorded.'}\n\n` +
|
|
277
|
+
`### Recommended Actions\n` +
|
|
278
|
+
`1. Review the failure patterns above\n` +
|
|
279
|
+
`2. Consider if the acceptance criteria need clarification\n` +
|
|
280
|
+
`3. Investigate whether there's an architectural issue\n` +
|
|
281
|
+
`4. Manually fix or decompose the issue before re-enabling automation`);
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
issueLog.error('Failed to post escalation comment', { error: err });
|
|
285
|
+
}
|
|
286
|
+
// Create a blocker issue in Icebox with 'Needs Human' label
|
|
287
|
+
try {
|
|
288
|
+
const issue = await linearClient.getIssue(issueId);
|
|
289
|
+
const team = await issue.team;
|
|
290
|
+
if (team) {
|
|
291
|
+
const statuses = await linearClient.getTeamStatuses(team.id);
|
|
292
|
+
const iceboxStateId = statuses['Icebox'];
|
|
293
|
+
// Find 'Needs Human' label
|
|
294
|
+
const allLabels = await linearClient.linearClient.issueLabels();
|
|
295
|
+
const needsHumanLabel = allLabels.nodes.find((l) => l.name.toLowerCase() === 'needs human');
|
|
296
|
+
const blockerTitle = `Human review needed: ${issueIdentifier} failed ${cycleCount} automated cycles`;
|
|
297
|
+
const blockerDescription = [
|
|
298
|
+
`This issue has failed **${cycleCount} automated dev-QA-rejected cycles** and requires human intervention.`,
|
|
299
|
+
'',
|
|
300
|
+
'### Failure History',
|
|
301
|
+
failureSummary ?? 'No failure details recorded.',
|
|
302
|
+
'',
|
|
303
|
+
'---',
|
|
304
|
+
`*Source issue: ${issueIdentifier}*`,
|
|
305
|
+
].join('\n');
|
|
306
|
+
const createPayload = {
|
|
307
|
+
title: blockerTitle,
|
|
308
|
+
description: blockerDescription,
|
|
309
|
+
teamId: team.id,
|
|
310
|
+
...(iceboxStateId && { stateId: iceboxStateId }),
|
|
311
|
+
...(needsHumanLabel && { labelIds: [needsHumanLabel.id] }),
|
|
312
|
+
};
|
|
313
|
+
// Add project if available
|
|
314
|
+
const project = await issue.project;
|
|
315
|
+
if (project) {
|
|
316
|
+
createPayload.projectId = project.id;
|
|
317
|
+
}
|
|
318
|
+
const blockerIssue = await linearClient.createIssue(createPayload);
|
|
319
|
+
// Create blocking relation: blocker blocks source issue
|
|
320
|
+
await linearClient.createIssueRelation({
|
|
321
|
+
issueId: blockerIssue.id,
|
|
322
|
+
relatedIssueId: issueId,
|
|
323
|
+
type: 'blocks',
|
|
324
|
+
});
|
|
325
|
+
issueLog.info('Escalation ladder: blocker issue created', {
|
|
326
|
+
issueId,
|
|
327
|
+
blockerIssueId: blockerIssue.id,
|
|
328
|
+
blockerIdentifier: blockerIssue.identifier,
|
|
329
|
+
cycleCount,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
issueLog.warn('Escalation ladder: could not resolve team for blocker creation', { issueId });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
issueLog.error('Failed to create blocker issue for escalation', { error: err });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else if (strategy === 'decompose') {
|
|
341
|
+
issueLog.info('Escalation ladder: decompose strategy — refinement will attempt decomposition', {
|
|
342
|
+
cycleCount,
|
|
343
|
+
strategy,
|
|
344
|
+
});
|
|
345
|
+
// The decomposition strategy will be handled via prompt enrichment (SUP-713)
|
|
346
|
+
// by injecting decomposition instructions into the refinement prompt
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
issueLog.warn('Failed to check workflow state for escalation ladder', { error: err });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// === Handle → Backlog transition (auto-development) ===
|
|
355
|
+
// Triggers from: Icebox → Backlog (new issues), Rejected → Backlog (post-refinement retries), etc.
|
|
204
356
|
if (currentStateName === 'Backlog' && updatedFrom?.stateId) {
|
|
205
357
|
const previousStateName = await resolveStateName(config, payload.organizationId, issueId, updatedFrom.stateId);
|
|
206
|
-
|
|
207
|
-
|
|
358
|
+
// Skip transitions from states that don't indicate readiness for development
|
|
359
|
+
// (e.g., Backlog → Backlog is a no-op, Started → Backlog means work was abandoned)
|
|
360
|
+
const allowedPreviousStates = ['Icebox', 'Rejected', 'Canceled'];
|
|
361
|
+
if (!allowedPreviousStates.includes(previousStateName ?? '')) {
|
|
362
|
+
issueLog.debug('Issue transitioned to Backlog from non-triggering state', { previousStateName });
|
|
208
363
|
}
|
|
209
364
|
else {
|
|
210
|
-
|
|
365
|
+
const isRetry = previousStateName === 'Rejected';
|
|
366
|
+
issueLog.info('Issue transitioned to Backlog', {
|
|
211
367
|
previousStateName,
|
|
368
|
+
isRetry,
|
|
212
369
|
actorName: actor?.name,
|
|
213
370
|
});
|
|
371
|
+
// Circuit breaker: check workflow state for escalate-human strategy
|
|
372
|
+
if (isRetry) {
|
|
373
|
+
try {
|
|
374
|
+
const workflowState = await getWorkflowState(issueId);
|
|
375
|
+
if (workflowState && workflowState.strategy === 'escalate-human') {
|
|
376
|
+
issueLog.warn('Circuit breaker: issue at escalate-human strategy, skipping auto-development', {
|
|
377
|
+
cycleCount: workflowState.cycleCount,
|
|
378
|
+
strategy: workflowState.strategy,
|
|
379
|
+
});
|
|
380
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'circuit_breaker_escalate_human' });
|
|
381
|
+
}
|
|
382
|
+
if (workflowState) {
|
|
383
|
+
issueLog.info('Workflow state found for retry', {
|
|
384
|
+
cycleCount: workflowState.cycleCount,
|
|
385
|
+
strategy: workflowState.strategy,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
issueLog.warn('Failed to check workflow state for circuit breaker', { error: err });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
214
393
|
const existingSession = await getSessionStateByIssue(issueId);
|
|
215
394
|
if (existingSession && ['running', 'claimed', 'pending'].includes(existingSession.status)) {
|
|
216
395
|
issueLog.info('Session already active, skipping development trigger');
|
|
@@ -257,7 +436,31 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
257
436
|
catch (err) {
|
|
258
437
|
issueLog.warn('Failed to check if issue is parent', { error: err });
|
|
259
438
|
}
|
|
260
|
-
|
|
439
|
+
let prompt = config.generatePrompt(issueIdentifier, workType);
|
|
440
|
+
// Enrich prompt with failure context for retries
|
|
441
|
+
if (isRetry) {
|
|
442
|
+
try {
|
|
443
|
+
const workflowState = await getWorkflowState(issueId);
|
|
444
|
+
if (workflowState && workflowState.cycleCount > 0) {
|
|
445
|
+
const wfContext = {
|
|
446
|
+
cycleCount: workflowState.cycleCount,
|
|
447
|
+
strategy: workflowState.strategy,
|
|
448
|
+
failureSummary: workflowState.failureSummary,
|
|
449
|
+
};
|
|
450
|
+
const contextBlock = buildFailureContextBlock(workType, wfContext);
|
|
451
|
+
if (contextBlock) {
|
|
452
|
+
prompt += contextBlock;
|
|
453
|
+
issueLog.info('Development prompt enriched with failure context', {
|
|
454
|
+
cycleCount: workflowState.cycleCount,
|
|
455
|
+
strategy: workflowState.strategy,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
issueLog.warn('Failed to enrich development prompt with failure context', { error: err });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
261
464
|
await storeSessionState(devSessionId, {
|
|
262
465
|
issueId,
|
|
263
466
|
issueIdentifier,
|
|
@@ -283,7 +486,8 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
283
486
|
};
|
|
284
487
|
const devResult = await dispatchWork(devWork);
|
|
285
488
|
if (devResult.dispatched || devResult.parked) {
|
|
286
|
-
|
|
489
|
+
const retryLabel = isRetry ? ' (retry)' : '';
|
|
490
|
+
issueLog.info(`Development work dispatched${retryLabel}`, { sessionId: devSessionId });
|
|
287
491
|
try {
|
|
288
492
|
const appUrl = getAppUrl(config);
|
|
289
493
|
await linearClient.updateAgentSession({
|
|
@@ -295,7 +499,10 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
295
499
|
issueLog.warn('Failed to update session externalUrl', { error: err });
|
|
296
500
|
}
|
|
297
501
|
try {
|
|
298
|
-
|
|
502
|
+
const activityMsg = isRetry
|
|
503
|
+
? 'Development work queued (retry after refinement). Waiting for an available worker...'
|
|
504
|
+
: 'Development work queued. Waiting for an available worker...';
|
|
505
|
+
await emitActivity(linearClient, devSessionId, 'thought', activityMsg);
|
|
299
506
|
}
|
|
300
507
|
catch (err) {
|
|
301
508
|
issueLog.warn('Failed to emit queued activity', { error: err });
|
|
@@ -1 +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;
|
|
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;AAoBtF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAS3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAE/D,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,CAyX9B"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Handle agent session 'create' events — new session initiated.
|
|
3
3
|
*/
|
|
4
4
|
import { NextResponse } from 'next/server';
|
|
5
|
-
import { TERMINAL_STATUSES, validateWorkTypeForStatus, WORK_TYPE_ALLOWED_STATUSES, STATUS_WORK_TYPE_MAP, getValidWorkTypesForStatus, } from '@supaku/agentfactory-linear';
|
|
6
|
-
import { generateIdempotencyKey, isWebhookProcessed, storeSessionState, getSessionState, updateSessionStatus, dispatchWork, } from '@supaku/agentfactory-server';
|
|
5
|
+
import { TERMINAL_STATUSES, validateWorkTypeForStatus, WORK_TYPE_ALLOWED_STATUSES, STATUS_WORK_TYPE_MAP, getValidWorkTypesForStatus, buildFailureContextBlock, } from '@supaku/agentfactory-linear';
|
|
6
|
+
import { generateIdempotencyKey, isWebhookProcessed, storeSessionState, getSessionState, updateSessionStatus, dispatchWork, getWorkflowState, } from '@supaku/agentfactory-server';
|
|
7
7
|
import { emitActivity, determineWorkType, isProjectAllowed, getAppUrl, getPriority, WORK_TYPE_MESSAGES, } from '../utils.js';
|
|
8
8
|
export async function handleSessionCreated(config, payload, rawPayload, log) {
|
|
9
9
|
const agentSession = rawPayload.agentSession;
|
|
@@ -194,6 +194,46 @@ export async function handleSessionCreated(config, payload, rawPayload, log) {
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
// Extract PR info for QA/acceptance agents so they know which PRs to validate
|
|
198
|
+
let prContext = '';
|
|
199
|
+
const needsPrContext = workType === 'qa' ||
|
|
200
|
+
workType === 'acceptance' ||
|
|
201
|
+
workType === 'qa-coordination' ||
|
|
202
|
+
workType === 'acceptance-coordination';
|
|
203
|
+
if (needsPrContext) {
|
|
204
|
+
try {
|
|
205
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
206
|
+
const issueForAttachments = await linearClient.getIssue(issueId);
|
|
207
|
+
const attachments = await issueForAttachments.attachments();
|
|
208
|
+
const prLinks = attachments.nodes
|
|
209
|
+
.filter((a) => a.url?.includes('github.com') && a.url?.includes('/pull/'))
|
|
210
|
+
.map((a) => {
|
|
211
|
+
const match = a.url.match(/\/pull\/(\d+)/);
|
|
212
|
+
return match ? { url: a.url, number: parseInt(match[1], 10), title: a.title ?? '' } : null;
|
|
213
|
+
})
|
|
214
|
+
.filter((pr) => pr !== null);
|
|
215
|
+
if (prLinks.length > 0) {
|
|
216
|
+
prContext = '\n\nLinked PRs:\n';
|
|
217
|
+
prContext += prLinks.map((pr) => `- PR #${pr.number}: ${pr.title} (${pr.url})`).join('\n');
|
|
218
|
+
if (prLinks.length > 1) {
|
|
219
|
+
prContext +=
|
|
220
|
+
'\n\nNote: Multiple PRs are linked. Check each PR state (open/merged/closed) and validate the most recent OPEN one.';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
sessionLog.debug('Extracted PR context for QA/acceptance', {
|
|
224
|
+
prLinksCount: prLinks.length,
|
|
225
|
+
hasPrContext: prContext.length > 0,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
sessionLog.warn('Failed to extract PR info from issue attachments', { error: err });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const enhancedPromptContext = prContext
|
|
233
|
+
? promptContext
|
|
234
|
+
? promptContext + prContext
|
|
235
|
+
: prContext
|
|
236
|
+
: promptContext;
|
|
197
237
|
const priority = getPriority(config, workType);
|
|
198
238
|
await storeSessionState(sessionId, {
|
|
199
239
|
issueId,
|
|
@@ -209,6 +249,31 @@ export async function handleSessionCreated(config, payload, rawPayload, log) {
|
|
|
209
249
|
agentId,
|
|
210
250
|
projectName,
|
|
211
251
|
});
|
|
252
|
+
// Enrich prompt with workflow failure context for retries (refinement, development)
|
|
253
|
+
let workflowContextBlock = '';
|
|
254
|
+
if (workType === 'refinement' || (workType === 'development' && currentStatus === 'Backlog')) {
|
|
255
|
+
try {
|
|
256
|
+
const workflowState = await getWorkflowState(issueId);
|
|
257
|
+
if (workflowState && workflowState.cycleCount > 0) {
|
|
258
|
+
const wfContext = {
|
|
259
|
+
cycleCount: workflowState.cycleCount,
|
|
260
|
+
strategy: workflowState.strategy,
|
|
261
|
+
failureSummary: workflowState.failureSummary,
|
|
262
|
+
};
|
|
263
|
+
workflowContextBlock = buildFailureContextBlock(workType, wfContext);
|
|
264
|
+
if (workflowContextBlock) {
|
|
265
|
+
sessionLog.info('Prompt enriched with workflow failure context', {
|
|
266
|
+
workType,
|
|
267
|
+
cycleCount: workflowState.cycleCount,
|
|
268
|
+
strategy: workflowState.strategy,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
sessionLog.warn('Failed to enrich prompt with workflow context', { error: err });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
212
277
|
// Queue work
|
|
213
278
|
const work = {
|
|
214
279
|
sessionId,
|
|
@@ -217,7 +282,7 @@ export async function handleSessionCreated(config, payload, rawPayload, log) {
|
|
|
217
282
|
priority,
|
|
218
283
|
queuedAt: Date.now(),
|
|
219
284
|
workType,
|
|
220
|
-
prompt: config.generatePrompt(issueIdentifier, workType,
|
|
285
|
+
prompt: config.generatePrompt(issueIdentifier, workType, enhancedPromptContext) + workflowContextBlock,
|
|
221
286
|
projectName,
|
|
222
287
|
};
|
|
223
288
|
const result = await dispatchWork(work);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supaku/agentfactory-nextjs",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Next.js API route handlers for AgentFactory — webhook processor, worker/session management, public stats",
|
|
6
6
|
"author": "Supaku (https://supaku.com)",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@supaku/agentfactory": "0.7.
|
|
52
|
-
"@supaku/agentfactory-
|
|
53
|
-
"@supaku/agentfactory-
|
|
51
|
+
"@supaku/agentfactory": "0.7.10",
|
|
52
|
+
"@supaku/agentfactory-linear": "0.7.10",
|
|
53
|
+
"@supaku/agentfactory-server": "0.7.10"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"next": ">=14.0.0"
|