@exaudeus/workrail 3.44.0 → 3.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-pipeline.d.ts +17 -0
- package/dist/cli/commands/worktrain-pipeline.js +121 -0
- package/dist/console-ui/assets/{index-Bi38ITiQ.js → index-BQFhoMcY.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/adaptive-pipeline.d.ts +57 -0
- package/dist/coordinators/adaptive-pipeline.js +104 -0
- package/dist/coordinators/modes/full-pipeline.d.ts +4 -0
- package/dist/coordinators/modes/full-pipeline.js +256 -0
- package/dist/coordinators/modes/implement-shared.d.ts +4 -0
- package/dist/coordinators/modes/implement-shared.js +201 -0
- package/dist/coordinators/modes/implement.d.ts +3 -0
- package/dist/coordinators/modes/implement.js +108 -0
- package/dist/coordinators/modes/quick-review.d.ts +3 -0
- package/dist/coordinators/modes/quick-review.js +37 -0
- package/dist/coordinators/modes/review-only.d.ts +2 -0
- package/dist/coordinators/modes/review-only.js +28 -0
- package/dist/coordinators/routing/route-task.d.ts +21 -0
- package/dist/coordinators/routing/route-task.js +55 -0
- package/dist/daemon/workflow-runner.d.ts +12 -2
- package/dist/daemon/workflow-runner.js +96 -13
- package/dist/manifest.json +101 -29
- package/dist/mcp/output-schemas.d.ts +16 -16
- package/dist/trigger/notification-service.d.ts +1 -1
- package/dist/trigger/notification-service.js +4 -0
- package/dist/trigger/trigger-router.d.ts +3 -0
- package/dist/trigger/trigger-router.js +17 -0
- package/dist/trigger/types.d.ts +2 -0
- package/dist/v2/durable-core/schemas/artifacts/discovery-handoff.d.ts +29 -0
- package/dist/v2/durable-core/schemas/artifacts/discovery-handoff.js +26 -0
- package/dist/v2/durable-core/schemas/artifacts/index.d.ts +2 -1
- package/dist/v2/durable-core/schemas/artifacts/index.js +7 -1
- package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +8 -8
- package/dist/v2/usecases/console-routes.js +3 -0
- package/docs/design/design-candidates-stuck-escalation.md +183 -0
- package/docs/design/design-review-findings-stuck-escalation.md +93 -0
- package/docs/design/implementation-plan-stuck-escalation.md +172 -0
- package/docs/ideas/backlog.md +86 -0
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.touchesUI = touchesUI;
|
|
4
|
+
exports.runImplementPipeline = runImplementPipeline;
|
|
5
|
+
const adaptive_pipeline_js_1 = require("../adaptive-pipeline.js");
|
|
6
|
+
const implement_shared_js_1 = require("./implement-shared.js");
|
|
7
|
+
const PR_POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
|
+
const UI_KEYWORDS = [
|
|
9
|
+
'ui', 'screen', 'view', 'layout', 'component', 'design', 'ux', 'frontend',
|
|
10
|
+
];
|
|
11
|
+
function touchesUI(goal) {
|
|
12
|
+
const lower = goal.toLowerCase();
|
|
13
|
+
return UI_KEYWORDS.some((kw) => lower.includes(kw));
|
|
14
|
+
}
|
|
15
|
+
async function runImplementPipeline(deps, opts, pitchPath, coordinatorStartMs) {
|
|
16
|
+
deps.stderr(`[implement] Starting IMPLEMENT pipeline for workspace=${opts.workspace}`);
|
|
17
|
+
const archiveDir = opts.workspace + '/.workrail/used-pitches';
|
|
18
|
+
const archiveTimestamp = deps.nowIso().replace(/[:.]/g, '-');
|
|
19
|
+
const archivePath = archiveDir + '/pitch-' + archiveTimestamp + '.md';
|
|
20
|
+
let outcome;
|
|
21
|
+
try {
|
|
22
|
+
outcome = await runImplementCore(deps, opts, pitchPath, coordinatorStartMs);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
try {
|
|
26
|
+
await deps.mkdir(archiveDir, { recursive: true });
|
|
27
|
+
await deps.archiveFile(pitchPath, archivePath);
|
|
28
|
+
deps.stderr(`[implement] Pitch archived to ${archivePath}`);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
deps.stderr(`[WARN implement] Failed to archive pitch.md: ${e instanceof Error ? e.message : String(e)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return outcome;
|
|
35
|
+
}
|
|
36
|
+
async function runImplementCore(deps, opts, pitchPath, coordinatorStartMs) {
|
|
37
|
+
if (touchesUI(opts.goal)) {
|
|
38
|
+
deps.stderr(`[implement] UX signals detected in goal, dispatching ui-ux-design-workflow`);
|
|
39
|
+
const cutoffCheck = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'ux-gate');
|
|
40
|
+
if (cutoffCheck)
|
|
41
|
+
return cutoffCheck;
|
|
42
|
+
const uxSpawnResult = await deps.spawnSession('ui-ux-design-workflow', opts.goal, opts.workspace, {
|
|
43
|
+
pitchPath,
|
|
44
|
+
});
|
|
45
|
+
if (uxSpawnResult.kind === 'err') {
|
|
46
|
+
deps.stderr(`[implement] UX gate spawn failed: ${uxSpawnResult.error}`);
|
|
47
|
+
return {
|
|
48
|
+
kind: 'escalated',
|
|
49
|
+
escalationReason: { phase: 'ux-gate', reason: `UX gate spawn failed: ${uxSpawnResult.error}` },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const uxHandle = uxSpawnResult.value;
|
|
53
|
+
const uxAwait = await deps.awaitSessions([uxHandle], adaptive_pipeline_js_1.REVIEW_TIMEOUT_MS);
|
|
54
|
+
const uxResult = uxAwait.results[0];
|
|
55
|
+
if (!uxResult || uxResult.outcome !== 'success') {
|
|
56
|
+
const outcome = uxResult?.outcome ?? 'not_found';
|
|
57
|
+
return {
|
|
58
|
+
kind: 'escalated',
|
|
59
|
+
escalationReason: { phase: 'ux-gate', reason: `UX design session ${outcome}` },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
deps.stderr(`[implement] UX design session completed`);
|
|
63
|
+
}
|
|
64
|
+
const cutoffCheck = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'coding');
|
|
65
|
+
if (cutoffCheck)
|
|
66
|
+
return cutoffCheck;
|
|
67
|
+
deps.stderr(`[implement] Spawning coding-task-workflow-agentic`);
|
|
68
|
+
const codingSpawnResult = await deps.spawnSession('coding-task-workflow-agentic', opts.goal, opts.workspace, {
|
|
69
|
+
pitchPath,
|
|
70
|
+
});
|
|
71
|
+
if (codingSpawnResult.kind === 'err') {
|
|
72
|
+
return {
|
|
73
|
+
kind: 'escalated',
|
|
74
|
+
escalationReason: { phase: 'coding', reason: `coding session spawn failed: ${codingSpawnResult.error}` },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const codingHandle = codingSpawnResult.value;
|
|
78
|
+
if (!codingHandle) {
|
|
79
|
+
return {
|
|
80
|
+
kind: 'escalated',
|
|
81
|
+
escalationReason: { phase: 'coding', reason: 'coding session returned empty handle (zombie detection)' },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const codingAwait = await deps.awaitSessions([codingHandle], adaptive_pipeline_js_1.CODING_TIMEOUT_MS);
|
|
85
|
+
const codingResult = codingAwait.results[0];
|
|
86
|
+
if (!codingResult || codingResult.outcome !== 'success') {
|
|
87
|
+
const outcome = codingResult?.outcome ?? 'not_found';
|
|
88
|
+
return {
|
|
89
|
+
kind: 'escalated',
|
|
90
|
+
escalationReason: { phase: 'coding', reason: `coding session ${outcome}` },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
deps.stderr(`[implement] Coding session completed (${Math.round((codingResult.durationMs ?? 0) / 1000)}s)`);
|
|
94
|
+
const branchPattern = `worktrain/${codingHandle.slice(0, 16)}`;
|
|
95
|
+
deps.stderr(`[implement] Polling for PR on branch pattern: ${branchPattern}`);
|
|
96
|
+
const prUrl = await deps.pollForPR(branchPattern, PR_POLL_TIMEOUT_MS);
|
|
97
|
+
if (!prUrl) {
|
|
98
|
+
return {
|
|
99
|
+
kind: 'escalated',
|
|
100
|
+
escalationReason: {
|
|
101
|
+
phase: 'pr-detection',
|
|
102
|
+
reason: `no PR found matching ${branchPattern} within ${PR_POLL_TIMEOUT_MS / 60000} minutes`,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
deps.stderr(`[implement] PR detected: ${prUrl}`);
|
|
107
|
+
return (0, implement_shared_js_1.runReviewAndVerdictCycle)(deps, opts, prUrl, coordinatorStartMs, 0);
|
|
108
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AdaptiveCoordinatorDeps, AdaptivePipelineOpts, PipelineOutcome } from '../adaptive-pipeline.js';
|
|
2
|
+
export declare function runQuickReviewPipeline(deps: AdaptiveCoordinatorDeps, opts: AdaptivePipelineOpts, prNumbers: readonly number[], _coordinatorStartMs: number): Promise<PipelineOutcome>;
|
|
3
|
+
export declare function buildDepBumpGoal(prNumbers: readonly number[], originalGoal: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runQuickReviewPipeline = runQuickReviewPipeline;
|
|
4
|
+
exports.buildDepBumpGoal = buildDepBumpGoal;
|
|
5
|
+
const pr_review_js_1 = require("../pr-review.js");
|
|
6
|
+
async function runQuickReviewPipeline(deps, opts, prNumbers, _coordinatorStartMs) {
|
|
7
|
+
deps.stderr(`[quick-review] Dep-bump review for PR(s) [${prNumbers.join(', ')}]`);
|
|
8
|
+
const depBumpGoal = buildDepBumpGoal(prNumbers, opts.goal);
|
|
9
|
+
deps.stderr(`[quick-review] Goal: "${depBumpGoal.slice(0, 100)}"`);
|
|
10
|
+
const result = await (0, pr_review_js_1.runPrReviewCoordinator)(deps, {
|
|
11
|
+
workspace: opts.workspace,
|
|
12
|
+
prs: prNumbers.length > 0 ? [...prNumbers] : undefined,
|
|
13
|
+
dryRun: opts.dryRun ?? false,
|
|
14
|
+
port: opts.port,
|
|
15
|
+
});
|
|
16
|
+
if (result.hasErrors || result.escalated > 0) {
|
|
17
|
+
return {
|
|
18
|
+
kind: 'escalated',
|
|
19
|
+
escalationReason: {
|
|
20
|
+
phase: 'review',
|
|
21
|
+
reason: result.hasErrors
|
|
22
|
+
? `dep-bump review coordinator reported errors (escalated=${result.escalated})`
|
|
23
|
+
: `${result.escalated} dep-bump PR(s) escalated during review`,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
kind: 'merged',
|
|
29
|
+
prUrl: result.mergedPrs.length > 0 ? `PR #${result.mergedPrs[0]}` : null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function buildDepBumpGoal(prNumbers, originalGoal) {
|
|
33
|
+
const firstPr = prNumbers[0];
|
|
34
|
+
const prRef = firstPr !== undefined ? `PR #${firstPr}` : 'dep-bump PR';
|
|
35
|
+
const prTitle = originalGoal.slice(0, 80);
|
|
36
|
+
return `[DEP BUMP] Review ${prRef}: ${prTitle} -- skip architecture audit, verify version compatibility and test coverage only`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { AdaptiveCoordinatorDeps, AdaptivePipelineOpts, PipelineOutcome } from '../adaptive-pipeline.js';
|
|
2
|
+
export declare function runReviewOnlyPipeline(deps: AdaptiveCoordinatorDeps, opts: AdaptivePipelineOpts, prNumbers: readonly number[], _coordinatorStartMs: number): Promise<PipelineOutcome>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runReviewOnlyPipeline = runReviewOnlyPipeline;
|
|
4
|
+
const pr_review_js_1 = require("../pr-review.js");
|
|
5
|
+
async function runReviewOnlyPipeline(deps, opts, prNumbers, _coordinatorStartMs) {
|
|
6
|
+
deps.stderr(`[review-only] Delegating to runPrReviewCoordinator() for ${prNumbers.length > 0 ? `PR(s) [${prNumbers.join(', ')}]` : 'all open PRs'}`);
|
|
7
|
+
const result = await (0, pr_review_js_1.runPrReviewCoordinator)(deps, {
|
|
8
|
+
workspace: opts.workspace,
|
|
9
|
+
prs: prNumbers.length > 0 ? [...prNumbers] : undefined,
|
|
10
|
+
dryRun: opts.dryRun ?? false,
|
|
11
|
+
port: opts.port,
|
|
12
|
+
});
|
|
13
|
+
if (result.hasErrors || result.escalated > 0) {
|
|
14
|
+
return {
|
|
15
|
+
kind: 'escalated',
|
|
16
|
+
escalationReason: {
|
|
17
|
+
phase: 'review',
|
|
18
|
+
reason: result.hasErrors
|
|
19
|
+
? `review coordinator reported errors (escalated=${result.escalated})`
|
|
20
|
+
: `${result.escalated} PR(s) escalated during review`,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
kind: 'merged',
|
|
26
|
+
prUrl: result.mergedPrs.length > 0 ? `PR #${result.mergedPrs[0]}` : null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type PipelineMode = {
|
|
2
|
+
readonly kind: 'QUICK_REVIEW';
|
|
3
|
+
readonly prNumbers: readonly number[];
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'REVIEW_ONLY';
|
|
6
|
+
readonly prNumbers: readonly number[];
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'IMPLEMENT';
|
|
9
|
+
readonly pitchPath: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'FULL';
|
|
12
|
+
readonly goal: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'ESCALATE';
|
|
15
|
+
readonly reason: string;
|
|
16
|
+
};
|
|
17
|
+
export interface RoutingDeps {
|
|
18
|
+
readonly fileExists: (path: string) => boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function extractPrNumbers(goal: string): number[];
|
|
21
|
+
export declare function routeTask(goal: string, workspace: string, deps: RoutingDeps, triggerProvider?: string): PipelineMode;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractPrNumbers = extractPrNumbers;
|
|
4
|
+
exports.routeTask = routeTask;
|
|
5
|
+
const DEP_BUMP_KEYWORDS = [
|
|
6
|
+
'bump',
|
|
7
|
+
'chore:',
|
|
8
|
+
'dependabot',
|
|
9
|
+
'dependency upgrade',
|
|
10
|
+
];
|
|
11
|
+
const PR_REGEX = /\bPR\s*#\d+\b/i;
|
|
12
|
+
const MR_REGEX = /\bMR\s*!?\d+\b/i;
|
|
13
|
+
const PITCH_FILE_PATH = '.workrail/current-pitch.md';
|
|
14
|
+
function extractPrNumbers(goal) {
|
|
15
|
+
const numbers = [];
|
|
16
|
+
const prMatches = goal.matchAll(/\bPR\s*#(\d+)\b/gi);
|
|
17
|
+
for (const match of prMatches) {
|
|
18
|
+
const n = parseInt(match[1], 10);
|
|
19
|
+
if (!isNaN(n))
|
|
20
|
+
numbers.push(n);
|
|
21
|
+
}
|
|
22
|
+
const mrMatches = goal.matchAll(/\bMR\s*!?#?(\d+)\b/gi);
|
|
23
|
+
for (const match of mrMatches) {
|
|
24
|
+
const n = parseInt(match[1], 10);
|
|
25
|
+
if (!isNaN(n))
|
|
26
|
+
numbers.push(n);
|
|
27
|
+
}
|
|
28
|
+
return numbers;
|
|
29
|
+
}
|
|
30
|
+
function hasDependencyBumpKeywords(goal) {
|
|
31
|
+
const lower = goal.toLowerCase();
|
|
32
|
+
return DEP_BUMP_KEYWORDS.some((kw) => lower.includes(kw));
|
|
33
|
+
}
|
|
34
|
+
function hasPrOrMrReference(goal) {
|
|
35
|
+
return PR_REGEX.test(goal) || MR_REGEX.test(goal);
|
|
36
|
+
}
|
|
37
|
+
function routeTask(goal, workspace, deps, triggerProvider) {
|
|
38
|
+
const prNumbers = extractPrNumbers(goal);
|
|
39
|
+
const hasDepBump = hasDependencyBumpKeywords(goal);
|
|
40
|
+
const hasPrRef = hasPrOrMrReference(goal);
|
|
41
|
+
const isGithubPrsPoll = triggerProvider === 'github_prs_poll';
|
|
42
|
+
if (hasDepBump && hasPrRef) {
|
|
43
|
+
return { kind: 'QUICK_REVIEW', prNumbers };
|
|
44
|
+
}
|
|
45
|
+
if (hasPrRef || isGithubPrsPoll) {
|
|
46
|
+
return { kind: 'REVIEW_ONLY', prNumbers };
|
|
47
|
+
}
|
|
48
|
+
const pitchPath = workspace.endsWith('/')
|
|
49
|
+
? workspace + PITCH_FILE_PATH
|
|
50
|
+
: workspace + '/' + PITCH_FILE_PATH;
|
|
51
|
+
if (deps.fileExists(pitchPath)) {
|
|
52
|
+
return { kind: 'IMPLEMENT', pitchPath };
|
|
53
|
+
}
|
|
54
|
+
return { kind: 'FULL', goal };
|
|
55
|
+
}
|
|
@@ -24,6 +24,8 @@ export interface WorkflowTrigger {
|
|
|
24
24
|
readonly maxSessionMinutes?: number;
|
|
25
25
|
readonly maxTurns?: number;
|
|
26
26
|
readonly maxSubagentDepth?: number;
|
|
27
|
+
readonly stuckAbortPolicy?: 'abort' | 'notify_only';
|
|
28
|
+
readonly noProgressAbortEnabled?: boolean;
|
|
27
29
|
};
|
|
28
30
|
readonly _preAllocatedStartResponse?: import('zod').infer<typeof V2StartWorkflowOutputSchema>;
|
|
29
31
|
readonly parentSessionId?: string;
|
|
@@ -60,14 +62,22 @@ export interface WorkflowRunTimeout {
|
|
|
60
62
|
readonly message: string;
|
|
61
63
|
readonly stopReason: string;
|
|
62
64
|
}
|
|
65
|
+
export interface WorkflowRunStuck {
|
|
66
|
+
readonly _tag: 'stuck';
|
|
67
|
+
readonly workflowId: string;
|
|
68
|
+
readonly reason: 'repeated_tool_call' | 'no_progress';
|
|
69
|
+
readonly message: string;
|
|
70
|
+
readonly stopReason: string;
|
|
71
|
+
readonly issueSummaries?: readonly string[];
|
|
72
|
+
}
|
|
63
73
|
export interface WorkflowDeliveryFailed {
|
|
64
74
|
readonly _tag: 'delivery_failed';
|
|
65
75
|
readonly workflowId: string;
|
|
66
76
|
readonly stopReason: string;
|
|
67
77
|
readonly deliveryError: string;
|
|
68
78
|
}
|
|
69
|
-
export type WorkflowRunResult = WorkflowRunSuccess | WorkflowRunError | WorkflowRunTimeout | WorkflowDeliveryFailed;
|
|
70
|
-
export type ChildWorkflowRunResult = WorkflowRunSuccess | WorkflowRunError | WorkflowRunTimeout;
|
|
79
|
+
export type WorkflowRunResult = WorkflowRunSuccess | WorkflowRunError | WorkflowRunTimeout | WorkflowRunStuck | WorkflowDeliveryFailed;
|
|
80
|
+
export type ChildWorkflowRunResult = WorkflowRunSuccess | WorkflowRunError | WorkflowRunTimeout | WorkflowRunStuck;
|
|
71
81
|
export type SteerRegistry = Map<string, (text: string) => void>;
|
|
72
82
|
export interface OrphanedSession {
|
|
73
83
|
readonly sessionId: string;
|
|
@@ -1081,6 +1081,16 @@ function makeSpawnAgentTool(sessionId, ctx, apiKey, thisWorkrailSessionId, curre
|
|
|
1081
1081
|
notes: childResult.message,
|
|
1082
1082
|
};
|
|
1083
1083
|
}
|
|
1084
|
+
else if (childResult._tag === 'stuck') {
|
|
1085
|
+
resultObj = {
|
|
1086
|
+
childSessionId,
|
|
1087
|
+
outcome: 'stuck',
|
|
1088
|
+
notes: childResult.message,
|
|
1089
|
+
...(childResult.issueSummaries !== undefined
|
|
1090
|
+
? { issueSummaries: childResult.issueSummaries }
|
|
1091
|
+
: {}),
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1084
1094
|
else {
|
|
1085
1095
|
(0, assert_never_js_1.assertNever)(childResult);
|
|
1086
1096
|
}
|
|
@@ -1093,6 +1103,31 @@ function makeSpawnAgentTool(sessionId, ctx, apiKey, thisWorkrailSessionId, curre
|
|
|
1093
1103
|
},
|
|
1094
1104
|
};
|
|
1095
1105
|
}
|
|
1106
|
+
async function writeStuckOutboxEntry(opts) {
|
|
1107
|
+
try {
|
|
1108
|
+
const outboxPath = path.join(os.homedir(), '.workrail', 'outbox.jsonl');
|
|
1109
|
+
await fs.mkdir(path.dirname(outboxPath), { recursive: true });
|
|
1110
|
+
const entry = JSON.stringify({
|
|
1111
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
1112
|
+
kind: 'stuck',
|
|
1113
|
+
message: `Session stuck (${opts.reason}): workflowId=${opts.workflowId}` +
|
|
1114
|
+
(opts.issueSummaries && opts.issueSummaries.length > 0
|
|
1115
|
+
? ` -- issues: ${opts.issueSummaries.join('; ')}`
|
|
1116
|
+
: ''),
|
|
1117
|
+
timestamp: new Date().toISOString(),
|
|
1118
|
+
workflowId: opts.workflowId,
|
|
1119
|
+
reason: opts.reason,
|
|
1120
|
+
...(opts.issueSummaries && opts.issueSummaries.length > 0
|
|
1121
|
+
? { issueSummaries: opts.issueSummaries }
|
|
1122
|
+
: {}),
|
|
1123
|
+
});
|
|
1124
|
+
await fs.appendFile(outboxPath, entry + '\n');
|
|
1125
|
+
}
|
|
1126
|
+
catch (err) {
|
|
1127
|
+
console.warn(`[WorkflowRunner] Could not write stuck outbox entry: ` +
|
|
1128
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1096
1131
|
async function appendIssueAsync(issuesDir, sessionId, record) {
|
|
1097
1132
|
await fs.mkdir(issuesDir, { recursive: true });
|
|
1098
1133
|
const filePath = path.join(issuesDir, `${sessionId}.jsonl`);
|
|
@@ -1449,19 +1484,6 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1449
1484
|
if (startContinueToken) {
|
|
1450
1485
|
await persistTokens(sessionId, startContinueToken, startCheckpointToken);
|
|
1451
1486
|
}
|
|
1452
|
-
if (trigger.botIdentity) {
|
|
1453
|
-
try {
|
|
1454
|
-
await execFileAsync('git', ['-C', trigger.workspacePath, 'config', 'user.name', trigger.botIdentity.name]);
|
|
1455
|
-
await execFileAsync('git', ['-C', trigger.workspacePath, 'config', 'user.email', trigger.botIdentity.email]);
|
|
1456
|
-
console.log(`[WorkflowRunner] Bot identity set: sessionId=${sessionId} ` +
|
|
1457
|
-
`name=${trigger.botIdentity.name} email=${trigger.botIdentity.email}`);
|
|
1458
|
-
}
|
|
1459
|
-
catch (identityErr) {
|
|
1460
|
-
console.warn(`[WorkflowRunner] WARNING: Failed to set bot identity for sessionId=${sessionId}: ` +
|
|
1461
|
-
`${identityErr instanceof Error ? identityErr.message : String(identityErr)}. ` +
|
|
1462
|
-
`Commits will use default git config.`);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
1487
|
let sessionWorkspacePath = trigger.workspacePath;
|
|
1466
1488
|
let sessionWorktreePath;
|
|
1467
1489
|
if (trigger.branchStrategy === 'worktree') {
|
|
@@ -1497,6 +1519,19 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1497
1519
|
};
|
|
1498
1520
|
}
|
|
1499
1521
|
}
|
|
1522
|
+
if (trigger.botIdentity) {
|
|
1523
|
+
try {
|
|
1524
|
+
await execFileAsync('git', ['-C', sessionWorkspacePath, 'config', 'user.name', trigger.botIdentity.name]);
|
|
1525
|
+
await execFileAsync('git', ['-C', sessionWorkspacePath, 'config', 'user.email', trigger.botIdentity.email]);
|
|
1526
|
+
console.log(`[WorkflowRunner] Bot identity set: sessionId=${sessionId} ` +
|
|
1527
|
+
`name=${trigger.botIdentity.name} email=${trigger.botIdentity.email}`);
|
|
1528
|
+
}
|
|
1529
|
+
catch (identityErr) {
|
|
1530
|
+
console.warn(`[WorkflowRunner] WARNING: Failed to set bot identity for sessionId=${sessionId}: ` +
|
|
1531
|
+
`${identityErr instanceof Error ? identityErr.message : String(identityErr)}. ` +
|
|
1532
|
+
`Commits will use default git config.`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1500
1535
|
if (firstStep.isComplete) {
|
|
1501
1536
|
await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
|
|
1502
1537
|
emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(workrailSessionId) });
|
|
@@ -1588,7 +1623,10 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1588
1623
|
});
|
|
1589
1624
|
const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
|
|
1590
1625
|
const maxTurns = trigger.agentConfig?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
1626
|
+
const sessionStartMs = Date.now();
|
|
1627
|
+
void sessionStartMs;
|
|
1591
1628
|
let timeoutReason = null;
|
|
1629
|
+
let stuckReason = null;
|
|
1592
1630
|
let turnCount = 0;
|
|
1593
1631
|
const unsubscribe = agent.subscribe(async (event) => {
|
|
1594
1632
|
if (event.type !== 'turn_end')
|
|
@@ -1623,6 +1661,17 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1623
1661
|
argsSummary: lastNToolCalls[0]?.argsSummary,
|
|
1624
1662
|
...withWorkrailSession(workrailSessionId),
|
|
1625
1663
|
});
|
|
1664
|
+
void writeStuckOutboxEntry({
|
|
1665
|
+
workflowId: trigger.workflowId,
|
|
1666
|
+
reason: 'repeated_tool_call',
|
|
1667
|
+
...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
|
|
1668
|
+
});
|
|
1669
|
+
const stuckPolicy = trigger.agentConfig?.stuckAbortPolicy ?? 'abort';
|
|
1670
|
+
if (stuckPolicy !== 'notify_only' && stuckReason === null && timeoutReason === null) {
|
|
1671
|
+
stuckReason = 'repeated_tool_call';
|
|
1672
|
+
agent.abort();
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1626
1675
|
}
|
|
1627
1676
|
if (maxTurns > 0 &&
|
|
1628
1677
|
turnCount >= Math.floor(maxTurns * 0.8) &&
|
|
@@ -1634,6 +1683,20 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1634
1683
|
detail: `${turnCount} turns used, 0 step advances (${maxTurns} turn limit)`,
|
|
1635
1684
|
...withWorkrailSession(workrailSessionId),
|
|
1636
1685
|
});
|
|
1686
|
+
const noProgressAbortEnabled = trigger.agentConfig?.noProgressAbortEnabled ?? false;
|
|
1687
|
+
if (noProgressAbortEnabled) {
|
|
1688
|
+
void writeStuckOutboxEntry({
|
|
1689
|
+
workflowId: trigger.workflowId,
|
|
1690
|
+
reason: 'no_progress',
|
|
1691
|
+
...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
|
|
1692
|
+
});
|
|
1693
|
+
const noProgressPolicy = trigger.agentConfig?.stuckAbortPolicy ?? 'abort';
|
|
1694
|
+
if (noProgressPolicy !== 'notify_only' && stuckReason === null && timeoutReason === null) {
|
|
1695
|
+
stuckReason = 'no_progress';
|
|
1696
|
+
agent.abort();
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1637
1700
|
}
|
|
1638
1701
|
if (timeoutReason !== null) {
|
|
1639
1702
|
emitter?.emit({
|
|
@@ -1693,6 +1756,26 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1693
1756
|
}
|
|
1694
1757
|
console.log(`[WorkflowRunner] Agent loop ended: sessionId=${sessionId} stopReason=${stopReason}${errorMessage ? ` error=${errorMessage.slice(0, 120)}` : ''}`);
|
|
1695
1758
|
}
|
|
1759
|
+
if (stuckReason !== null) {
|
|
1760
|
+
emitter?.emit({
|
|
1761
|
+
kind: 'session_completed',
|
|
1762
|
+
sessionId,
|
|
1763
|
+
workflowId: trigger.workflowId,
|
|
1764
|
+
outcome: 'timeout',
|
|
1765
|
+
detail: stuckReason,
|
|
1766
|
+
...withWorkrailSession(workrailSessionId),
|
|
1767
|
+
});
|
|
1768
|
+
if (workrailSessionId !== null)
|
|
1769
|
+
daemonRegistry?.unregister(workrailSessionId, 'failed');
|
|
1770
|
+
return {
|
|
1771
|
+
_tag: 'stuck',
|
|
1772
|
+
workflowId: trigger.workflowId,
|
|
1773
|
+
reason: stuckReason,
|
|
1774
|
+
message: `Session aborted: stuck heuristic fired (${stuckReason})`,
|
|
1775
|
+
stopReason: 'aborted',
|
|
1776
|
+
...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1696
1779
|
if (timeoutReason !== null) {
|
|
1697
1780
|
emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'timeout', detail: timeoutReason, ...withWorkrailSession(workrailSessionId) });
|
|
1698
1781
|
if (workrailSessionId !== null)
|