@supaku/agentfactory-nextjs 0.7.9 → 0.7.11
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/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- 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 +19 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/webhook/governor-bridge.d.ts +23 -0
- package/dist/src/webhook/governor-bridge.d.ts.map +1 -0
- package/dist/src/webhook/governor-bridge.js +36 -0
- 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 +246 -11
- 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
package/dist/src/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { verifyCronAuth } from './middleware/cron-auth.js';
|
|
|
20
20
|
export { verifyWorkerAuth, requireWorkerAuth, unauthorizedResponse, isWorkerAuthConfigured, } from './middleware/worker-auth.js';
|
|
21
21
|
export { createWebhookHandler } from './webhook/processor.js';
|
|
22
22
|
export { verifyWebhookSignature } from './webhook/signature.js';
|
|
23
|
+
export { setGovernorEventBus, getGovernorEventBus, publishGovernorEvent } from './webhook/governor-bridge.js';
|
|
23
24
|
export { createWorkerRegisterHandler } from './handlers/workers/register.js';
|
|
24
25
|
export { createWorkerListHandler } from './handlers/workers/list.js';
|
|
25
26
|
export { createWorkerGetHandler, createWorkerDeleteHandler } from './handlers/workers/get-delete.js';
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,oBAAoB,EACpB,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,YAAY,GACb,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAG9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AACzE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAG/D,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AACtE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAG7D,OAAO,EACL,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,yBAAyB,CAAA;AAChC,YAAY,EACV,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,yBAAyB,CAAA;AAGhC,OAAO,EAAE,iCAAiC,EAAE,MAAM,6BAA6B,CAAA;AAC/E,YAAY,EAAE,iCAAiC,EAAE,MAAM,6BAA6B,CAAA;AAGpF,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,6BAA6B,CAAA;AAGpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { verifyWorkerAuth, requireWorkerAuth, unauthorizedResponse, isWorkerAuth
|
|
|
21
21
|
// Webhook
|
|
22
22
|
export { createWebhookHandler } from './webhook/processor.js';
|
|
23
23
|
export { verifyWebhookSignature } from './webhook/signature.js';
|
|
24
|
+
// Governor bridge
|
|
25
|
+
export { setGovernorEventBus, getGovernorEventBus, publishGovernorEvent } from './webhook/governor-bridge.js';
|
|
24
26
|
// Individual handler factories (for custom wiring)
|
|
25
27
|
// Worker handlers
|
|
26
28
|
export { createWorkerRegisterHandler } from './handlers/workers/register.js';
|
|
@@ -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,18 @@ 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;
|
|
57
|
+
/**
|
|
58
|
+
* Governor integration mode:
|
|
59
|
+
* - 'direct' (default): Today's behavior -- webhooks dispatch work directly
|
|
60
|
+
* - 'event-bridge': Dual-write -- webhooks dispatch AND publish governor events
|
|
61
|
+
* - 'governor-only': Events only -- webhooks publish to governor, no direct dispatch
|
|
62
|
+
*/
|
|
63
|
+
governorMode?: 'direct' | 'event-bridge' | 'governor-only';
|
|
52
64
|
}
|
|
53
65
|
/**
|
|
54
66
|
* Resolved webhook config with all defaults applied.
|
|
@@ -64,6 +76,13 @@ export interface ResolvedWebhookConfig extends RouteConfig {
|
|
|
64
76
|
buildParentAcceptanceContext?: (identifier: string, subIssues: SubIssueStatus[]) => string;
|
|
65
77
|
/** Linear project names this server handles. Empty/undefined = all projects. */
|
|
66
78
|
projects?: string[];
|
|
79
|
+
/**
|
|
80
|
+
* Governor integration mode:
|
|
81
|
+
* - 'direct' (default): Today's behavior -- webhooks dispatch work directly
|
|
82
|
+
* - 'event-bridge': Dual-write -- webhooks dispatch AND publish governor events
|
|
83
|
+
* - 'governor-only': Events only -- webhooks publish to governor, no direct dispatch
|
|
84
|
+
*/
|
|
85
|
+
governorMode?: 'direct' | 'event-bridge' | 'governor-only';
|
|
67
86
|
}
|
|
68
87
|
/**
|
|
69
88
|
* Configuration for cron-authenticated routes.
|
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":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governor Bridge
|
|
3
|
+
*
|
|
4
|
+
* Connects webhook handlers to the GovernorEventBus. When configured,
|
|
5
|
+
* webhook handlers publish events to the bus in addition to (or instead of)
|
|
6
|
+
* their normal direct-dispatch behavior.
|
|
7
|
+
*/
|
|
8
|
+
import type { GovernorEventBus, GovernorEvent } from '@supaku/agentfactory';
|
|
9
|
+
/**
|
|
10
|
+
* Configure the governor event bus for webhook bridging.
|
|
11
|
+
* Call this during server initialization when governorMode != 'direct'.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setGovernorEventBus(bus: GovernorEventBus): void;
|
|
14
|
+
/**
|
|
15
|
+
* Get the configured event bus, or null if not configured.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getGovernorEventBus(): GovernorEventBus | null;
|
|
18
|
+
/**
|
|
19
|
+
* Publish a GovernorEvent if a bus is configured.
|
|
20
|
+
* Returns the event ID if published, null if no bus is configured.
|
|
21
|
+
*/
|
|
22
|
+
export declare function publishGovernorEvent(event: GovernorEvent): Promise<string | null>;
|
|
23
|
+
//# sourceMappingURL=governor-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor-bridge.d.ts","sourceRoot":"","sources":["../../../src/webhook/governor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAI3E;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAE/D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAE7D;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQvF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governor Bridge
|
|
3
|
+
*
|
|
4
|
+
* Connects webhook handlers to the GovernorEventBus. When configured,
|
|
5
|
+
* webhook handlers publish events to the bus in addition to (or instead of)
|
|
6
|
+
* their normal direct-dispatch behavior.
|
|
7
|
+
*/
|
|
8
|
+
let _eventBus = null;
|
|
9
|
+
/**
|
|
10
|
+
* Configure the governor event bus for webhook bridging.
|
|
11
|
+
* Call this during server initialization when governorMode != 'direct'.
|
|
12
|
+
*/
|
|
13
|
+
export function setGovernorEventBus(bus) {
|
|
14
|
+
_eventBus = bus;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get the configured event bus, or null if not configured.
|
|
18
|
+
*/
|
|
19
|
+
export function getGovernorEventBus() {
|
|
20
|
+
return _eventBus;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Publish a GovernorEvent if a bus is configured.
|
|
24
|
+
* Returns the event ID if published, null if no bus is configured.
|
|
25
|
+
*/
|
|
26
|
+
export async function publishGovernorEvent(event) {
|
|
27
|
+
if (!_eventBus)
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
return await _eventBus.publish(event);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error('[governor-bridge] Failed to publish event:', err);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -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;AA0BtF,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,CAyxB9B"}
|
|
@@ -2,13 +2,16 @@
|
|
|
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';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { buildFailureContextBlock } from '@supaku/agentfactory-linear';
|
|
12
|
+
import { checkIssueDeploymentStatus, formatFailedDeployments, eventTimestamp, } from '@supaku/agentfactory';
|
|
13
|
+
import { publishGovernorEvent } from '../governor-bridge.js';
|
|
14
|
+
import { generateIdempotencyKey, isWebhookProcessed, storeSessionState, getSessionStateByIssue, dispatchWork, wasAgentWorked, didJustFailQA, getQAAttemptCount, recordQAAttempt, clearQAFailed, didJustQueueDevelopment, markDevelopmentQueued, didJustQueueAcceptance, markAcceptanceQueued, getWorkflowState, } from '@supaku/agentfactory-server';
|
|
12
15
|
import { emitActivity, resolveStateName, isProjectAllowed, hasExcludedLabel, getAppUrl, } from '../utils.js';
|
|
13
16
|
export async function handleIssueUpdated(config, payload, log) {
|
|
14
17
|
const { data, updatedFrom, actor } = payload;
|
|
@@ -24,6 +27,33 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
24
27
|
return NextResponse.json({ success: true, skipped: true, reason: 'project_not_allowed' });
|
|
25
28
|
}
|
|
26
29
|
const autoTrigger = config.autoTrigger;
|
|
30
|
+
// --- Governor event bridge ---
|
|
31
|
+
if (currentStateName && updatedFrom?.stateId) {
|
|
32
|
+
const governorIssue = {
|
|
33
|
+
id: issueId,
|
|
34
|
+
identifier: issueIdentifier,
|
|
35
|
+
title: data.title ?? '',
|
|
36
|
+
description: data.description ?? undefined,
|
|
37
|
+
status: currentStateName,
|
|
38
|
+
labels: (data.labels ?? []).map(l => l.name),
|
|
39
|
+
createdAt: data.createdAt ? new Date(data.createdAt).getTime() : Date.now(),
|
|
40
|
+
parentId: data.parent?.id,
|
|
41
|
+
project: projectName,
|
|
42
|
+
};
|
|
43
|
+
await publishGovernorEvent({
|
|
44
|
+
type: 'issue-status-changed',
|
|
45
|
+
issueId,
|
|
46
|
+
issue: governorIssue,
|
|
47
|
+
previousStatus: updatedFrom.state?.name,
|
|
48
|
+
newStatus: currentStateName,
|
|
49
|
+
timestamp: eventTimestamp(),
|
|
50
|
+
source: 'webhook',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (config.governorMode === 'governor-only') {
|
|
54
|
+
issueLog.info('Governor-only mode: skipping direct dispatch', { currentStateName });
|
|
55
|
+
return NextResponse.json({ success: true, governorMode: 'governor-only' });
|
|
56
|
+
}
|
|
27
57
|
// === Handle Finished transition (auto-QA) ===
|
|
28
58
|
if (currentStateName === 'Finished' && updatedFrom?.stateId) {
|
|
29
59
|
issueLog.info('Issue transitioned to Finished', {
|
|
@@ -71,6 +101,20 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
71
101
|
issueLog.info('Issue in QA cooldown period, skipping');
|
|
72
102
|
return NextResponse.json({ success: true, skipped: true, reason: 'qa_cooldown' });
|
|
73
103
|
}
|
|
104
|
+
// Check workflow state for circuit breaker before QA
|
|
105
|
+
try {
|
|
106
|
+
const workflowState = await getWorkflowState(issueId);
|
|
107
|
+
if (workflowState?.strategy === 'escalate-human') {
|
|
108
|
+
issueLog.warn('Circuit breaker: escalate-human strategy, blocking QA', {
|
|
109
|
+
cycleCount: workflowState.cycleCount,
|
|
110
|
+
strategy: workflowState.strategy,
|
|
111
|
+
});
|
|
112
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'circuit_breaker_escalate_human' });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
issueLog.warn('Failed to check workflow state for circuit breaker', { error: err });
|
|
117
|
+
}
|
|
74
118
|
const attemptCount = await getQAAttemptCount(issueId);
|
|
75
119
|
if (attemptCount >= 3) {
|
|
76
120
|
issueLog.warn('QA attempt limit reached', { attemptCount });
|
|
@@ -124,6 +168,31 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
124
168
|
catch (err) {
|
|
125
169
|
issueLog.warn('Failed to detect parent issue for QA routing', { error: err });
|
|
126
170
|
}
|
|
171
|
+
// Enrich QA prompt with previous failure context
|
|
172
|
+
if (attemptCount > 0) {
|
|
173
|
+
try {
|
|
174
|
+
const workflowState = await getWorkflowState(issueId);
|
|
175
|
+
if (workflowState && workflowState.failureSummary) {
|
|
176
|
+
const wfContext = {
|
|
177
|
+
cycleCount: workflowState.cycleCount,
|
|
178
|
+
strategy: workflowState.strategy,
|
|
179
|
+
failureSummary: workflowState.failureSummary,
|
|
180
|
+
qaAttemptCount: attemptCount,
|
|
181
|
+
};
|
|
182
|
+
const contextBlock = buildFailureContextBlock(qaWorkType, wfContext);
|
|
183
|
+
if (contextBlock) {
|
|
184
|
+
qaPrompt += contextBlock;
|
|
185
|
+
issueLog.info('QA prompt enriched with failure context', {
|
|
186
|
+
cycleCount: workflowState.cycleCount,
|
|
187
|
+
attemptCount,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
issueLog.warn('Failed to enrich QA prompt with failure context', { error: err });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
127
196
|
// Create Linear AgentSession for QA
|
|
128
197
|
let qaSessionId;
|
|
129
198
|
try {
|
|
@@ -200,17 +269,155 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
200
269
|
issueLog.error('Failed to queue QA work');
|
|
201
270
|
}
|
|
202
271
|
}
|
|
203
|
-
// === Handle
|
|
272
|
+
// === Handle → Rejected transition (escalation ladder) ===
|
|
273
|
+
// When QA/acceptance fails, the orchestrator transitions the issue to Rejected.
|
|
274
|
+
// Check the escalation strategy and act accordingly.
|
|
275
|
+
if (currentStateName === 'Rejected' && updatedFrom?.stateId) {
|
|
276
|
+
try {
|
|
277
|
+
const workflowState = await getWorkflowState(issueId);
|
|
278
|
+
if (workflowState) {
|
|
279
|
+
const { strategy, cycleCount, failureSummary } = workflowState;
|
|
280
|
+
if (strategy === 'escalate-human') {
|
|
281
|
+
issueLog.warn('Escalation ladder: escalate-human — creating blocker and stopping loop', {
|
|
282
|
+
cycleCount,
|
|
283
|
+
strategy,
|
|
284
|
+
});
|
|
285
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
286
|
+
// Post escalation summary comment
|
|
287
|
+
const totalCostLine = workflowState.phases
|
|
288
|
+
? (() => {
|
|
289
|
+
const allPhases = [
|
|
290
|
+
...workflowState.phases.development,
|
|
291
|
+
...workflowState.phases.qa,
|
|
292
|
+
...workflowState.phases.refinement,
|
|
293
|
+
...workflowState.phases.acceptance,
|
|
294
|
+
];
|
|
295
|
+
const totalCost = allPhases.reduce((sum, p) => sum + (p.costUsd ?? 0), 0);
|
|
296
|
+
return totalCost > 0 ? `\n**Total cost across all attempts:** $${totalCost.toFixed(2)}` : '';
|
|
297
|
+
})()
|
|
298
|
+
: '';
|
|
299
|
+
try {
|
|
300
|
+
await linearClient.createComment(issueId, `## Circuit Breaker: Human Intervention Required\n\n` +
|
|
301
|
+
`This issue has gone through **${cycleCount} dev-QA-rejected cycles** without passing.\n` +
|
|
302
|
+
`The automated system is stopping further attempts.\n` +
|
|
303
|
+
totalCostLine +
|
|
304
|
+
`\n\n### Failure History\n\n${failureSummary ?? 'No failure details recorded.'}\n\n` +
|
|
305
|
+
`### Recommended Actions\n` +
|
|
306
|
+
`1. Review the failure patterns above\n` +
|
|
307
|
+
`2. Consider if the acceptance criteria need clarification\n` +
|
|
308
|
+
`3. Investigate whether there's an architectural issue\n` +
|
|
309
|
+
`4. Manually fix or decompose the issue before re-enabling automation`);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
issueLog.error('Failed to post escalation comment', { error: err });
|
|
313
|
+
}
|
|
314
|
+
// Create a blocker issue in Icebox with 'Needs Human' label
|
|
315
|
+
try {
|
|
316
|
+
const issue = await linearClient.getIssue(issueId);
|
|
317
|
+
const team = await issue.team;
|
|
318
|
+
if (team) {
|
|
319
|
+
const statuses = await linearClient.getTeamStatuses(team.id);
|
|
320
|
+
const iceboxStateId = statuses['Icebox'];
|
|
321
|
+
// Find 'Needs Human' label
|
|
322
|
+
const allLabels = await linearClient.linearClient.issueLabels();
|
|
323
|
+
const needsHumanLabel = allLabels.nodes.find((l) => l.name.toLowerCase() === 'needs human');
|
|
324
|
+
const blockerTitle = `Human review needed: ${issueIdentifier} failed ${cycleCount} automated cycles`;
|
|
325
|
+
const blockerDescription = [
|
|
326
|
+
`This issue has failed **${cycleCount} automated dev-QA-rejected cycles** and requires human intervention.`,
|
|
327
|
+
'',
|
|
328
|
+
'### Failure History',
|
|
329
|
+
failureSummary ?? 'No failure details recorded.',
|
|
330
|
+
'',
|
|
331
|
+
'---',
|
|
332
|
+
`*Source issue: ${issueIdentifier}*`,
|
|
333
|
+
].join('\n');
|
|
334
|
+
const createPayload = {
|
|
335
|
+
title: blockerTitle,
|
|
336
|
+
description: blockerDescription,
|
|
337
|
+
teamId: team.id,
|
|
338
|
+
...(iceboxStateId && { stateId: iceboxStateId }),
|
|
339
|
+
...(needsHumanLabel && { labelIds: [needsHumanLabel.id] }),
|
|
340
|
+
};
|
|
341
|
+
// Add project if available
|
|
342
|
+
const project = await issue.project;
|
|
343
|
+
if (project) {
|
|
344
|
+
createPayload.projectId = project.id;
|
|
345
|
+
}
|
|
346
|
+
const blockerIssue = await linearClient.createIssue(createPayload);
|
|
347
|
+
// Create blocking relation: blocker blocks source issue
|
|
348
|
+
await linearClient.createIssueRelation({
|
|
349
|
+
issueId: blockerIssue.id,
|
|
350
|
+
relatedIssueId: issueId,
|
|
351
|
+
type: 'blocks',
|
|
352
|
+
});
|
|
353
|
+
issueLog.info('Escalation ladder: blocker issue created', {
|
|
354
|
+
issueId,
|
|
355
|
+
blockerIssueId: blockerIssue.id,
|
|
356
|
+
blockerIdentifier: blockerIssue.identifier,
|
|
357
|
+
cycleCount,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
issueLog.warn('Escalation ladder: could not resolve team for blocker creation', { issueId });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
issueLog.error('Failed to create blocker issue for escalation', { error: err });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else if (strategy === 'decompose') {
|
|
369
|
+
issueLog.info('Escalation ladder: decompose strategy — refinement will attempt decomposition', {
|
|
370
|
+
cycleCount,
|
|
371
|
+
strategy,
|
|
372
|
+
});
|
|
373
|
+
// The decomposition strategy will be handled via prompt enrichment (SUP-713)
|
|
374
|
+
// by injecting decomposition instructions into the refinement prompt
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
issueLog.warn('Failed to check workflow state for escalation ladder', { error: err });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// === Handle → Backlog transition (auto-development) ===
|
|
383
|
+
// Triggers from: Icebox → Backlog (new issues), Rejected → Backlog (post-refinement retries), etc.
|
|
204
384
|
if (currentStateName === 'Backlog' && updatedFrom?.stateId) {
|
|
205
385
|
const previousStateName = await resolveStateName(config, payload.organizationId, issueId, updatedFrom.stateId);
|
|
206
|
-
|
|
207
|
-
|
|
386
|
+
// Skip transitions from states that don't indicate readiness for development
|
|
387
|
+
// (e.g., Backlog → Backlog is a no-op, Started → Backlog means work was abandoned)
|
|
388
|
+
const allowedPreviousStates = ['Icebox', 'Rejected', 'Canceled'];
|
|
389
|
+
if (!allowedPreviousStates.includes(previousStateName ?? '')) {
|
|
390
|
+
issueLog.debug('Issue transitioned to Backlog from non-triggering state', { previousStateName });
|
|
208
391
|
}
|
|
209
392
|
else {
|
|
210
|
-
|
|
393
|
+
const isRetry = previousStateName === 'Rejected';
|
|
394
|
+
issueLog.info('Issue transitioned to Backlog', {
|
|
211
395
|
previousStateName,
|
|
396
|
+
isRetry,
|
|
212
397
|
actorName: actor?.name,
|
|
213
398
|
});
|
|
399
|
+
// Circuit breaker: check workflow state for escalate-human strategy
|
|
400
|
+
if (isRetry) {
|
|
401
|
+
try {
|
|
402
|
+
const workflowState = await getWorkflowState(issueId);
|
|
403
|
+
if (workflowState && workflowState.strategy === 'escalate-human') {
|
|
404
|
+
issueLog.warn('Circuit breaker: issue at escalate-human strategy, skipping auto-development', {
|
|
405
|
+
cycleCount: workflowState.cycleCount,
|
|
406
|
+
strategy: workflowState.strategy,
|
|
407
|
+
});
|
|
408
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'circuit_breaker_escalate_human' });
|
|
409
|
+
}
|
|
410
|
+
if (workflowState) {
|
|
411
|
+
issueLog.info('Workflow state found for retry', {
|
|
412
|
+
cycleCount: workflowState.cycleCount,
|
|
413
|
+
strategy: workflowState.strategy,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
issueLog.warn('Failed to check workflow state for circuit breaker', { error: err });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
214
421
|
const existingSession = await getSessionStateByIssue(issueId);
|
|
215
422
|
if (existingSession && ['running', 'claimed', 'pending'].includes(existingSession.status)) {
|
|
216
423
|
issueLog.info('Session already active, skipping development trigger');
|
|
@@ -257,7 +464,31 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
257
464
|
catch (err) {
|
|
258
465
|
issueLog.warn('Failed to check if issue is parent', { error: err });
|
|
259
466
|
}
|
|
260
|
-
|
|
467
|
+
let prompt = config.generatePrompt(issueIdentifier, workType);
|
|
468
|
+
// Enrich prompt with failure context for retries
|
|
469
|
+
if (isRetry) {
|
|
470
|
+
try {
|
|
471
|
+
const workflowState = await getWorkflowState(issueId);
|
|
472
|
+
if (workflowState && workflowState.cycleCount > 0) {
|
|
473
|
+
const wfContext = {
|
|
474
|
+
cycleCount: workflowState.cycleCount,
|
|
475
|
+
strategy: workflowState.strategy,
|
|
476
|
+
failureSummary: workflowState.failureSummary,
|
|
477
|
+
};
|
|
478
|
+
const contextBlock = buildFailureContextBlock(workType, wfContext);
|
|
479
|
+
if (contextBlock) {
|
|
480
|
+
prompt += contextBlock;
|
|
481
|
+
issueLog.info('Development prompt enriched with failure context', {
|
|
482
|
+
cycleCount: workflowState.cycleCount,
|
|
483
|
+
strategy: workflowState.strategy,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
issueLog.warn('Failed to enrich development prompt with failure context', { error: err });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
261
492
|
await storeSessionState(devSessionId, {
|
|
262
493
|
issueId,
|
|
263
494
|
issueIdentifier,
|
|
@@ -283,7 +514,8 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
283
514
|
};
|
|
284
515
|
const devResult = await dispatchWork(devWork);
|
|
285
516
|
if (devResult.dispatched || devResult.parked) {
|
|
286
|
-
|
|
517
|
+
const retryLabel = isRetry ? ' (retry)' : '';
|
|
518
|
+
issueLog.info(`Development work dispatched${retryLabel}`, { sessionId: devSessionId });
|
|
287
519
|
try {
|
|
288
520
|
const appUrl = getAppUrl(config);
|
|
289
521
|
await linearClient.updateAgentSession({
|
|
@@ -295,7 +527,10 @@ export async function handleIssueUpdated(config, payload, log) {
|
|
|
295
527
|
issueLog.warn('Failed to update session externalUrl', { error: err });
|
|
296
528
|
}
|
|
297
529
|
try {
|
|
298
|
-
|
|
530
|
+
const activityMsg = isRetry
|
|
531
|
+
? 'Development work queued (retry after refinement). Waiting for an available worker...'
|
|
532
|
+
: 'Development work queued. Waiting for an available worker...';
|
|
533
|
+
await emitActivity(linearClient, devSessionId, 'thought', activityMsg);
|
|
299
534
|
}
|
|
300
535
|
catch (err) {
|
|
301
536
|
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.11",
|
|
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.11",
|
|
52
|
+
"@supaku/agentfactory-linear": "0.7.11",
|
|
53
|
+
"@supaku/agentfactory-server": "0.7.11"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"next": ">=14.0.0"
|