@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.
Files changed (40) hide show
  1. package/dist/cli/commands/index.d.ts +1 -0
  2. package/dist/cli/commands/index.js +3 -1
  3. package/dist/cli/commands/worktrain-pipeline.d.ts +17 -0
  4. package/dist/cli/commands/worktrain-pipeline.js +121 -0
  5. package/dist/console-ui/assets/{index-Bi38ITiQ.js → index-BQFhoMcY.js} +1 -1
  6. package/dist/console-ui/index.html +1 -1
  7. package/dist/coordinators/adaptive-pipeline.d.ts +57 -0
  8. package/dist/coordinators/adaptive-pipeline.js +104 -0
  9. package/dist/coordinators/modes/full-pipeline.d.ts +4 -0
  10. package/dist/coordinators/modes/full-pipeline.js +256 -0
  11. package/dist/coordinators/modes/implement-shared.d.ts +4 -0
  12. package/dist/coordinators/modes/implement-shared.js +201 -0
  13. package/dist/coordinators/modes/implement.d.ts +3 -0
  14. package/dist/coordinators/modes/implement.js +108 -0
  15. package/dist/coordinators/modes/quick-review.d.ts +3 -0
  16. package/dist/coordinators/modes/quick-review.js +37 -0
  17. package/dist/coordinators/modes/review-only.d.ts +2 -0
  18. package/dist/coordinators/modes/review-only.js +28 -0
  19. package/dist/coordinators/routing/route-task.d.ts +21 -0
  20. package/dist/coordinators/routing/route-task.js +55 -0
  21. package/dist/daemon/workflow-runner.d.ts +12 -2
  22. package/dist/daemon/workflow-runner.js +96 -13
  23. package/dist/manifest.json +101 -29
  24. package/dist/mcp/output-schemas.d.ts +16 -16
  25. package/dist/trigger/notification-service.d.ts +1 -1
  26. package/dist/trigger/notification-service.js +4 -0
  27. package/dist/trigger/trigger-router.d.ts +3 -0
  28. package/dist/trigger/trigger-router.js +17 -0
  29. package/dist/trigger/types.d.ts +2 -0
  30. package/dist/v2/durable-core/schemas/artifacts/discovery-handoff.d.ts +29 -0
  31. package/dist/v2/durable-core/schemas/artifacts/discovery-handoff.js +26 -0
  32. package/dist/v2/durable-core/schemas/artifacts/index.d.ts +2 -1
  33. package/dist/v2/durable-core/schemas/artifacts/index.js +7 -1
  34. package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +8 -8
  35. package/dist/v2/usecases/console-routes.js +3 -0
  36. package/docs/design/design-candidates-stuck-escalation.md +183 -0
  37. package/docs/design/design-review-findings-stuck-escalation.md +93 -0
  38. package/docs/design/implementation-plan-stuck-escalation.md +172 -0
  39. package/docs/ideas/backlog.md +86 -0
  40. package/package.json +1 -1
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COORDINATOR_MAX_MS = exports.COORDINATOR_SPAWN_CUTOFF_MS = exports.REVIEW_TIMEOUT_MS = exports.CODING_TIMEOUT_MS = exports.SHAPING_TIMEOUT_MS = exports.DISCOVERY_TIMEOUT_MS = void 0;
4
+ exports.checkSpawnCutoff = checkSpawnCutoff;
5
+ exports.runAdaptivePipeline = runAdaptivePipeline;
6
+ const route_task_js_1 = require("./routing/route-task.js");
7
+ exports.DISCOVERY_TIMEOUT_MS = 35 * 60 * 1000;
8
+ exports.SHAPING_TIMEOUT_MS = 35 * 60 * 1000;
9
+ exports.CODING_TIMEOUT_MS = 65 * 60 * 1000;
10
+ exports.REVIEW_TIMEOUT_MS = 25 * 60 * 1000;
11
+ exports.COORDINATOR_SPAWN_CUTOFF_MS = 100 * 60 * 1000;
12
+ exports.COORDINATOR_MAX_MS = 120 * 60 * 1000;
13
+ function checkSpawnCutoff(coordinatorStartMs, now, phase) {
14
+ if (now - coordinatorStartMs > exports.COORDINATOR_SPAWN_CUTOFF_MS) {
15
+ return {
16
+ kind: 'escalated',
17
+ escalationReason: {
18
+ phase,
19
+ reason: `coordinator elapsed > ${exports.COORDINATOR_SPAWN_CUTOFF_MS / 60000} minutes, refusing new spawns`,
20
+ },
21
+ };
22
+ }
23
+ return null;
24
+ }
25
+ async function runAdaptivePipeline(deps, opts, executors) {
26
+ const coordinatorStartMs = deps.now();
27
+ const triggerProvider = opts.taskCandidate !== undefined
28
+ ? 'github_prs_poll'
29
+ : opts.triggerProvider;
30
+ let pipelineMode;
31
+ if (opts.modeOverride) {
32
+ pipelineMode = buildModeFromOverride(opts.modeOverride, opts.goal, opts.workspace);
33
+ }
34
+ else {
35
+ pipelineMode = (0, route_task_js_1.routeTask)(opts.goal, opts.workspace, deps, triggerProvider);
36
+ }
37
+ const logEntry = {
38
+ timestamp: deps.nowIso(),
39
+ mode: pipelineMode.kind,
40
+ goal: opts.goal,
41
+ workspace: opts.workspace,
42
+ signals: {
43
+ depBumpKeyword: hasDepBumpKeyword(opts.goal),
44
+ prReference: hasPrReference(opts.goal),
45
+ pitchFilePresent: pipelineMode.kind === 'IMPLEMENT',
46
+ githubPrsPollProvider: triggerProvider === 'github_prs_poll',
47
+ modeOverride: opts.modeOverride ?? null,
48
+ },
49
+ };
50
+ const runsDir = opts.workspace + '/.workrail/pipeline-runs';
51
+ const logPath = runsDir + '/' + deps.nowIso().replace(/[:.]/g, '-') + '-' + pipelineMode.kind + '.json';
52
+ try {
53
+ await deps.mkdir(runsDir, { recursive: true });
54
+ await deps.writeFile(logPath, JSON.stringify(logEntry, null, 2) + '\n');
55
+ }
56
+ catch (e) {
57
+ deps.stderr(`[WARN adaptive-pipeline] Failed to write routing log: ${e instanceof Error ? e.message : String(e)}`);
58
+ }
59
+ deps.stderr(`[adaptive-pipeline] Routing: mode=${pipelineMode.kind} goal="${opts.goal.slice(0, 80)}"`);
60
+ if (opts.dryRun) {
61
+ deps.stderr(`[adaptive-pipeline] Dry run: would execute ${pipelineMode.kind} pipeline`);
62
+ return { kind: 'dry_run', mode: pipelineMode.kind };
63
+ }
64
+ switch (pipelineMode.kind) {
65
+ case 'QUICK_REVIEW':
66
+ return executors.runQuickReview(deps, opts, pipelineMode.prNumbers, coordinatorStartMs);
67
+ case 'REVIEW_ONLY':
68
+ return executors.runReviewOnly(deps, opts, pipelineMode.prNumbers, coordinatorStartMs);
69
+ case 'IMPLEMENT':
70
+ return executors.runImplement(deps, opts, pipelineMode.pitchPath, coordinatorStartMs);
71
+ case 'FULL':
72
+ return executors.runFull(deps, opts, coordinatorStartMs);
73
+ case 'ESCALATE': {
74
+ const reason = pipelineMode.reason;
75
+ deps.stderr(`[adaptive-pipeline] Routing escalation: ${reason}`);
76
+ await deps.postToOutbox(`Adaptive pipeline escalated during routing: ${reason}`, { goal: opts.goal, workspace: opts.workspace, phase: 'routing' });
77
+ return {
78
+ kind: 'escalated',
79
+ escalationReason: { phase: 'routing', reason },
80
+ };
81
+ }
82
+ }
83
+ }
84
+ function buildModeFromOverride(override, goal, workspace) {
85
+ const prNumbers = (0, route_task_js_1.extractPrNumbers)(goal);
86
+ const pitchPath = workspace + '/.workrail/current-pitch.md';
87
+ switch (override) {
88
+ case 'QUICK_REVIEW':
89
+ return { kind: 'QUICK_REVIEW', prNumbers };
90
+ case 'REVIEW_ONLY':
91
+ return { kind: 'REVIEW_ONLY', prNumbers };
92
+ case 'IMPLEMENT':
93
+ return { kind: 'IMPLEMENT', pitchPath };
94
+ case 'FULL':
95
+ return { kind: 'FULL', goal };
96
+ }
97
+ }
98
+ function hasDepBumpKeyword(goal) {
99
+ const lower = goal.toLowerCase();
100
+ return ['bump', 'chore:', 'dependabot', 'dependency upgrade'].some((kw) => lower.includes(kw));
101
+ }
102
+ function hasPrReference(goal) {
103
+ return /\bPR\s*#\d+\b/i.test(goal) || /\bMR\s*!?\d+\b/i.test(goal);
104
+ }
@@ -0,0 +1,4 @@
1
+ import type { AdaptiveCoordinatorDeps, AdaptivePipelineOpts, PipelineOutcome } from '../adaptive-pipeline.js';
2
+ import { type DiscoveryHandoffArtifactV1 } from '../../v2/durable-core/schemas/artifacts/discovery-handoff.js';
3
+ export declare function renderHandoff(artifact: DiscoveryHandoffArtifactV1): string;
4
+ export declare function runFullPipeline(deps: AdaptiveCoordinatorDeps, opts: AdaptivePipelineOpts, coordinatorStartMs: number): Promise<PipelineOutcome>;
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderHandoff = renderHandoff;
4
+ exports.runFullPipeline = runFullPipeline;
5
+ const adaptive_pipeline_js_1 = require("../adaptive-pipeline.js");
6
+ const discovery_handoff_js_1 = require("../../v2/durable-core/schemas/artifacts/discovery-handoff.js");
7
+ const implement_shared_js_1 = require("./implement-shared.js");
8
+ const implement_js_1 = require("./implement.js");
9
+ const PR_POLL_TIMEOUT_MS = 5 * 60 * 1000;
10
+ const UX_GATE_ACK_TIMEOUT_MS = 24 * 60 * 60 * 1000;
11
+ const MIN_NOTES_LENGTH_FOR_FALLBACK = 50;
12
+ function renderHandoff(artifact) {
13
+ const lines = [
14
+ `## Discovery Handoff`,
15
+ ``,
16
+ `**Selected Direction:** ${artifact.selectedDirection}`,
17
+ `**Confidence:** ${artifact.confidenceBand}`,
18
+ ];
19
+ if (artifact.designDocPath) {
20
+ lines.push(`**Design Doc:** ${artifact.designDocPath}`);
21
+ }
22
+ if (artifact.keyInvariants.length > 0) {
23
+ lines.push(``, `**Key Invariants:**`);
24
+ for (const invariant of artifact.keyInvariants) {
25
+ lines.push(`- ${invariant}`);
26
+ }
27
+ }
28
+ return lines.join('\n');
29
+ }
30
+ function readDiscoveryHandoffArtifact(artifacts, sessionHandle, stderrFn) {
31
+ const handlePrefix = sessionHandle.slice(0, 16);
32
+ for (const raw of artifacts) {
33
+ if (!(0, discovery_handoff_js_1.isDiscoveryHandoffArtifact)(raw))
34
+ continue;
35
+ const result = discovery_handoff_js_1.DiscoveryHandoffArtifactV1Schema.safeParse(raw);
36
+ if (!result.success) {
37
+ const issues = result.error.issues
38
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
39
+ .join('; ');
40
+ stderrFn(`[WARN full-pipeline handle=${handlePrefix}] discovery handoff schema validation failed: ${issues}`);
41
+ continue;
42
+ }
43
+ return result.data;
44
+ }
45
+ return null;
46
+ }
47
+ async function runFullPipeline(deps, opts, coordinatorStartMs) {
48
+ deps.stderr(`[full-pipeline] Starting FULL pipeline for workspace=${opts.workspace}`);
49
+ const pitchPath = opts.workspace + '/.workrail/current-pitch.md';
50
+ const archiveDir = opts.workspace + '/.workrail/used-pitches';
51
+ const archiveTimestamp = deps.nowIso().replace(/[:.]/g, '-');
52
+ const archivePath = archiveDir + '/pitch-' + archiveTimestamp + '.md';
53
+ let outcome;
54
+ try {
55
+ outcome = await runFullPipelineCore(deps, opts, coordinatorStartMs);
56
+ }
57
+ finally {
58
+ try {
59
+ await deps.mkdir(archiveDir, { recursive: true });
60
+ await deps.archiveFile(pitchPath, archivePath);
61
+ deps.stderr(`[full-pipeline] Pitch archived to ${archivePath}`);
62
+ }
63
+ catch (e) {
64
+ deps.stderr(`[WARN full-pipeline] Failed to archive pitch.md: ${e instanceof Error ? e.message : String(e)}`);
65
+ }
66
+ }
67
+ return outcome;
68
+ }
69
+ async function runFullPipelineCore(deps, opts, coordinatorStartMs) {
70
+ const discoveryCutoff = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'discovery');
71
+ if (discoveryCutoff)
72
+ return discoveryCutoff;
73
+ deps.stderr(`[full-pipeline] Spawning wr.discovery session`);
74
+ const discoverySpawnResult = await deps.spawnSession('wr.discovery', opts.goal, opts.workspace);
75
+ if (discoverySpawnResult.kind === 'err') {
76
+ return {
77
+ kind: 'escalated',
78
+ escalationReason: {
79
+ phase: 'discovery',
80
+ reason: `discovery session spawn failed: ${discoverySpawnResult.error}`,
81
+ },
82
+ };
83
+ }
84
+ const discoveryHandle = discoverySpawnResult.value;
85
+ if (!discoveryHandle) {
86
+ return {
87
+ kind: 'escalated',
88
+ escalationReason: { phase: 'discovery', reason: 'discovery session returned empty handle' },
89
+ };
90
+ }
91
+ const discoveryAwait = await deps.awaitSessions([discoveryHandle], adaptive_pipeline_js_1.DISCOVERY_TIMEOUT_MS);
92
+ const discoveryResult = discoveryAwait.results[0];
93
+ if (!discoveryResult || discoveryResult.outcome !== 'success') {
94
+ const outcome = discoveryResult?.outcome ?? 'not_found';
95
+ return {
96
+ kind: 'escalated',
97
+ escalationReason: { phase: 'discovery', reason: `discovery session ${outcome}` },
98
+ };
99
+ }
100
+ deps.stderr(`[full-pipeline] Discovery session completed`);
101
+ const discoveryAgentResult = await deps.getAgentResult(discoveryHandle);
102
+ const handoffArtifact = readDiscoveryHandoffArtifact(discoveryAgentResult.artifacts, discoveryHandle, deps.stderr);
103
+ let shapingContext;
104
+ if (handoffArtifact !== null) {
105
+ deps.stderr(`[full-pipeline] Discovery handoff artifact found -- injecting structured context`);
106
+ shapingContext = {
107
+ selectedDirection: handoffArtifact.selectedDirection,
108
+ designDocPath: handoffArtifact.designDocPath,
109
+ assembledContextSummary: renderHandoff(handoffArtifact),
110
+ };
111
+ }
112
+ else {
113
+ const notes = discoveryAgentResult.recapMarkdown;
114
+ if (notes !== null && notes.trim().length > MIN_NOTES_LENGTH_FOR_FALLBACK) {
115
+ deps.stderr(`[full-pipeline] No handoff artifact -- using lastStepNotes as context (length=${notes.trim().length})`);
116
+ shapingContext = { assembledContextSummary: notes.trim() };
117
+ }
118
+ else {
119
+ const reason = notes === null ? 'null' : `length=${notes.trim().length} <= ${MIN_NOTES_LENGTH_FOR_FALLBACK}`;
120
+ deps.stderr(`[full-pipeline] No handoff artifact and notes too short (${reason}) -- proceeding without context`);
121
+ shapingContext = {};
122
+ }
123
+ }
124
+ const shapingCutoff = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'shaping');
125
+ if (shapingCutoff)
126
+ return shapingCutoff;
127
+ deps.stderr(`[full-pipeline] Spawning wr.shaping session`);
128
+ const shapingSpawnResult = await deps.spawnSession('wr.shaping', opts.goal, opts.workspace, shapingContext);
129
+ if (shapingSpawnResult.kind === 'err') {
130
+ return {
131
+ kind: 'escalated',
132
+ escalationReason: {
133
+ phase: 'shaping',
134
+ reason: `shaping session spawn failed: ${shapingSpawnResult.error}`,
135
+ },
136
+ };
137
+ }
138
+ const shapingHandle = shapingSpawnResult.value;
139
+ if (!shapingHandle) {
140
+ return {
141
+ kind: 'escalated',
142
+ escalationReason: { phase: 'shaping', reason: 'shaping session returned empty handle' },
143
+ };
144
+ }
145
+ const shapingAwait = await deps.awaitSessions([shapingHandle], adaptive_pipeline_js_1.SHAPING_TIMEOUT_MS);
146
+ const shapingResult = shapingAwait.results[0];
147
+ if (!shapingResult || shapingResult.outcome !== 'success') {
148
+ const outcome = shapingResult?.outcome ?? 'not_found';
149
+ return {
150
+ kind: 'escalated',
151
+ escalationReason: { phase: 'shaping', reason: `shaping session ${outcome}` },
152
+ };
153
+ }
154
+ deps.stderr(`[full-pipeline] Shaping session completed`);
155
+ if ((0, implement_js_1.touchesUI)(opts.goal)) {
156
+ deps.stderr(`[full-pipeline] UX signals detected -- dispatching ui-ux-design-workflow`);
157
+ const uxCutoff = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'ux-gate');
158
+ if (uxCutoff)
159
+ return uxCutoff;
160
+ const uxSpawnResult = await deps.spawnSession('ui-ux-design-workflow', opts.goal, opts.workspace, { shapingComplete: true });
161
+ if (uxSpawnResult.kind === 'err') {
162
+ return {
163
+ kind: 'escalated',
164
+ escalationReason: {
165
+ phase: 'ux-gate',
166
+ reason: `UX design workflow spawn failed: ${uxSpawnResult.error}`,
167
+ },
168
+ };
169
+ }
170
+ const uxHandle = uxSpawnResult.value;
171
+ if (!uxHandle) {
172
+ return {
173
+ kind: 'escalated',
174
+ escalationReason: { phase: 'ux-gate', reason: 'UX design session returned empty handle' },
175
+ };
176
+ }
177
+ const uxAwait = await deps.awaitSessions([uxHandle], adaptive_pipeline_js_1.REVIEW_TIMEOUT_MS);
178
+ const uxResult = uxAwait.results[0];
179
+ if (!uxResult || uxResult.outcome !== 'success') {
180
+ const outcome = uxResult?.outcome ?? 'not_found';
181
+ return {
182
+ kind: 'escalated',
183
+ escalationReason: { phase: 'ux-gate', reason: `UX design session ${outcome}` },
184
+ };
185
+ }
186
+ deps.stderr(`[full-pipeline] UX design session completed -- requesting human acknowledgment`);
187
+ const ackRequestId = deps.generateId();
188
+ await deps.postToOutbox(`UX design complete for "${opts.goal}" -- please review and acknowledge before coding starts`, {
189
+ requestId: ackRequestId,
190
+ goal: opts.goal,
191
+ workspace: opts.workspace,
192
+ phase: 'ux-gate',
193
+ uxSessionHandle: uxHandle,
194
+ note: 'Acknowledge this message to allow coding to begin. No response in 24h = escalation.',
195
+ });
196
+ const ackResult = await deps.pollOutboxAck(ackRequestId, UX_GATE_ACK_TIMEOUT_MS);
197
+ if (ackResult === 'timeout') {
198
+ await deps.postToOutbox(`UX gate timed out: no acknowledgment received within 24 hours for "${opts.goal}"`, { requestId: ackRequestId, goal: opts.goal, phase: 'ux-gate-timeout' });
199
+ return {
200
+ kind: 'escalated',
201
+ escalationReason: {
202
+ phase: 'ux-gate',
203
+ reason: 'no human acknowledgment received within 24 hours',
204
+ },
205
+ };
206
+ }
207
+ deps.stderr(`[full-pipeline] UX gate acknowledged -- proceeding to coding`);
208
+ }
209
+ const codingCutoff = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'coding');
210
+ if (codingCutoff)
211
+ return codingCutoff;
212
+ deps.stderr(`[full-pipeline] Spawning coding-task-workflow-agentic`);
213
+ const codingSpawnResult = await deps.spawnSession('coding-task-workflow-agentic', opts.goal, opts.workspace, {
214
+ pitchPath: opts.workspace + '/.workrail/current-pitch.md',
215
+ });
216
+ if (codingSpawnResult.kind === 'err') {
217
+ return {
218
+ kind: 'escalated',
219
+ escalationReason: {
220
+ phase: 'coding',
221
+ reason: `coding session spawn failed: ${codingSpawnResult.error}`,
222
+ },
223
+ };
224
+ }
225
+ const codingHandle = codingSpawnResult.value;
226
+ if (!codingHandle) {
227
+ return {
228
+ kind: 'escalated',
229
+ escalationReason: { phase: 'coding', reason: 'coding session returned empty handle' },
230
+ };
231
+ }
232
+ const codingAwait = await deps.awaitSessions([codingHandle], adaptive_pipeline_js_1.CODING_TIMEOUT_MS);
233
+ const codingResult = codingAwait.results[0];
234
+ if (!codingResult || codingResult.outcome !== 'success') {
235
+ const outcome = codingResult?.outcome ?? 'not_found';
236
+ return {
237
+ kind: 'escalated',
238
+ escalationReason: { phase: 'coding', reason: `coding session ${outcome}` },
239
+ };
240
+ }
241
+ deps.stderr(`[full-pipeline] Coding session completed`);
242
+ const branchPattern = `worktrain/${codingHandle.slice(0, 16)}`;
243
+ deps.stderr(`[full-pipeline] Polling for PR on branch pattern: ${branchPattern}`);
244
+ const prUrl = await deps.pollForPR(branchPattern, PR_POLL_TIMEOUT_MS);
245
+ if (!prUrl) {
246
+ return {
247
+ kind: 'escalated',
248
+ escalationReason: {
249
+ phase: 'pr-detection',
250
+ reason: `no PR found matching ${branchPattern} within ${PR_POLL_TIMEOUT_MS / 60000} minutes`,
251
+ },
252
+ };
253
+ }
254
+ deps.stderr(`[full-pipeline] PR detected: ${prUrl}`);
255
+ return (0, implement_shared_js_1.runReviewAndVerdictCycle)(deps, opts, prUrl, coordinatorStartMs, 0);
256
+ }
@@ -0,0 +1,4 @@
1
+ import type { AdaptiveCoordinatorDeps, AdaptivePipelineOpts, PipelineOutcome } from '../adaptive-pipeline.js';
2
+ export declare const MAX_FIX_ITERATIONS = 2;
3
+ export declare function runReviewAndVerdictCycle(deps: AdaptiveCoordinatorDeps, opts: AdaptivePipelineOpts, prUrl: string, coordinatorStartMs: number, iteration: number): Promise<PipelineOutcome>;
4
+ export declare function runAuditChain(deps: AdaptiveCoordinatorDeps, opts: AdaptivePipelineOpts, prUrl: string, coordinatorStartMs: number, severity: 'blocking' | 'unknown'): Promise<PipelineOutcome>;
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_FIX_ITERATIONS = void 0;
4
+ exports.runReviewAndVerdictCycle = runReviewAndVerdictCycle;
5
+ exports.runAuditChain = runAuditChain;
6
+ const adaptive_pipeline_js_1 = require("../adaptive-pipeline.js");
7
+ const pr_review_js_1 = require("../pr-review.js");
8
+ exports.MAX_FIX_ITERATIONS = 2;
9
+ async function runReviewAndVerdictCycle(deps, opts, prUrl, coordinatorStartMs, iteration) {
10
+ const cutoffCheck = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'review');
11
+ if (cutoffCheck)
12
+ return cutoffCheck;
13
+ const reviewGoal = iteration === 0
14
+ ? `Review PR for merge: ${prUrl}`
15
+ : `Re-review PR after fixes (iteration ${iteration}): ${prUrl}`;
16
+ deps.stderr(`[review-cycle] Spawning review session (iteration=${iteration}): ${reviewGoal.slice(0, 80)}`);
17
+ const reviewSpawnResult = await deps.spawnSession('mr-review-workflow-agentic', reviewGoal, opts.workspace, { prUrl });
18
+ if (reviewSpawnResult.kind === 'err') {
19
+ return {
20
+ kind: 'escalated',
21
+ escalationReason: { phase: 'review', reason: `review session spawn failed: ${reviewSpawnResult.error}` },
22
+ };
23
+ }
24
+ const reviewHandle = reviewSpawnResult.value;
25
+ if (!reviewHandle) {
26
+ return {
27
+ kind: 'escalated',
28
+ escalationReason: { phase: 'review', reason: 'review session returned empty handle' },
29
+ };
30
+ }
31
+ const reviewAwait = await deps.awaitSessions([reviewHandle], adaptive_pipeline_js_1.REVIEW_TIMEOUT_MS);
32
+ const reviewResult = reviewAwait.results[0];
33
+ if (!reviewResult || reviewResult.outcome !== 'success') {
34
+ const outcome = reviewResult?.outcome ?? 'not_found';
35
+ return {
36
+ kind: 'escalated',
37
+ escalationReason: { phase: 'review', reason: `review session ${outcome}` },
38
+ };
39
+ }
40
+ const agentResult = await deps.getAgentResult(reviewHandle);
41
+ const verdictFromArtifact = (0, pr_review_js_1.readVerdictArtifact)(agentResult.artifacts, reviewHandle);
42
+ const findingsResult = verdictFromArtifact !== null
43
+ ? { kind: 'ok', value: verdictFromArtifact }
44
+ : (0, pr_review_js_1.parseFindingsFromNotes)(agentResult.recapMarkdown);
45
+ if (findingsResult.kind === 'err') {
46
+ return {
47
+ kind: 'escalated',
48
+ escalationReason: { phase: 'review', reason: `review verdict parse failed: ${findingsResult.error}` },
49
+ };
50
+ }
51
+ const findings = findingsResult.value;
52
+ deps.stderr(`[review-cycle] Verdict: ${findings.severity} (iteration=${iteration})`);
53
+ switch (findings.severity) {
54
+ case 'clean':
55
+ deps.stderr(`[review-cycle] Verdict clean -- merging PR`);
56
+ return { kind: 'merged', prUrl };
57
+ case 'minor': {
58
+ if (iteration >= exports.MAX_FIX_ITERATIONS) {
59
+ deps.stderr(`[review-cycle] ${exports.MAX_FIX_ITERATIONS} fix iterations exhausted -- escalating`);
60
+ await deps.postToOutbox(`Adaptive pipeline escalated: fix loop exhausted after ${exports.MAX_FIX_ITERATIONS} iterations`, { prUrl, phase: 'fix-loop', reason: 'max iterations reached', findingSummaries: findings.findingSummaries });
61
+ return {
62
+ kind: 'escalated',
63
+ escalationReason: { phase: 'fix-loop', reason: `${exports.MAX_FIX_ITERATIONS} fix iterations exhausted` },
64
+ };
65
+ }
66
+ deps.stderr(`[review-cycle] Verdict minor -- running fix iteration ${iteration + 1}/${exports.MAX_FIX_ITERATIONS}`);
67
+ const fixCutoff = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'fix-agent');
68
+ if (fixCutoff)
69
+ return fixCutoff;
70
+ const fixGoal = `Fix review findings: ${findings.findingSummaries.slice(0, 3).join('; ')}`;
71
+ const fixSpawnResult = await deps.spawnSession('coding-task-workflow-agentic', fixGoal, opts.workspace, { prUrl, findings: findings.findingSummaries });
72
+ if (fixSpawnResult.kind === 'err') {
73
+ return {
74
+ kind: 'escalated',
75
+ escalationReason: { phase: 'fix-agent', reason: `fix agent spawn failed: ${fixSpawnResult.error}` },
76
+ };
77
+ }
78
+ const fixHandle = fixSpawnResult.value;
79
+ if (!fixHandle) {
80
+ return {
81
+ kind: 'escalated',
82
+ escalationReason: { phase: 'fix-agent', reason: 'fix agent returned empty handle' },
83
+ };
84
+ }
85
+ const fixAwait = await deps.awaitSessions([fixHandle], adaptive_pipeline_js_1.CODING_TIMEOUT_MS);
86
+ const fixResult = fixAwait.results[0];
87
+ if (!fixResult || fixResult.outcome !== 'success') {
88
+ const outcome = fixResult?.outcome ?? 'not_found';
89
+ return {
90
+ kind: 'escalated',
91
+ escalationReason: { phase: 'fix-agent', reason: `fix agent ${outcome}` },
92
+ };
93
+ }
94
+ deps.stderr(`[review-cycle] Fix iteration ${iteration + 1} complete -- re-reviewing`);
95
+ return runReviewAndVerdictCycle(deps, opts, prUrl, coordinatorStartMs, iteration + 1);
96
+ }
97
+ case 'blocking':
98
+ case 'unknown': {
99
+ return runAuditChain(deps, opts, prUrl, coordinatorStartMs, findings.severity);
100
+ }
101
+ }
102
+ }
103
+ async function runAuditChain(deps, opts, prUrl, coordinatorStartMs, severity) {
104
+ deps.stderr(`[audit-chain] ${severity.toUpperCase()} finding -- running audit chain`);
105
+ const auditCutoff = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 'audit');
106
+ if (auditCutoff)
107
+ return auditCutoff;
108
+ const auditWorkflow = 'production-readiness-audit';
109
+ const auditSpawnResult = await deps.spawnSession(auditWorkflow, `Audit PR before merge: ${prUrl}`, opts.workspace, { prUrl, severity });
110
+ if (auditSpawnResult.kind === 'err') {
111
+ await deps.postToOutbox(`Adaptive pipeline escalated: audit workflow failed to spawn`, { prUrl, phase: 'audit', reason: auditSpawnResult.error, severity });
112
+ return {
113
+ kind: 'escalated',
114
+ escalationReason: { phase: 'audit', reason: `audit spawn failed: ${auditSpawnResult.error}` },
115
+ };
116
+ }
117
+ const auditHandle = auditSpawnResult.value;
118
+ if (!auditHandle) {
119
+ await deps.postToOutbox(`Adaptive pipeline escalated: audit returned empty handle`, { prUrl, phase: 'audit', severity });
120
+ return {
121
+ kind: 'escalated',
122
+ escalationReason: { phase: 'audit', reason: 'audit returned empty handle' },
123
+ };
124
+ }
125
+ const auditAwait = await deps.awaitSessions([auditHandle], adaptive_pipeline_js_1.REVIEW_TIMEOUT_MS);
126
+ const auditResult = auditAwait.results[0];
127
+ if (!auditResult || auditResult.outcome !== 'success') {
128
+ const outcome = auditResult?.outcome ?? 'not_found';
129
+ await deps.postToOutbox(`Adaptive pipeline escalated: audit session ${outcome}`, { prUrl, phase: 'audit', auditOutcome: outcome, severity });
130
+ return {
131
+ kind: 'escalated',
132
+ escalationReason: { phase: 'audit', reason: `audit session ${outcome}` },
133
+ };
134
+ }
135
+ deps.stderr(`[audit-chain] Audit complete -- re-reviewing PR`);
136
+ const reReviewCutoff = (0, adaptive_pipeline_js_1.checkSpawnCutoff)(coordinatorStartMs, deps.now(), 're-review-after-audit');
137
+ if (reReviewCutoff)
138
+ return reReviewCutoff;
139
+ const reReviewSpawnResult = await deps.spawnSession('mr-review-workflow-agentic', `Re-review after audit: ${prUrl}`, opts.workspace, { prUrl, auditComplete: true });
140
+ if (reReviewSpawnResult.kind === 'err') {
141
+ await deps.postToOutbox(`Adaptive pipeline escalated: re-review after audit failed to spawn`, { prUrl, phase: 're-review-after-audit', reason: reReviewSpawnResult.error });
142
+ return {
143
+ kind: 'escalated',
144
+ escalationReason: {
145
+ phase: 're-review-after-audit',
146
+ reason: `re-review spawn failed: ${reReviewSpawnResult.error}`,
147
+ },
148
+ };
149
+ }
150
+ const reReviewHandle = reReviewSpawnResult.value;
151
+ if (!reReviewHandle) {
152
+ await deps.postToOutbox(`Adaptive pipeline escalated: re-review after audit returned empty handle`, { prUrl, phase: 're-review-after-audit' });
153
+ return {
154
+ kind: 'escalated',
155
+ escalationReason: { phase: 're-review-after-audit', reason: 're-review returned empty handle' },
156
+ };
157
+ }
158
+ const reReviewAwait = await deps.awaitSessions([reReviewHandle], adaptive_pipeline_js_1.REVIEW_TIMEOUT_MS);
159
+ const reReviewResult = reReviewAwait.results[0];
160
+ if (!reReviewResult || reReviewResult.outcome !== 'success') {
161
+ const outcome = reReviewResult?.outcome ?? 'not_found';
162
+ await deps.postToOutbox(`Adaptive pipeline escalated: re-review after audit session ${outcome}`, { prUrl, phase: 're-review-after-audit', reReviewOutcome: outcome });
163
+ return {
164
+ kind: 'escalated',
165
+ escalationReason: { phase: 're-review-after-audit', reason: `re-review session ${outcome}` },
166
+ };
167
+ }
168
+ const reAgentResult = await deps.getAgentResult(reReviewHandle);
169
+ const reVerdictFromArtifact = (0, pr_review_js_1.readVerdictArtifact)(reAgentResult.artifacts, reReviewHandle);
170
+ const reFindingsResult = reVerdictFromArtifact !== null
171
+ ? { kind: 'ok', value: reVerdictFromArtifact }
172
+ : (0, pr_review_js_1.parseFindingsFromNotes)(reAgentResult.recapMarkdown);
173
+ if (reFindingsResult.kind === 'err') {
174
+ await deps.postToOutbox(`Adaptive pipeline escalated: re-review verdict unparseable after audit`, { prUrl, phase: 're-review-after-audit' });
175
+ return {
176
+ kind: 'escalated',
177
+ escalationReason: { phase: 're-review-after-audit', reason: `re-review verdict parse failed` },
178
+ };
179
+ }
180
+ const reFindings = reFindingsResult.value;
181
+ deps.stderr(`[audit-chain] Post-audit re-review verdict: ${reFindings.severity}`);
182
+ if (reFindings.severity === 'clean' || reFindings.severity === 'minor') {
183
+ deps.stderr(`[audit-chain] Post-audit verdict acceptable (${reFindings.severity}) -- merging`);
184
+ return { kind: 'merged', prUrl };
185
+ }
186
+ deps.stderr(`[audit-chain] Post-audit verdict still ${reFindings.severity} -- escalating to Human Outbox`);
187
+ await deps.postToOutbox(`PR requires human review: still ${reFindings.severity} after production-readiness audit`, {
188
+ prUrl,
189
+ phase: 'audit-chain-complete',
190
+ severity: reFindings.severity,
191
+ findingSummaries: reFindings.findingSummaries,
192
+ note: 'Do NOT auto-merge. Human review required.',
193
+ });
194
+ return {
195
+ kind: 'escalated',
196
+ escalationReason: {
197
+ phase: 'audit-chain',
198
+ reason: `PR still ${reFindings.severity} after audit -- posted to Human Outbox, do NOT merge`,
199
+ },
200
+ };
201
+ }
@@ -0,0 +1,3 @@
1
+ import type { AdaptiveCoordinatorDeps, AdaptivePipelineOpts, PipelineOutcome } from '../adaptive-pipeline.js';
2
+ export declare function touchesUI(goal: string): boolean;
3
+ export declare function runImplementPipeline(deps: AdaptiveCoordinatorDeps, opts: AdaptivePipelineOpts, pitchPath: string, coordinatorStartMs: number): Promise<PipelineOutcome>;