@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.
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-orchestrator.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/webhook-orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA4BH,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,CA8O7B"}
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
  }
@@ -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.
@@ -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;CACpB;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"}
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
- * - Icebox Backlogauto-development trigger
5
+ * - Finished → auto-QA trigger (with circuit breaker)
6
+ * - → Rejectedescalation 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;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAiB,MAAM,6BAA6B,CAAA;AAsBtF,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,CA+hB9B"}
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
- * - Icebox Backlogauto-development trigger
5
+ * - Finished → auto-QA trigger (with circuit breaker)
6
+ * - → Rejectedescalation 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 Icebox Backlog transition (auto-development) ===
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
- if (previousStateName !== 'Icebox') {
207
- issueLog.debug('Issue transitioned to Backlog but not from Icebox', { previousStateName });
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
- issueLog.info('Issue transitioned from Icebox to Backlog', {
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
- const prompt = config.generatePrompt(issueIdentifier, workType);
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
- issueLog.info('Development work dispatched', { sessionId: devSessionId });
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
- await emitActivity(linearClient, devSessionId, 'thought', 'Development work queued. Waiting for an available worker...');
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;AAiBtF,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,CAkT9B"}
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, promptContext),
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.8",
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.8",
52
- "@supaku/agentfactory-server": "0.7.8",
53
- "@supaku/agentfactory-linear": "0.7.8"
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"