steroids-cli 0.10.39 → 0.10.41
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/commands/loop-phases-coder-decision.d.ts +27 -0
- package/dist/commands/loop-phases-coder-decision.d.ts.map +1 -0
- package/dist/commands/loop-phases-coder-decision.js +246 -0
- package/dist/commands/loop-phases-coder-decision.js.map +1 -0
- package/dist/commands/loop-phases-coder.d.ts +7 -0
- package/dist/commands/loop-phases-coder.d.ts.map +1 -0
- package/dist/commands/loop-phases-coder.js +343 -0
- package/dist/commands/loop-phases-coder.js.map +1 -0
- package/dist/commands/loop-phases-helpers.d.ts +79 -0
- package/dist/commands/loop-phases-helpers.d.ts.map +1 -0
- package/dist/commands/loop-phases-helpers.js +284 -0
- package/dist/commands/loop-phases-helpers.js.map +1 -0
- package/dist/commands/loop-phases-reviewer-resolution.d.ts +22 -0
- package/dist/commands/loop-phases-reviewer-resolution.d.ts.map +1 -0
- package/dist/commands/loop-phases-reviewer-resolution.js +188 -0
- package/dist/commands/loop-phases-reviewer-resolution.js.map +1 -0
- package/dist/commands/loop-phases-reviewer.d.ts +7 -0
- package/dist/commands/loop-phases-reviewer.d.ts.map +1 -0
- package/dist/commands/loop-phases-reviewer.js +331 -0
- package/dist/commands/loop-phases-reviewer.js.map +1 -0
- package/dist/commands/loop-phases.d.ts +13 -19
- package/dist/commands/loop-phases.d.ts.map +1 -1
- package/dist/commands/loop-phases.js +13 -1283
- package/dist/commands/loop-phases.js.map +1 -1
- package/dist/orchestrator/coder.d.ts.map +1 -1
- package/dist/orchestrator/coder.js +6 -0
- package/dist/orchestrator/coder.js.map +1 -1
- package/dist/orchestrator/coordinator.d.ts.map +1 -1
- package/dist/orchestrator/coordinator.js +2 -1
- package/dist/orchestrator/coordinator.js.map +1 -1
- package/dist/orchestrator/post-coder.d.ts.map +1 -1
- package/dist/orchestrator/post-coder.js +2 -0
- package/dist/orchestrator/post-coder.js.map +1 -1
- package/dist/orchestrator/post-reviewer.d.ts.map +1 -1
- package/dist/orchestrator/post-reviewer.js +9 -4
- package/dist/orchestrator/post-reviewer.js.map +1 -1
- package/dist/parallel/clone.d.ts.map +1 -1
- package/dist/parallel/clone.js +16 -2
- package/dist/parallel/clone.js.map +1 -1
- package/dist/prompts/coder.d.ts +2 -0
- package/dist/prompts/coder.d.ts.map +1 -1
- package/dist/prompts/coder.js +28 -11
- package/dist/prompts/coder.js.map +1 -1
- package/dist/prompts/prompt-helpers.d.ts +4 -2
- package/dist/prompts/prompt-helpers.d.ts.map +1 -1
- package/dist/prompts/prompt-helpers.js +27 -6
- package/dist/prompts/prompt-helpers.js.map +1 -1
- package/dist/prompts/reviewer.d.ts.map +1 -1
- package/dist/prompts/reviewer.js +9 -4
- package/dist/prompts/reviewer.js.map +1 -1
- package/dist/runners/heartbeat.d.ts.map +1 -1
- package/dist/runners/heartbeat.js +11 -5
- package/dist/runners/heartbeat.js.map +1 -1
- package/dist/runners/orchestrator-loop.d.ts.map +1 -1
- package/dist/runners/orchestrator-loop.js +19 -14
- package/dist/runners/orchestrator-loop.js.map +1 -1
- package/dist/runners/wakeup.d.ts.map +1 -1
- package/dist/runners/wakeup.js +7 -11
- package/dist/runners/wakeup.js.map +1 -1
- package/dist/workspace/git-lifecycle.d.ts.map +1 -1
- package/dist/workspace/git-lifecycle.js +33 -1
- package/dist/workspace/git-lifecycle.js.map +1 -1
- package/dist/workspace/pool.d.ts +2 -1
- package/dist/workspace/pool.d.ts.map +1 -1
- package/dist/workspace/pool.js +13 -5
- package/dist/workspace/pool.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,1287 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runCoderPhase = runCoderPhase;
|
|
4
|
-
exports.runReviewerPhase = runReviewerPhase;
|
|
5
|
-
const global_db_js_1 = require("../runners/global-db.js");
|
|
6
|
-
/**
|
|
7
|
-
* Loop phase functions for coder and reviewer invocation
|
|
8
|
-
* ORCHESTRATOR-DRIVEN: The orchestrator makes ALL status decisions
|
|
9
|
-
*/
|
|
10
|
-
const node_child_process_1 = require("node:child_process");
|
|
11
|
-
const queries_js_1 = require("../database/queries.js");
|
|
12
|
-
const coder_js_1 = require("../orchestrator/coder.js");
|
|
13
|
-
const reviewer_js_1 = require("../orchestrator/reviewer.js");
|
|
14
|
-
const coordinator_js_1 = require("../orchestrator/coordinator.js");
|
|
15
|
-
const push_js_1 = require("../git/push.js");
|
|
16
|
-
const status_js_1 = require("../git/status.js");
|
|
17
|
-
const submission_resolution_js_1 = require("../git/submission-resolution.js");
|
|
18
|
-
const invoke_js_1 = require("../orchestrator/invoke.js");
|
|
19
|
-
const fallback_handler_js_1 = require("../orchestrator/fallback-handler.js");
|
|
20
|
-
const loader_js_1 = require("../config/loader.js");
|
|
21
|
-
const registry_js_1 = require("../providers/registry.js");
|
|
22
|
-
const git_lifecycle_js_1 = require("../workspace/git-lifecycle.js");
|
|
23
|
-
const pool_js_1 = require("../workspace/pool.js");
|
|
24
|
-
const merge_pipeline_js_1 = require("../workspace/merge-pipeline.js");
|
|
25
|
-
const WORKSTREAM_LEASE_TTL_SECONDS = 120;
|
|
26
|
-
const LEASE_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
27
|
-
function refreshParallelWorkstreamLease(projectPath, leaseFence) {
|
|
28
|
-
if (!leaseFence?.parallelSessionId) {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
return (0, global_db_js_1.withGlobalDatabase)((db) => {
|
|
32
|
-
const row = db
|
|
33
|
-
.prepare(`SELECT id, claim_generation, runner_id
|
|
34
|
-
FROM workstreams
|
|
35
|
-
WHERE session_id = ?
|
|
36
|
-
AND clone_path = ?
|
|
37
|
-
AND status = 'running'
|
|
38
|
-
LIMIT 1`)
|
|
39
|
-
.get(leaseFence.parallelSessionId, projectPath);
|
|
40
|
-
if (!row) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
const owner = leaseFence.runnerId ?? row.runner_id ?? `runner:${process.pid ?? 'unknown'}`;
|
|
44
|
-
const updateResult = db
|
|
45
|
-
.prepare(`UPDATE workstreams
|
|
46
|
-
SET runner_id = ?,
|
|
47
|
-
lease_expires_at = datetime('now', '+${WORKSTREAM_LEASE_TTL_SECONDS} seconds')
|
|
48
|
-
WHERE id = ?
|
|
49
|
-
AND status = 'running'
|
|
50
|
-
AND claim_generation = ?`)
|
|
51
|
-
.run(owner, row.id, row.claim_generation);
|
|
52
|
-
return updateResult.changes === 1;
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
async function invokeWithLeaseHeartbeat(projectPath, leaseFence, invokeFn) {
|
|
56
|
-
if (!leaseFence?.parallelSessionId) {
|
|
57
|
-
return { superseded: false, result: await invokeFn() };
|
|
58
|
-
}
|
|
59
|
-
let superseded = false;
|
|
60
|
-
const interval = setInterval(() => {
|
|
61
|
-
if (superseded) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const refreshed = refreshParallelWorkstreamLease(projectPath, leaseFence);
|
|
65
|
-
if (!refreshed) {
|
|
66
|
-
superseded = true;
|
|
67
|
-
}
|
|
68
|
-
}, LEASE_HEARTBEAT_INTERVAL_MS);
|
|
69
|
-
interval.unref?.();
|
|
70
|
-
try {
|
|
71
|
-
const result = await invokeFn();
|
|
72
|
-
if (superseded) {
|
|
73
|
-
return { superseded: true };
|
|
74
|
-
}
|
|
75
|
-
return { superseded: false, result };
|
|
76
|
-
}
|
|
77
|
-
finally {
|
|
78
|
-
clearInterval(interval);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function summarizeErrorMessage(error) {
|
|
82
|
-
const raw = error instanceof Error ? error.message : String(error);
|
|
83
|
-
return raw.replace(/\s+/g, ' ').trim().slice(0, 220);
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Classify orchestrator invocation failures so we can distinguish transient parse
|
|
87
|
-
* noise from hard provider/config failures (auth/model/availability).
|
|
88
|
-
*/
|
|
89
|
-
async function classifyOrchestratorFailure(error, projectPath) {
|
|
90
|
-
const message = summarizeErrorMessage(error);
|
|
91
|
-
const lower = message.toLowerCase();
|
|
92
|
-
if (lower.includes('orchestrator ai provider not configured')) {
|
|
93
|
-
return { type: 'orchestrator_unconfigured', message, retryable: false };
|
|
94
|
-
}
|
|
95
|
-
if (lower.includes("provider '") && lower.includes('is not available')) {
|
|
96
|
-
return { type: 'provider_unavailable', message, retryable: false };
|
|
97
|
-
}
|
|
98
|
-
const config = (0, loader_js_1.loadConfig)(projectPath);
|
|
99
|
-
const providerName = config.ai?.orchestrator?.provider;
|
|
100
|
-
if (!providerName) {
|
|
101
|
-
return { type: 'orchestrator_unconfigured', message, retryable: false };
|
|
102
|
-
}
|
|
103
|
-
const registry = await (0, registry_js_1.getProviderRegistry)();
|
|
104
|
-
const provider = registry.tryGet(providerName);
|
|
105
|
-
if (!provider) {
|
|
106
|
-
return { type: 'provider_unavailable', message, retryable: false };
|
|
107
|
-
}
|
|
108
|
-
const syntheticFailure = {
|
|
109
|
-
success: false,
|
|
110
|
-
exitCode: 1,
|
|
111
|
-
stdout: '',
|
|
112
|
-
stderr: message,
|
|
113
|
-
duration: 0,
|
|
114
|
-
timedOut: false,
|
|
115
|
-
};
|
|
116
|
-
const classified = provider.classifyResult(syntheticFailure);
|
|
117
|
-
if (!classified) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
type: classified.type,
|
|
122
|
-
message: classified.message || message,
|
|
123
|
-
retryable: classified.retryable,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
const MAX_ORCHESTRATOR_PARSE_RETRIES = 3;
|
|
127
|
-
const MAX_CONTRACT_VIOLATION_RETRIES = 3;
|
|
128
|
-
const MAX_PROVIDER_NONZERO_FAILURES = 3;
|
|
129
|
-
const MAX_CONSECUTIVE_CODER_RETRIES = 3;
|
|
130
|
-
const CODER_PARSE_FALLBACK_MARKER = '[retry] FALLBACK: Orchestrator failed, defaulting to retry';
|
|
131
|
-
const REVIEWER_PARSE_FALLBACK_MARKER = '[unclear] FALLBACK: Orchestrator failed, retrying review';
|
|
132
|
-
const CONTRACT_CHECKLIST_MARKER = '[contract:checklist]';
|
|
133
|
-
const CONTRACT_REJECTION_RESPONSE_MARKER = '[contract:rejection_response]';
|
|
134
|
-
const MUST_IMPLEMENT_MARKER = '[must_implement]';
|
|
135
|
-
function formatProviderFailureMessage(taskId, context) {
|
|
136
|
-
const output = context.output || 'provider invocation failed with no output.';
|
|
137
|
-
return `Task ${taskId}: provider ${context.provider}/${context.model} exited with non-zero status ${context.exitCode} during ${context.role} phase: ${output}`;
|
|
138
|
-
}
|
|
139
|
-
async function handleProviderInvocationFailure(db, taskId, context, jsonMode) {
|
|
140
|
-
const failureCount = (0, queries_js_1.incrementTaskFailureCount)(db, taskId);
|
|
141
|
-
const providerMessage = formatProviderFailureMessage(taskId, context);
|
|
142
|
-
if (failureCount >= MAX_PROVIDER_NONZERO_FAILURES) {
|
|
143
|
-
const reason = `${providerMessage} (provider invocation failed ${failureCount} time(s). Task failed.)`;
|
|
144
|
-
(0, queries_js_1.updateTaskStatus)(db, taskId, 'failed', 'orchestrator', reason);
|
|
145
|
-
if (!jsonMode) {
|
|
146
|
-
console.log(`\n✗ Task failed (${reason})`);
|
|
147
|
-
}
|
|
148
|
-
return { shouldStopTask: true };
|
|
149
|
-
}
|
|
150
|
-
if (!jsonMode) {
|
|
151
|
-
const retriesLeft = MAX_PROVIDER_NONZERO_FAILURES - failureCount;
|
|
152
|
-
console.log(`\n⟳ Provider invocation failed (${failureCount}/${MAX_PROVIDER_NONZERO_FAILURES}) for task ${taskId}; retrying (${retriesLeft} attempt(s) left).`);
|
|
153
|
-
console.log(` ${providerMessage}`);
|
|
154
|
-
}
|
|
155
|
-
return { shouldStopTask: false };
|
|
156
|
-
}
|
|
157
|
-
function countConsecutiveOrchestratorFallbackEntries(db, taskId, marker) {
|
|
158
|
-
const audit = (0, queries_js_1.getTaskAudit)(db, taskId);
|
|
159
|
-
let count = 0;
|
|
160
|
-
for (let i = audit.length - 1; i >= 0; i--) {
|
|
161
|
-
const entry = audit[i];
|
|
162
|
-
if (entry.actor !== 'orchestrator')
|
|
163
|
-
break;
|
|
164
|
-
// Use category and error_code instead of marker string search
|
|
165
|
-
const isFallback = entry.category === 'fallback';
|
|
166
|
-
const matchesCode = entry.error_code === marker;
|
|
167
|
-
if (isFallback && matchesCode) {
|
|
168
|
-
count += 1;
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return count;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Count consecutive unclear orchestrator entries (regardless of specific marker).
|
|
178
|
-
* This catches ALL unclear decisions — orchestrator parse failures, missing decision tokens, etc.
|
|
179
|
-
*/
|
|
180
|
-
function countConsecutiveUnclearEntries(db, taskId) {
|
|
181
|
-
const audit = (0, queries_js_1.getTaskAudit)(db, taskId);
|
|
182
|
-
let count = 0;
|
|
183
|
-
for (let i = audit.length - 1; i >= 0; i--) {
|
|
184
|
-
const entry = audit[i];
|
|
185
|
-
if (entry.actor !== 'orchestrator')
|
|
186
|
-
break;
|
|
187
|
-
if ((entry.notes ?? '').startsWith('[unclear]')) {
|
|
188
|
-
count += 1;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
return count;
|
|
194
|
-
}
|
|
195
2
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
3
|
+
* Public API re-exports — implementation split across focused modules.
|
|
4
|
+
*
|
|
5
|
+
* loop-phases-helpers.ts — shared types, constants, and utility functions
|
|
6
|
+
* loop-phases-coder.ts — runCoderPhase implementation
|
|
7
|
+
* loop-phases-coder-decision.ts — coordinator invocation + decision execution helpers
|
|
8
|
+
* loop-phases-reviewer.ts — runReviewerPhase implementation
|
|
9
|
+
* loop-phases-reviewer-resolution.ts — reviewer decision resolution helper
|
|
198
10
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
break;
|
|
206
|
-
if ((entry.notes ?? '').startsWith('[retry]')) {
|
|
207
|
-
count += 1;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
return count;
|
|
213
|
-
}
|
|
214
|
-
function countConsecutiveTaggedOrchestratorEntries(db, taskId, requiredTag, tagFamilyPrefix) {
|
|
215
|
-
const audit = (0, queries_js_1.getTaskAudit)(db, taskId);
|
|
216
|
-
let count = 0;
|
|
217
|
-
for (let i = audit.length - 1; i >= 0; i--) {
|
|
218
|
-
const entry = audit[i];
|
|
219
|
-
if (entry.actor !== 'orchestrator') {
|
|
220
|
-
continue; // tolerate non-orchestrator audit noise
|
|
221
|
-
}
|
|
222
|
-
const notes = entry.notes ?? '';
|
|
223
|
-
if (notes.includes(requiredTag)) {
|
|
224
|
-
count += 1;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (tagFamilyPrefix && notes.includes(tagFamilyPrefix)) {
|
|
228
|
-
break; // same family, different category = sequence ended
|
|
229
|
-
}
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
return count;
|
|
233
|
-
}
|
|
234
|
-
function countLatestOpenRejectionItems(notes) {
|
|
235
|
-
if (!notes)
|
|
236
|
-
return 0;
|
|
237
|
-
return notes
|
|
238
|
-
.split('\n')
|
|
239
|
-
.filter(line => /^\s*[-*]\s*\[\s\]\s+/.test(line))
|
|
240
|
-
.length;
|
|
241
|
-
}
|
|
242
|
-
function hasCoderCompletionSignal(output) {
|
|
243
|
-
const lower = output.toLowerCase();
|
|
244
|
-
if (/\b(?:not|no)\s+(?:task\s+)?(?:is\s+)?(?:complete|complete[d]?|finished|done|ready)\b/.test(lower)) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
return /\b(?:task\s+)?(?:is\s+)?(?:complete|complete[d]?|implemented|finished|done|ready for review|ready)\b/.test(lower);
|
|
248
|
-
}
|
|
249
|
-
function extractSubmissionCommitToken(output) {
|
|
250
|
-
const match = output.match(/^\s*SUBMISSION_COMMIT\s*:\s*([0-9a-fA-F]{7,40})\b/im);
|
|
251
|
-
return match?.[1] ?? null;
|
|
252
|
-
}
|
|
253
|
-
function resolveCoderSubmittedCommitSha(projectPath, coderOutput, options = {}) {
|
|
254
|
-
const tokenSha = extractSubmissionCommitToken(coderOutput);
|
|
255
|
-
if (tokenSha && (0, status_js_1.isCommitReachableWithFetch)(projectPath, tokenSha, { forceFetch: true })) {
|
|
256
|
-
try {
|
|
257
|
-
return (0, node_child_process_1.execSync)(`git rev-parse ${tokenSha}^{commit}`, {
|
|
258
|
-
cwd: projectPath,
|
|
259
|
-
encoding: 'utf-8',
|
|
260
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
261
|
-
}).trim();
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return tokenSha;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (options.requireExplicitToken) {
|
|
268
|
-
return undefined;
|
|
269
|
-
}
|
|
270
|
-
return (0, status_js_1.getCurrentCommitSha)(projectPath) || undefined;
|
|
271
|
-
}
|
|
272
|
-
async function runCoderPhase(db, task, projectPath, action, jsonMode = false, coordinatorCache, coordinatorThresholds, leaseFence, branchName = 'main', poolSlotContext) {
|
|
273
|
-
if (!task)
|
|
274
|
-
return;
|
|
275
|
-
if (!refreshParallelWorkstreamLease(projectPath, leaseFence)) {
|
|
276
|
-
if (!jsonMode) {
|
|
277
|
-
console.log('\n↺ Lease ownership lost before coder phase; skipping task in this runner.');
|
|
278
|
-
}
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
// ── Pool slot: prepare workspace for task ──
|
|
282
|
-
let poolStartingSha;
|
|
283
|
-
let effectiveProjectPath = projectPath;
|
|
284
|
-
if (poolSlotContext) {
|
|
285
|
-
const prepResult = (0, git_lifecycle_js_1.prepareForTask)(poolSlotContext.globalDb, poolSlotContext.slot, task.id, projectPath);
|
|
286
|
-
if (!prepResult.ok) {
|
|
287
|
-
if (!jsonMode) {
|
|
288
|
-
console.log(`\n✗ Workspace preparation failed: ${prepResult.reason}`);
|
|
289
|
-
}
|
|
290
|
-
if (prepResult.blocked) {
|
|
291
|
-
const { setTaskBlocked } = await import('../database/queries.js');
|
|
292
|
-
setTaskBlocked(db, task.id, 'blocked_error', prepResult.reason);
|
|
293
|
-
}
|
|
294
|
-
(0, pool_js_1.releaseSlot)(poolSlotContext.globalDb, poolSlotContext.slot.id);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
poolStartingSha = prepResult.startingSha;
|
|
298
|
-
effectiveProjectPath = poolSlotContext.slot.slot_path;
|
|
299
|
-
if (!jsonMode) {
|
|
300
|
-
console.log(`\n✓ Workspace prepared (branch: ${prepResult.taskBranch}, base: ${prepResult.baseBranch})`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
let coordinatorGuidance;
|
|
304
|
-
const thresholds = coordinatorThresholds || [2, 5, 9];
|
|
305
|
-
const persistedMustImplement = (0, queries_js_1.getLatestMustImplementGuidance)(db, task.id);
|
|
306
|
-
const activeMustImplement = persistedMustImplement &&
|
|
307
|
-
task.status === 'in_progress' &&
|
|
308
|
-
task.rejection_count >= persistedMustImplement.rejection_count_watermark
|
|
309
|
-
? persistedMustImplement
|
|
310
|
-
: null;
|
|
311
|
-
// Run coordinator at rejection thresholds (same as before)
|
|
312
|
-
const shouldInvokeCoordinator = thresholds.includes(task.rejection_count);
|
|
313
|
-
const cachedResult = coordinatorCache?.get(task.id);
|
|
314
|
-
if (activeMustImplement) {
|
|
315
|
-
coordinatorGuidance = activeMustImplement.guidance;
|
|
316
|
-
coordinatorCache?.set(task.id, {
|
|
317
|
-
success: true,
|
|
318
|
-
decision: 'guide_coder',
|
|
319
|
-
guidance: activeMustImplement.guidance,
|
|
320
|
-
});
|
|
321
|
-
if (!jsonMode) {
|
|
322
|
-
console.log(`\nActive MUST_IMPLEMENT override detected (rc=${activeMustImplement.rejection_count_watermark})`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (shouldInvokeCoordinator) {
|
|
326
|
-
if (activeMustImplement &&
|
|
327
|
-
task.rejection_count === activeMustImplement.rejection_count_watermark) {
|
|
328
|
-
if (!jsonMode) {
|
|
329
|
-
console.log('\nSkipping coordinator reinvocation in same rejection cycle due to active MUST_IMPLEMENT override');
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
if (!jsonMode) {
|
|
334
|
-
console.log(`\n>>> Task has ${task.rejection_count} rejections (threshold hit) - invoking COORDINATOR...\n`);
|
|
335
|
-
}
|
|
336
|
-
try {
|
|
337
|
-
const rejectionHistory = (0, queries_js_1.getTaskRejections)(db, task.id);
|
|
338
|
-
const coordExtra = {};
|
|
339
|
-
if (task.section_id) {
|
|
340
|
-
const allSectionTasks = (0, queries_js_1.listTasks)(db, { sectionId: task.section_id });
|
|
341
|
-
coordExtra.sectionTasks = allSectionTasks.map(t => ({
|
|
342
|
-
id: t.id, title: t.title, status: t.status,
|
|
343
|
-
}));
|
|
344
|
-
}
|
|
345
|
-
coordExtra.submissionNotes = (0, queries_js_1.getLatestSubmissionNotes)(db, task.id);
|
|
346
|
-
const modified = (0, status_js_1.getModifiedFiles)(effectiveProjectPath);
|
|
347
|
-
if (modified.length > 0) {
|
|
348
|
-
coordExtra.gitDiffSummary = modified.join('\n');
|
|
349
|
-
}
|
|
350
|
-
if (cachedResult) {
|
|
351
|
-
coordExtra.previousGuidance = cachedResult.guidance;
|
|
352
|
-
}
|
|
353
|
-
if (activeMustImplement) {
|
|
354
|
-
coordExtra.lockedMustImplementGuidance = activeMustImplement.guidance;
|
|
355
|
-
coordExtra.lockedMustImplementWatermark = activeMustImplement.rejection_count_watermark;
|
|
356
|
-
}
|
|
357
|
-
const coordResult = await (0, coordinator_js_1.invokeCoordinator)(task, rejectionHistory, projectPath, coordExtra);
|
|
358
|
-
if (coordResult) {
|
|
359
|
-
const mustKeepOverride = activeMustImplement &&
|
|
360
|
-
task.rejection_count > activeMustImplement.rejection_count_watermark;
|
|
361
|
-
const normalizedGuidance = mustKeepOverride && !coordResult.guidance.includes('MUST_IMPLEMENT:')
|
|
362
|
-
? `${activeMustImplement.guidance}\n\nAdditional coordinator guidance:\n${coordResult.guidance}`
|
|
363
|
-
: coordResult.guidance;
|
|
364
|
-
coordinatorGuidance = normalizedGuidance;
|
|
365
|
-
coordinatorCache?.set(task.id, {
|
|
366
|
-
...coordResult,
|
|
367
|
-
guidance: normalizedGuidance,
|
|
368
|
-
});
|
|
369
|
-
(0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'coordinator', {
|
|
370
|
-
actorType: 'orchestrator',
|
|
371
|
-
notes: `[${coordResult.decision}] ${normalizedGuidance}`,
|
|
372
|
-
});
|
|
373
|
-
if (!jsonMode) {
|
|
374
|
-
console.log(`\nCoordinator decision: ${coordResult.decision}`);
|
|
375
|
-
console.log('Coordinator guidance stored for both coder and reviewer.');
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
if (!jsonMode) {
|
|
381
|
-
console.warn('Coordinator invocation failed, continuing without guidance:', error);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
else if (cachedResult && !activeMustImplement) {
|
|
387
|
-
coordinatorGuidance = cachedResult.guidance;
|
|
388
|
-
if (!jsonMode && task.rejection_count >= 2) {
|
|
389
|
-
console.log(`\nReusing cached coordinator guidance (decision: ${cachedResult.decision})`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
// STEP 1: Invoke coder (no status commands in prompt anymore)
|
|
393
|
-
if (!jsonMode) {
|
|
394
|
-
console.log('\n>>> Invoking CODER...\n');
|
|
395
|
-
}
|
|
396
|
-
const initialSha = poolStartingSha || (0, status_js_1.getCurrentCommitSha)(effectiveProjectPath) || '';
|
|
397
|
-
const coderConfig = (0, loader_js_1.loadConfig)(projectPath).ai?.coder;
|
|
398
|
-
const coderInvocation = await invokeWithLeaseHeartbeat(projectPath, leaseFence, () => (0, coder_js_1.invokeCoder)(task, effectiveProjectPath, action, coordinatorGuidance, leaseFence?.runnerId));
|
|
399
|
-
if (coderInvocation.superseded || !coderInvocation.result) {
|
|
400
|
-
if (!jsonMode) {
|
|
401
|
-
console.log('\n↺ Lease ownership changed during coder invocation; skipping post-processing in this runner.');
|
|
402
|
-
}
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
const coderResult = coderInvocation.result;
|
|
406
|
-
if (coderResult.timedOut || !coderResult.success) {
|
|
407
|
-
const providerName = coderConfig?.provider ?? (0, loader_js_1.loadConfig)(projectPath).ai?.coder?.provider ?? 'unknown';
|
|
408
|
-
const modelName = coderConfig?.model ?? (0, loader_js_1.loadConfig)(projectPath).ai?.coder?.model ?? 'unknown';
|
|
409
|
-
// Check for credit/rate_limit exhaustion before counting as a provider failure
|
|
410
|
-
const registry = await (0, registry_js_1.getProviderRegistry)();
|
|
411
|
-
const prov = registry.tryGet(providerName);
|
|
412
|
-
if (prov) {
|
|
413
|
-
const classified = prov.classifyResult(coderResult);
|
|
414
|
-
if (classified?.type === 'credit_exhaustion') {
|
|
415
|
-
return { action: 'pause_credit_exhaustion', provider: providerName, model: modelName, role: 'coder', message: classified.message };
|
|
416
|
-
}
|
|
417
|
-
if (classified?.type === 'rate_limit') {
|
|
418
|
-
return { action: 'rate_limit', provider: providerName, model: modelName, role: 'coder', message: classified.message, retryAfterMs: classified.retryAfterMs };
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
const output = (coderResult.stderr || coderResult.stdout || '').trim();
|
|
422
|
-
const failed = await handleProviderInvocationFailure(db, task.id, {
|
|
423
|
-
role: 'coder',
|
|
424
|
-
provider: providerName,
|
|
425
|
-
model: modelName,
|
|
426
|
-
exitCode: coderResult.exitCode ?? 1,
|
|
427
|
-
output,
|
|
428
|
-
}, jsonMode);
|
|
429
|
-
if (failed.shouldStopTask) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
(0, queries_js_1.clearTaskFailureCount)(db, task.id);
|
|
435
|
-
// ── Pool slot: post-coder verification gate ──
|
|
436
|
-
if (poolSlotContext && poolStartingSha) {
|
|
437
|
-
const gateResult = (0, git_lifecycle_js_1.postCoderGate)(effectiveProjectPath, poolStartingSha, task.title);
|
|
438
|
-
if (!gateResult.ok) {
|
|
439
|
-
if (!jsonMode) {
|
|
440
|
-
console.log(`\n⟳ Post-coder gate: ${gateResult.reason}. Returning to coder.`);
|
|
441
|
-
}
|
|
442
|
-
(0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'orchestrator', {
|
|
443
|
-
actorType: 'orchestrator',
|
|
444
|
-
notes: `[retry] Post-coder gate: ${gateResult.reason}`,
|
|
445
|
-
});
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
if (gateResult.autoCommitted && !jsonMode) {
|
|
449
|
-
console.log('\n✓ Post-coder gate: auto-committed uncommitted work');
|
|
450
|
-
}
|
|
451
|
-
(0, pool_js_1.updateSlotStatus)(poolSlotContext.globalDb, poolSlotContext.slot.id, 'awaiting_review');
|
|
452
|
-
}
|
|
453
|
-
// STEP 2: Gather git state
|
|
454
|
-
const commits = (0, status_js_1.getRecentCommits)(effectiveProjectPath, 5, initialSha);
|
|
455
|
-
const files_changed = (0, status_js_1.getChangedFiles)(effectiveProjectPath);
|
|
456
|
-
const has_uncommitted = (0, status_js_1.hasUncommittedChanges)(effectiveProjectPath);
|
|
457
|
-
const diff_summary = (0, status_js_1.getDiffSummary)(effectiveProjectPath);
|
|
458
|
-
const hasRelevantChanges = has_uncommitted || commits.length > 0 || files_changed.length > 0;
|
|
459
|
-
const gitState = {
|
|
460
|
-
commits,
|
|
461
|
-
files_changed,
|
|
462
|
-
has_uncommitted_changes: has_uncommitted,
|
|
463
|
-
diff_summary,
|
|
464
|
-
};
|
|
465
|
-
// STEP 3: Build orchestrator context
|
|
466
|
-
// Get rejection notes if any
|
|
467
|
-
const lastRejectionNotes = task.rejection_count > 0
|
|
468
|
-
? (0, queries_js_1.getTaskRejections)(db, task.id).slice(-1)[0]?.notes ?? undefined
|
|
469
|
-
: undefined;
|
|
470
|
-
const requiresExplicitSubmissionCommit = (lastRejectionNotes ?? '').includes('[commit_recovery]');
|
|
471
|
-
const rejectionItemCount = countLatestOpenRejectionItems(lastRejectionNotes);
|
|
472
|
-
const context = {
|
|
473
|
-
task: {
|
|
474
|
-
id: task.id,
|
|
475
|
-
title: task.title,
|
|
476
|
-
description: task.title, // Use title as description for now
|
|
477
|
-
rejection_notes: lastRejectionNotes,
|
|
478
|
-
rejection_count: task.rejection_count,
|
|
479
|
-
rejection_item_count: rejectionItemCount,
|
|
480
|
-
},
|
|
481
|
-
coder_output: {
|
|
482
|
-
stdout: coderResult.stdout,
|
|
483
|
-
stderr: coderResult.stderr,
|
|
484
|
-
exit_code: coderResult.exitCode,
|
|
485
|
-
timed_out: coderResult.timedOut,
|
|
486
|
-
duration_ms: coderResult.duration,
|
|
487
|
-
},
|
|
488
|
-
git_state: gitState,
|
|
489
|
-
};
|
|
490
|
-
// STEP 4: Invoke orchestrator
|
|
491
|
-
let orchestratorOutput;
|
|
492
|
-
try {
|
|
493
|
-
orchestratorOutput = await (0, invoke_js_1.invokeCoderOrchestrator)(context, projectPath);
|
|
494
|
-
}
|
|
495
|
-
catch (error) {
|
|
496
|
-
console.error('Orchestrator invocation failed:', error);
|
|
497
|
-
const orchestratorFailure = await classifyOrchestratorFailure(error, projectPath);
|
|
498
|
-
if (orchestratorFailure && !orchestratorFailure.retryable) {
|
|
499
|
-
orchestratorOutput = `STATUS: ERROR\nREASON: FALLBACK: Non-retryable orchestrator failure (${orchestratorFailure.type})\nCONFIDENCE: LOW`;
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
// Check if coder seems finished even if orchestrator failed
|
|
503
|
-
const isTaskComplete = hasCoderCompletionSignal(coderResult.stdout);
|
|
504
|
-
// Only count as having work if there are actual relevant uncommitted changes,
|
|
505
|
-
// changed files, or recent commits
|
|
506
|
-
const hasWork = hasRelevantChanges;
|
|
507
|
-
if (isTaskComplete && hasWork) {
|
|
508
|
-
orchestratorOutput = `STATUS: REVIEW\nREASON: FALLBACK: Orchestrator failed but coder signaled completion\nCONFIDENCE: LOW`;
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
// Fallback to safe default: retry
|
|
512
|
-
orchestratorOutput = `STATUS: RETRY\nREASON: FALLBACK: Orchestrator failed, defaulting to retry\nCONFIDENCE: LOW`;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
// STEP 5: Parse orchestrator output with fallback
|
|
517
|
-
const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
|
|
518
|
-
let decision = handler.parseCoderOutput(orchestratorOutput);
|
|
519
|
-
// Fill git-state fields from actual git state (parser returns placeholders)
|
|
520
|
-
decision.commits = commits.map(c => c.sha);
|
|
521
|
-
decision.files_changed = files_changed.length;
|
|
522
|
-
decision.has_commits = commits.length > 0;
|
|
523
|
-
// Derive stage_commit_submit from submit + uncommitted + completion signal
|
|
524
|
-
if (decision.action === 'submit' && has_uncommitted && commits.length === 0) {
|
|
525
|
-
const isTaskComplete = hasCoderCompletionSignal(coderResult.stdout);
|
|
526
|
-
if (isTaskComplete) {
|
|
527
|
-
decision.action = 'stage_commit_submit';
|
|
528
|
-
}
|
|
529
|
-
// If not complete, leave as 'submit' — the existing submit handler
|
|
530
|
-
// will fail safely when it can't find a valid commit hash
|
|
531
|
-
}
|
|
532
|
-
// When orchestrator parse falls back to retry, check coder output + git state
|
|
533
|
-
// for completion signals before giving up (same logic as the catch block above)
|
|
534
|
-
if (decision.action === 'retry' && decision.reasoning.includes('FALLBACK: Orchestrator failed')) {
|
|
535
|
-
const isTaskComplete = hasCoderCompletionSignal(coderResult.stdout);
|
|
536
|
-
const hasWork = hasRelevantChanges;
|
|
537
|
-
if (isTaskComplete && hasWork) {
|
|
538
|
-
if (!jsonMode) {
|
|
539
|
-
console.log('[Orchestrator] Parse failed but coder signaled completion with work present - submitting');
|
|
540
|
-
}
|
|
541
|
-
decision = {
|
|
542
|
-
action: has_uncommitted ? 'stage_commit_submit' : 'submit',
|
|
543
|
-
reasoning: 'FALLBACK: Orchestrator failed but coder signaled completion with commits/changes',
|
|
544
|
-
commits: commits.map(c => c.sha),
|
|
545
|
-
commit_message: has_uncommitted ? 'feat: implement task specification' : undefined,
|
|
546
|
-
next_status: 'review',
|
|
547
|
-
files_changed: files_changed.length,
|
|
548
|
-
confidence: 'low',
|
|
549
|
-
exit_clean: true,
|
|
550
|
-
has_commits: commits.length > 0,
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
// No completion signal - apply the parse retry counter
|
|
555
|
-
const consecutiveParseFallbackRetries = countConsecutiveOrchestratorFallbackEntries(db, task.id, CODER_PARSE_FALLBACK_MARKER) + 1;
|
|
556
|
-
if (consecutiveParseFallbackRetries >= MAX_ORCHESTRATOR_PARSE_RETRIES) {
|
|
557
|
-
decision = {
|
|
558
|
-
...decision,
|
|
559
|
-
action: 'error',
|
|
560
|
-
reasoning: `Orchestrator parse failed ${consecutiveParseFallbackRetries} times; escalating to failed to stop retry loop`,
|
|
561
|
-
next_status: 'failed',
|
|
562
|
-
confidence: 'low',
|
|
563
|
-
exit_clean: false,
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
decision = {
|
|
568
|
-
...decision,
|
|
569
|
-
reasoning: `${decision.reasoning} (parse_retry ${consecutiveParseFallbackRetries}/${MAX_ORCHESTRATOR_PARSE_RETRIES})`,
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
// Contract-violation handling (detected via REASON prefix from text signals).
|
|
575
|
-
const legacyChecklistViolation = /^CHECKLIST_REQUIRED:/i.test(decision.reasoning || '');
|
|
576
|
-
const legacyRejectionResponseViolation = /^REJECTION_RESPONSE_REQUIRED:/i.test(decision.reasoning || '');
|
|
577
|
-
const contractViolation = legacyChecklistViolation ? 'checklist_required'
|
|
578
|
-
: legacyRejectionResponseViolation ? 'rejection_response_required'
|
|
579
|
-
: null;
|
|
580
|
-
if (contractViolation) {
|
|
581
|
-
const marker = contractViolation === 'checklist_required'
|
|
582
|
-
? CONTRACT_CHECKLIST_MARKER
|
|
583
|
-
: CONTRACT_REJECTION_RESPONSE_MARKER;
|
|
584
|
-
const reasonText = (decision.reasoning || '')
|
|
585
|
-
.replace(/^CHECKLIST_REQUIRED:\s*/i, '')
|
|
586
|
-
.replace(/^REJECTION_RESPONSE_REQUIRED:\s*/i, '')
|
|
587
|
-
.trim();
|
|
588
|
-
const cleanReason = reasonText || 'Required output contract not satisfied';
|
|
589
|
-
const consecutiveContractViolations = countConsecutiveTaggedOrchestratorEntries(db, task.id, marker, '[contract:') + 1;
|
|
590
|
-
if (consecutiveContractViolations >= MAX_CONTRACT_VIOLATION_RETRIES) {
|
|
591
|
-
decision = {
|
|
592
|
-
...decision,
|
|
593
|
-
action: 'error',
|
|
594
|
-
next_status: 'failed',
|
|
595
|
-
reasoning: `${marker} ${cleanReason} (retry_limit ${consecutiveContractViolations}/${MAX_CONTRACT_VIOLATION_RETRIES})`,
|
|
596
|
-
confidence: 'low',
|
|
597
|
-
exit_clean: false,
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
decision = {
|
|
602
|
-
...decision,
|
|
603
|
-
action: 'retry',
|
|
604
|
-
next_status: 'in_progress',
|
|
605
|
-
reasoning: `${marker} ${cleanReason} (retry ${consecutiveContractViolations}/${MAX_CONTRACT_VIOLATION_RETRIES})`,
|
|
606
|
-
confidence: 'medium',
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
// Enforce orchestrator authority over weak/unsupported WONT_FIX claims.
|
|
611
|
-
// WONT_FIX overrides are now encoded in the REASON prefix as semicolon-separated items.
|
|
612
|
-
const legacyWontFixOverrideMatch = (decision.reasoning || '').match(/WONT_FIX_OVERRIDE:\s*([\s\S]+)/i);
|
|
613
|
-
const overrideItems = legacyWontFixOverrideMatch?.[1]
|
|
614
|
-
? legacyWontFixOverrideMatch[1]
|
|
615
|
-
.split(';')
|
|
616
|
-
.map(line => line.trim())
|
|
617
|
-
.filter(Boolean)
|
|
618
|
-
: [];
|
|
619
|
-
if (overrideItems.length > 0) {
|
|
620
|
-
const mandatoryLines = overrideItems.map((item, idx) => `${idx + 1}. ${item}`);
|
|
621
|
-
const mandatoryGuidance = `MUST_IMPLEMENT:
|
|
622
|
-
${mandatoryLines.join('\n')}
|
|
623
|
-
|
|
624
|
-
This is a mandatory orchestrator override. Implement these changes before resubmitting.
|
|
625
|
-
Only use WONT_FIX if you provide exceptional technical evidence and the orchestrator explicitly accepts it.`;
|
|
626
|
-
const persistedNote = mandatoryGuidance;
|
|
627
|
-
(0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'coordinator', {
|
|
628
|
-
actorType: 'orchestrator',
|
|
629
|
-
notes: persistedNote,
|
|
630
|
-
category: 'must_implement',
|
|
631
|
-
metadata: { rejection_count: task.rejection_count }
|
|
632
|
-
});
|
|
633
|
-
coordinatorCache?.set(task.id, {
|
|
634
|
-
success: true,
|
|
635
|
-
decision: 'guide_coder',
|
|
636
|
-
guidance: mandatoryGuidance,
|
|
637
|
-
});
|
|
638
|
-
decision = {
|
|
639
|
-
...decision,
|
|
640
|
-
action: 'retry',
|
|
641
|
-
next_status: 'in_progress',
|
|
642
|
-
reasoning: `WONT_FIX override applied (MUST_IMPLEMENT)`,
|
|
643
|
-
confidence: 'medium',
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
// Universal retry cap: prevent infinite loops for ANY consecutive [retry] entries.
|
|
647
|
-
// This catches the bug where SignalParser returns 'unclear' → 'retry' without escalation.
|
|
648
|
-
if (decision.action === 'retry') {
|
|
649
|
-
const consecutiveRetries = countConsecutiveRetryEntries(db, task.id) + 1;
|
|
650
|
-
if (consecutiveRetries >= MAX_CONSECUTIVE_CODER_RETRIES) {
|
|
651
|
-
decision = {
|
|
652
|
-
...decision,
|
|
653
|
-
action: 'error',
|
|
654
|
-
reasoning: `Coder retry limit reached (${consecutiveRetries} consecutive retries); escalating to failed`,
|
|
655
|
-
next_status: 'failed',
|
|
656
|
-
confidence: 'low',
|
|
657
|
-
exit_clean: false,
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
// STEP 6: Log orchestrator decision for audit trail.
|
|
662
|
-
// Use a non-transition audit row here; actual status transitions are recorded
|
|
663
|
-
// only after execution succeeds (e.g. updateTaskStatus/approveTask/rejectTask).
|
|
664
|
-
(0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'orchestrator', {
|
|
665
|
-
actorType: 'orchestrator',
|
|
666
|
-
notes: `[${decision.action}] ${decision.reasoning} (confidence: ${decision.confidence})`,
|
|
667
|
-
category: 'decision',
|
|
668
|
-
});
|
|
669
|
-
if (!refreshParallelWorkstreamLease(projectPath, leaseFence)) {
|
|
670
|
-
if (!jsonMode) {
|
|
671
|
-
console.log('\n↺ Lease ownership changed before applying coder decision; skipping in this runner.');
|
|
672
|
-
}
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
// STEP 7: Execute the decision
|
|
676
|
-
switch (decision.action) {
|
|
677
|
-
case 'submit':
|
|
678
|
-
{
|
|
679
|
-
const submissionCommitSha = resolveCoderSubmittedCommitSha(effectiveProjectPath, coderResult.stdout, {
|
|
680
|
-
requireExplicitToken: requiresExplicitSubmissionCommit,
|
|
681
|
-
});
|
|
682
|
-
if (!submissionCommitSha && requiresExplicitSubmissionCommit) {
|
|
683
|
-
(0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'orchestrator', {
|
|
684
|
-
actorType: 'orchestrator',
|
|
685
|
-
notes: '[retry] Awaiting explicit SUBMISSION_COMMIT token for commit recovery',
|
|
686
|
-
});
|
|
687
|
-
if (!jsonMode) {
|
|
688
|
-
console.log('\n⟳ Waiting for explicit SUBMISSION_COMMIT token from coder');
|
|
689
|
-
}
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
if (!submissionCommitSha || !(0, status_js_1.isCommitReachable)(effectiveProjectPath, submissionCommitSha)) {
|
|
693
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: cannot submit to review without a valid commit hash');
|
|
694
|
-
if (!jsonMode) {
|
|
695
|
-
console.log('\n✗ Task failed (submission commit missing or not in workspace)');
|
|
696
|
-
}
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
if (leaseFence?.parallelSessionId) {
|
|
700
|
-
const pushResult = (0, push_js_1.pushToRemote)(projectPath, 'origin', branchName);
|
|
701
|
-
if (!pushResult.success) {
|
|
702
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', `Task failed: cannot publish submission commit ${submissionCommitSha} to ${branchName} before review`);
|
|
703
|
-
if (!jsonMode) {
|
|
704
|
-
console.log('\n✗ Task failed (unable to push submission commit to branch for review)');
|
|
705
|
-
}
|
|
706
|
-
break;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', decision.reasoning, submissionCommitSha);
|
|
710
|
-
}
|
|
711
|
-
if (!jsonMode) {
|
|
712
|
-
console.log(`\n✓ Coder complete, submitted to review (confidence: ${decision.confidence})`);
|
|
713
|
-
}
|
|
714
|
-
break;
|
|
715
|
-
case 'stage_commit_submit':
|
|
716
|
-
if (!has_uncommitted) {
|
|
717
|
-
const submissionCommitSha = resolveCoderSubmittedCommitSha(effectiveProjectPath, coderResult.stdout, {
|
|
718
|
-
requireExplicitToken: requiresExplicitSubmissionCommit,
|
|
719
|
-
});
|
|
720
|
-
if (!submissionCommitSha && requiresExplicitSubmissionCommit) {
|
|
721
|
-
(0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'orchestrator', {
|
|
722
|
-
actorType: 'orchestrator',
|
|
723
|
-
notes: '[retry] Awaiting explicit SUBMISSION_COMMIT token for commit recovery',
|
|
724
|
-
});
|
|
725
|
-
if (!jsonMode) {
|
|
726
|
-
console.log('\n⟳ Waiting for explicit SUBMISSION_COMMIT token from coder');
|
|
727
|
-
}
|
|
728
|
-
break;
|
|
729
|
-
}
|
|
730
|
-
if (!submissionCommitSha || !(0, status_js_1.isCommitReachable)(effectiveProjectPath, submissionCommitSha)) {
|
|
731
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: cannot submit to review without a valid commit hash');
|
|
732
|
-
if (!jsonMode) {
|
|
733
|
-
console.log('\n✗ Task failed (submission commit missing or not in workspace)');
|
|
734
|
-
}
|
|
735
|
-
break;
|
|
736
|
-
}
|
|
737
|
-
if (leaseFence?.parallelSessionId) {
|
|
738
|
-
const pushResult = (0, push_js_1.pushToRemote)(projectPath, 'origin', branchName);
|
|
739
|
-
if (!pushResult.success) {
|
|
740
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', `Task failed: cannot publish submission commit ${submissionCommitSha} to ${branchName} before review`);
|
|
741
|
-
if (!jsonMode) {
|
|
742
|
-
console.log('\n✗ Task failed (unable to push submission commit to branch for review)');
|
|
743
|
-
}
|
|
744
|
-
break;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', 'Auto-commit skipped: no uncommitted changes', submissionCommitSha);
|
|
748
|
-
if (!jsonMode) {
|
|
749
|
-
console.log('\n✓ Auto-commit skipped (no uncommitted files) and submitted to review');
|
|
750
|
-
}
|
|
751
|
-
break;
|
|
752
|
-
}
|
|
753
|
-
if (!refreshParallelWorkstreamLease(projectPath, leaseFence)) {
|
|
754
|
-
if (!jsonMode) {
|
|
755
|
-
console.log('\n↺ Lease ownership lost before auto-commit; skipping task in this runner.');
|
|
756
|
-
}
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
// Stage all changes
|
|
760
|
-
try {
|
|
761
|
-
(0, node_child_process_1.execSync)('git add -A', { cwd: effectiveProjectPath, stdio: 'pipe' });
|
|
762
|
-
const message = decision.commit_message || 'feat: implement task specification';
|
|
763
|
-
(0, node_child_process_1.execSync)(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
|
|
764
|
-
cwd: effectiveProjectPath,
|
|
765
|
-
stdio: 'pipe'
|
|
766
|
-
});
|
|
767
|
-
const submissionCommitSha = (0, status_js_1.getCurrentCommitSha)(effectiveProjectPath) || undefined;
|
|
768
|
-
if (!submissionCommitSha || !(0, status_js_1.isCommitReachable)(effectiveProjectPath, submissionCommitSha)) {
|
|
769
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: auto-committed but commit hash is not in current workspace');
|
|
770
|
-
if (!jsonMode) {
|
|
771
|
-
console.log('\n✗ Task failed (auto-commit hash not in workspace)');
|
|
772
|
-
}
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
if (leaseFence?.parallelSessionId) {
|
|
776
|
-
const pushResult = (0, push_js_1.pushToRemote)(projectPath, 'origin', branchName);
|
|
777
|
-
if (!pushResult.success) {
|
|
778
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', `Task failed: cannot publish submission commit ${submissionCommitSha} to ${branchName} before review`);
|
|
779
|
-
if (!jsonMode) {
|
|
780
|
-
console.log('\n✗ Task failed (unable to push submission commit to branch for review)');
|
|
781
|
-
}
|
|
782
|
-
break;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', `Auto-committed and submitted (${decision.reasoning})`, submissionCommitSha);
|
|
786
|
-
if (!jsonMode) {
|
|
787
|
-
console.log(`\n✓ Auto-committed and submitted to review (confidence: ${decision.confidence})`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
catch (error) {
|
|
791
|
-
const failureReason = summarizeErrorMessage(error);
|
|
792
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', `Task failed: auto-commit step failed before review (${failureReason})`);
|
|
793
|
-
if (!jsonMode) {
|
|
794
|
-
console.log('\n✗ Task failed (auto-commit step failed before review)');
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
break;
|
|
798
|
-
case 'retry':
|
|
799
|
-
if (!jsonMode) {
|
|
800
|
-
console.log(`\n⟳ Retrying coder (${decision.reasoning}, confidence: ${decision.confidence})`);
|
|
801
|
-
}
|
|
802
|
-
break;
|
|
803
|
-
case 'error':
|
|
804
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', `Task failed: ${decision.reasoning}`);
|
|
805
|
-
if (!jsonMode) {
|
|
806
|
-
console.log(`\n✗ Task failed (${decision.reasoning})`);
|
|
807
|
-
console.log('Human intervention required.');
|
|
808
|
-
}
|
|
809
|
-
break;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordinatorResult, branchName = 'main', leaseFence, poolSlotContext) {
|
|
813
|
-
if (!task)
|
|
814
|
-
return;
|
|
815
|
-
if (!refreshParallelWorkstreamLease(projectPath, leaseFence)) {
|
|
816
|
-
if (!jsonMode) {
|
|
817
|
-
console.log('\n↺ Lease ownership lost before reviewer phase; skipping task in this runner.');
|
|
818
|
-
}
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
// ── Pool slot: set effective path for reviewer ──
|
|
822
|
-
const effectiveProjectPath = poolSlotContext ? poolSlotContext.slot.slot_path : projectPath;
|
|
823
|
-
if (poolSlotContext) {
|
|
824
|
-
(0, pool_js_1.updateSlotStatus)(poolSlotContext.globalDb, poolSlotContext.slot.id, 'review_active');
|
|
825
|
-
}
|
|
826
|
-
const submissionResolution = (0, submission_resolution_js_1.resolveSubmissionCommitWithRecovery)(effectiveProjectPath, (0, queries_js_1.getSubmissionCommitShas)(db, task.id));
|
|
827
|
-
if (submissionResolution.status !== 'resolved') {
|
|
828
|
-
const attemptsText = submissionResolution.attempts.join(' | ') || 'none';
|
|
829
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'in_progress', 'orchestrator', `[commit_recovery] Missing reachable submission hash (${submissionResolution.reason}; attempts: ${attemptsText}). ` +
|
|
830
|
-
`Treating task as resubmission. Coder must output exact line: SUBMISSION_COMMIT: <sha> for the commit that implements the task.`);
|
|
831
|
-
if (!jsonMode) {
|
|
832
|
-
console.log('\n⟳ Reviewer hash missing; returning task to coder for hash resubmission');
|
|
833
|
-
}
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
const submissionCommitSha = submissionResolution.sha;
|
|
837
|
-
const phaseConfig = (0, loader_js_1.loadConfig)(projectPath);
|
|
838
|
-
const multiReviewEnabled = (0, reviewer_js_1.isMultiReviewEnabled)(phaseConfig);
|
|
839
|
-
let effectiveMultiReviewEnabled = multiReviewEnabled;
|
|
840
|
-
let reviewerResult;
|
|
841
|
-
let reviewerResults = [];
|
|
842
|
-
// STEP 1: Invoke reviewer(s)
|
|
843
|
-
if (multiReviewEnabled) {
|
|
844
|
-
const reviewerConfigs = (0, reviewer_js_1.getReviewerConfigs)(phaseConfig);
|
|
845
|
-
if (!jsonMode) {
|
|
846
|
-
console.log(`\n>>> Invoking ${reviewerConfigs.length} REVIEWERS in parallel...\n`);
|
|
847
|
-
if (coordinatorResult) {
|
|
848
|
-
console.log(`Coordinator guidance included (decision: ${coordinatorResult.decision})`);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
const reviewerInvocation = await invokeWithLeaseHeartbeat(projectPath, leaseFence, () => (0, reviewer_js_1.invokeReviewers)(task, effectiveProjectPath, reviewerConfigs, coordinatorResult?.guidance, coordinatorResult?.decision, leaseFence?.runnerId));
|
|
852
|
-
if (reviewerInvocation.superseded || !reviewerInvocation.result) {
|
|
853
|
-
if (!jsonMode) {
|
|
854
|
-
console.log('\n↺ Lease ownership changed during reviewer invocation; skipping post-processing in this runner.');
|
|
855
|
-
}
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
reviewerResults = reviewerInvocation.result;
|
|
859
|
-
const failedReviewerIndex = reviewerResults.findIndex((res) => !res.success || res.timedOut);
|
|
860
|
-
if (failedReviewerIndex !== -1) {
|
|
861
|
-
const failedReviewer = reviewerResults[failedReviewerIndex];
|
|
862
|
-
const failedConfig = reviewerConfigs[failedReviewerIndex];
|
|
863
|
-
const providerName = failedReviewer.provider ??
|
|
864
|
-
failedConfig?.provider ??
|
|
865
|
-
phaseConfig.ai?.reviewer?.provider ??
|
|
866
|
-
'unknown';
|
|
867
|
-
const modelName = failedReviewer.model ??
|
|
868
|
-
failedConfig?.model ??
|
|
869
|
-
phaseConfig.ai?.reviewer?.model ??
|
|
870
|
-
'unknown';
|
|
871
|
-
const output = (failedReviewer.stderr || failedReviewer.stdout || '').trim();
|
|
872
|
-
const failed = await handleProviderInvocationFailure(db, task.id, {
|
|
873
|
-
role: 'reviewer',
|
|
874
|
-
provider: providerName,
|
|
875
|
-
model: modelName,
|
|
876
|
-
exitCode: failedReviewer.exitCode ?? 1,
|
|
877
|
-
output,
|
|
878
|
-
}, jsonMode);
|
|
879
|
-
if (failed.shouldStopTask) {
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
(0, queries_js_1.clearTaskFailureCount)(db, task.id);
|
|
885
|
-
reviewerResult = reviewerResults[0];
|
|
886
|
-
effectiveMultiReviewEnabled = reviewerResults.length > 1;
|
|
887
|
-
}
|
|
888
|
-
else {
|
|
889
|
-
if (!jsonMode) {
|
|
890
|
-
console.log('\n>>> Invoking REVIEWER...\n');
|
|
891
|
-
if (coordinatorResult) {
|
|
892
|
-
console.log(`Coordinator guidance included (decision: ${coordinatorResult.decision})`);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
const reviewerInvocation = await invokeWithLeaseHeartbeat(projectPath, leaseFence, () => (0, reviewer_js_1.invokeReviewer)(task, effectiveProjectPath, coordinatorResult?.guidance, coordinatorResult?.decision, undefined, leaseFence?.runnerId));
|
|
896
|
-
if (reviewerInvocation.superseded || !reviewerInvocation.result) {
|
|
897
|
-
if (!jsonMode) {
|
|
898
|
-
console.log('\n↺ Lease ownership changed during reviewer invocation; skipping post-processing in this runner.');
|
|
899
|
-
}
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
reviewerResult = reviewerInvocation.result;
|
|
903
|
-
if (!reviewerResult.success || reviewerResult.timedOut) {
|
|
904
|
-
const providerName = reviewerResult.provider ??
|
|
905
|
-
phaseConfig.ai?.reviewer?.provider ??
|
|
906
|
-
'unknown';
|
|
907
|
-
const modelName = reviewerResult.model ??
|
|
908
|
-
phaseConfig.ai?.reviewer?.model ??
|
|
909
|
-
'unknown';
|
|
910
|
-
// Check for credit/rate_limit exhaustion before provider failure handling
|
|
911
|
-
const registry = await (0, registry_js_1.getProviderRegistry)();
|
|
912
|
-
const prov = registry.tryGet(providerName);
|
|
913
|
-
if (prov) {
|
|
914
|
-
const classified = prov.classifyResult(reviewerResult);
|
|
915
|
-
if (classified?.type === 'credit_exhaustion') {
|
|
916
|
-
return { action: 'pause_credit_exhaustion', provider: providerName, model: modelName, role: 'reviewer', message: classified.message };
|
|
917
|
-
}
|
|
918
|
-
if (classified?.type === 'rate_limit') {
|
|
919
|
-
return { action: 'rate_limit', provider: providerName, model: modelName, role: 'reviewer', message: classified.message, retryAfterMs: classified.retryAfterMs };
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
// Non-credit/rate-limit reviewer failure: fall through to orchestrator
|
|
923
|
-
}
|
|
924
|
-
else {
|
|
925
|
-
(0, queries_js_1.clearTaskFailureCount)(db, task.id);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
// STEP 2: Gather git context
|
|
929
|
-
const commit_sha = (0, status_js_1.getCurrentCommitSha)(effectiveProjectPath) || '';
|
|
930
|
-
const files_changed = (0, status_js_1.getModifiedFiles)(effectiveProjectPath);
|
|
931
|
-
const diffStats = (0, status_js_1.getDiffStats)(effectiveProjectPath);
|
|
932
|
-
const gitContext = {
|
|
933
|
-
commit_sha,
|
|
934
|
-
files_changed,
|
|
935
|
-
additions: diffStats.additions,
|
|
936
|
-
deletions: diffStats.deletions,
|
|
937
|
-
};
|
|
938
|
-
// STEP 3: Resolve decision and merge notes if needed
|
|
939
|
-
let decision;
|
|
940
|
-
if (effectiveMultiReviewEnabled) {
|
|
941
|
-
const { decision: finalDecision, needsMerge } = (0, reviewer_js_1.resolveDecision)(reviewerResults);
|
|
942
|
-
if (needsMerge) {
|
|
943
|
-
// Invoke multi-reviewer orchestrator to merge notes
|
|
944
|
-
const multiContext = {
|
|
945
|
-
task: {
|
|
946
|
-
id: task.id,
|
|
947
|
-
title: task.title,
|
|
948
|
-
rejection_count: task.rejection_count,
|
|
949
|
-
},
|
|
950
|
-
reviewer_results: reviewerResults.map(r => ({
|
|
951
|
-
provider: r.provider || 'unknown',
|
|
952
|
-
model: r.model || 'unknown',
|
|
953
|
-
decision: r.decision || 'unclear',
|
|
954
|
-
stdout: r.stdout,
|
|
955
|
-
stderr: r.stderr,
|
|
956
|
-
duration_ms: r.duration,
|
|
957
|
-
})),
|
|
958
|
-
git_context: gitContext,
|
|
959
|
-
};
|
|
960
|
-
try {
|
|
961
|
-
const orchestratorOutput = await (0, invoke_js_1.invokeMultiReviewerOrchestrator)(multiContext, projectPath);
|
|
962
|
-
const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
|
|
963
|
-
decision = handler.parseReviewerOutput(orchestratorOutput);
|
|
964
|
-
}
|
|
965
|
-
catch (error) {
|
|
966
|
-
console.error('Multi-reviewer orchestrator failed:', error);
|
|
967
|
-
decision = {
|
|
968
|
-
decision: 'unclear',
|
|
969
|
-
reasoning: 'FALLBACK: Multi-reviewer orchestrator failed',
|
|
970
|
-
notes: 'Review unclear, retrying',
|
|
971
|
-
next_status: 'review',
|
|
972
|
-
rejection_count: task.rejection_count,
|
|
973
|
-
confidence: 'low',
|
|
974
|
-
push_to_remote: false,
|
|
975
|
-
repeated_issue: false,
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
// No merge needed (decision is Approve, Dispute, or Skip)
|
|
981
|
-
const multiContext = {
|
|
982
|
-
task: {
|
|
983
|
-
id: task.id,
|
|
984
|
-
title: task.title,
|
|
985
|
-
rejection_count: task.rejection_count,
|
|
986
|
-
},
|
|
987
|
-
reviewer_results: reviewerResults.map(r => ({
|
|
988
|
-
provider: r.provider || 'unknown',
|
|
989
|
-
model: r.model || 'unknown',
|
|
990
|
-
decision: r.decision || 'unclear',
|
|
991
|
-
stdout: r.stdout,
|
|
992
|
-
stderr: r.stderr,
|
|
993
|
-
duration_ms: r.duration,
|
|
994
|
-
})),
|
|
995
|
-
git_context: gitContext,
|
|
996
|
-
};
|
|
997
|
-
try {
|
|
998
|
-
const orchestratorOutput = await (0, invoke_js_1.invokeMultiReviewerOrchestrator)(multiContext, projectPath);
|
|
999
|
-
const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
|
|
1000
|
-
decision = handler.parseReviewerOutput(orchestratorOutput);
|
|
1001
|
-
}
|
|
1002
|
-
catch (error) {
|
|
1003
|
-
console.error('Multi-reviewer orchestrator failed (consensus path):', error);
|
|
1004
|
-
const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
|
|
1005
|
-
// REQUIRE UNANIMOUS CONSENSUS for the non-reject finalDecision
|
|
1006
|
-
const isUnanimousConsensus = reviewerResults.length > 0 && reviewerResults.every(r => {
|
|
1007
|
-
if (r.decision === finalDecision)
|
|
1008
|
-
return true;
|
|
1009
|
-
return handler.extractExplicitReviewerDecision(r.stdout) === finalDecision;
|
|
1010
|
-
});
|
|
1011
|
-
if (isUnanimousConsensus) {
|
|
1012
|
-
const primaryResult = reviewerResults.find(r => r.decision === finalDecision) || reviewerResults[0];
|
|
1013
|
-
decision = {
|
|
1014
|
-
decision: (finalDecision === 'unclear' ? 'unclear' : finalDecision),
|
|
1015
|
-
reasoning: `FALLBACK: Multi-reviewer orchestrator failed but all reviewers reached consensus: ${finalDecision}`,
|
|
1016
|
-
notes: primaryResult?.notes || primaryResult?.stdout || 'No notes provided',
|
|
1017
|
-
next_status: finalDecision === 'approve' ? 'completed' :
|
|
1018
|
-
finalDecision === 'reject' ? 'in_progress' :
|
|
1019
|
-
finalDecision === 'dispute' ? 'disputed' :
|
|
1020
|
-
finalDecision === 'skip' ? 'skipped' : 'review',
|
|
1021
|
-
rejection_count: task.rejection_count,
|
|
1022
|
-
confidence: 'low',
|
|
1023
|
-
push_to_remote: ['approve', 'dispute', 'skip'].includes(finalDecision),
|
|
1024
|
-
repeated_issue: false,
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
else {
|
|
1028
|
-
decision = {
|
|
1029
|
-
decision: 'unclear',
|
|
1030
|
-
reasoning: 'FALLBACK: Multi-reviewer orchestrator failed and no unanimous consensus',
|
|
1031
|
-
notes: 'Review unclear, retrying',
|
|
1032
|
-
next_status: 'review',
|
|
1033
|
-
rejection_count: task.rejection_count,
|
|
1034
|
-
confidence: 'low',
|
|
1035
|
-
push_to_remote: false,
|
|
1036
|
-
repeated_issue: false,
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
else {
|
|
1043
|
-
// Single reviewer flow
|
|
1044
|
-
const context = {
|
|
1045
|
-
task: {
|
|
1046
|
-
id: task.id,
|
|
1047
|
-
title: task.title,
|
|
1048
|
-
rejection_count: task.rejection_count,
|
|
1049
|
-
},
|
|
1050
|
-
reviewer_output: {
|
|
1051
|
-
stdout: reviewerResult.stdout,
|
|
1052
|
-
stderr: reviewerResult.stderr,
|
|
1053
|
-
exit_code: reviewerResult.exitCode,
|
|
1054
|
-
timed_out: reviewerResult.timedOut,
|
|
1055
|
-
duration_ms: reviewerResult.duration,
|
|
1056
|
-
},
|
|
1057
|
-
git_context: gitContext,
|
|
1058
|
-
};
|
|
1059
|
-
try {
|
|
1060
|
-
const orchestratorOutput = await (0, invoke_js_1.invokeReviewerOrchestrator)(context, projectPath);
|
|
1061
|
-
const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
|
|
1062
|
-
decision = handler.parseReviewerOutput(orchestratorOutput);
|
|
1063
|
-
// If orchestrator returned unclear, check reviewer stdout for explicit decision token
|
|
1064
|
-
if (decision.decision === 'unclear') {
|
|
1065
|
-
const reviewerStdout = reviewerResult?.stdout ?? '';
|
|
1066
|
-
const explicitDecision = handler.extractExplicitReviewerDecision(reviewerStdout);
|
|
1067
|
-
if (explicitDecision) {
|
|
1068
|
-
decision = {
|
|
1069
|
-
decision: explicitDecision,
|
|
1070
|
-
reasoning: `FALLBACK: Orchestrator unclear but reviewer explicitly signaled ${explicitDecision.toUpperCase()}`,
|
|
1071
|
-
notes: reviewerStdout,
|
|
1072
|
-
next_status: explicitDecision === 'approve' ? 'completed' :
|
|
1073
|
-
explicitDecision === 'reject' ? 'in_progress' :
|
|
1074
|
-
explicitDecision === 'dispute' ? 'disputed' :
|
|
1075
|
-
explicitDecision === 'skip' ? 'skipped' : 'review',
|
|
1076
|
-
rejection_count: task.rejection_count,
|
|
1077
|
-
confidence: 'medium',
|
|
1078
|
-
push_to_remote: ['approve', 'dispute', 'skip'].includes(explicitDecision),
|
|
1079
|
-
repeated_issue: false,
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
catch (error) {
|
|
1085
|
-
console.error('Orchestrator invocation failed:', error);
|
|
1086
|
-
const orchestratorFailure = await classifyOrchestratorFailure(error, projectPath);
|
|
1087
|
-
const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
|
|
1088
|
-
const reviewerStdout = reviewerResult?.stdout ?? '';
|
|
1089
|
-
const explicitDecision = handler.extractExplicitReviewerDecision(reviewerStdout);
|
|
1090
|
-
const failureReason = orchestratorFailure
|
|
1091
|
-
? `${orchestratorFailure.type}: ${orchestratorFailure.message}`
|
|
1092
|
-
: summarizeErrorMessage(error);
|
|
1093
|
-
if (explicitDecision) {
|
|
1094
|
-
decision = {
|
|
1095
|
-
decision: explicitDecision,
|
|
1096
|
-
reasoning: `FALLBACK: Orchestrator failed (${failureReason}) but reviewer explicitly signaled ${explicitDecision.toUpperCase()}`,
|
|
1097
|
-
notes: reviewerStdout,
|
|
1098
|
-
next_status: explicitDecision === 'approve' ? 'completed' :
|
|
1099
|
-
explicitDecision === 'reject' ? 'in_progress' :
|
|
1100
|
-
explicitDecision === 'dispute' ? 'disputed' :
|
|
1101
|
-
explicitDecision === 'skip' ? 'skipped' : 'review',
|
|
1102
|
-
rejection_count: task.rejection_count,
|
|
1103
|
-
confidence: 'low',
|
|
1104
|
-
push_to_remote: ['approve', 'dispute', 'skip'].includes(explicitDecision),
|
|
1105
|
-
repeated_issue: false,
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
else {
|
|
1109
|
-
decision = {
|
|
1110
|
-
decision: 'unclear',
|
|
1111
|
-
reasoning: `FALLBACK: Orchestrator failed (${failureReason}), retrying review`,
|
|
1112
|
-
notes: 'Review unclear, retrying',
|
|
1113
|
-
next_status: 'review',
|
|
1114
|
-
rejection_count: task.rejection_count,
|
|
1115
|
-
confidence: 'low',
|
|
1116
|
-
push_to_remote: false,
|
|
1117
|
-
repeated_issue: false,
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
// STEP 4: Fallback for unclear decisions (catches ALL unclear, not just orchestrator parse failures)
|
|
1123
|
-
if (decision.decision === 'unclear') {
|
|
1124
|
-
const consecutiveParseFallbackRetries = countConsecutiveUnclearEntries(db, task.id) + 1;
|
|
1125
|
-
if (consecutiveParseFallbackRetries >= MAX_ORCHESTRATOR_PARSE_RETRIES) {
|
|
1126
|
-
decision = {
|
|
1127
|
-
...decision,
|
|
1128
|
-
decision: 'dispute',
|
|
1129
|
-
reasoning: `Orchestrator parse failed ${consecutiveParseFallbackRetries} times; escalating to dispute`,
|
|
1130
|
-
notes: 'Escalated to disputed to prevent endless unclear-review retries',
|
|
1131
|
-
next_status: 'disputed',
|
|
1132
|
-
confidence: 'low',
|
|
1133
|
-
push_to_remote: false,
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
else {
|
|
1137
|
-
decision = {
|
|
1138
|
-
...decision,
|
|
1139
|
-
reasoning: `${decision.reasoning} (parse_retry ${consecutiveParseFallbackRetries}/${MAX_ORCHESTRATOR_PARSE_RETRIES})`,
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
// STEP 5: Log orchestrator decision for audit trail (non-transition row).
|
|
1144
|
-
// Concrete status transitions are recorded by approve/reject/update calls below.
|
|
1145
|
-
(0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'orchestrator', {
|
|
1146
|
-
actorType: 'orchestrator',
|
|
1147
|
-
notes: `[${decision.decision}] ${decision.reasoning} (confidence: ${decision.confidence})`,
|
|
1148
|
-
category: 'decision',
|
|
1149
|
-
});
|
|
1150
|
-
if (!refreshParallelWorkstreamLease(projectPath, leaseFence)) {
|
|
1151
|
-
if (!jsonMode) {
|
|
1152
|
-
console.log('\n↺ Lease ownership changed before applying reviewer decision; skipping in this runner.');
|
|
1153
|
-
}
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
// STEP 5.5: Create follow-up tasks if any (ONLY on approval)
|
|
1157
|
-
if (decision.decision === 'approve' && decision.follow_up_tasks && decision.follow_up_tasks.length > 0) {
|
|
1158
|
-
const followUpConfig = (0, loader_js_1.loadConfig)(projectPath);
|
|
1159
|
-
const depth = (0, queries_js_1.getFollowUpDepth)(db, task.id);
|
|
1160
|
-
const maxDepth = followUpConfig.followUpTasks?.maxDepth ?? 2;
|
|
1161
|
-
if (depth < maxDepth) {
|
|
1162
|
-
for (const followUp of decision.follow_up_tasks) {
|
|
1163
|
-
try {
|
|
1164
|
-
const nextDepth = depth + 1;
|
|
1165
|
-
// Policy: Auto-implement depth 1 if configured.
|
|
1166
|
-
// Depth 2+ always requires human promotion (approval).
|
|
1167
|
-
let requiresPromotion = true;
|
|
1168
|
-
if (nextDepth === 1 && followUpConfig.followUpTasks?.autoImplementDepth1) {
|
|
1169
|
-
requiresPromotion = false;
|
|
1170
|
-
}
|
|
1171
|
-
const followUpId = (0, queries_js_1.createFollowUpTask)(db, {
|
|
1172
|
-
title: followUp.title,
|
|
1173
|
-
description: followUp.description,
|
|
1174
|
-
sectionId: task.section_id,
|
|
1175
|
-
referenceTaskId: task.id,
|
|
1176
|
-
referenceCommit: submissionCommitSha,
|
|
1177
|
-
requiresPromotion,
|
|
1178
|
-
depth: nextDepth,
|
|
1179
|
-
});
|
|
1180
|
-
if (!jsonMode) {
|
|
1181
|
-
const statusLabel = requiresPromotion ? '(deferred)' : '(active)';
|
|
1182
|
-
console.log(`\n+ Created follow-up task ${statusLabel}: ${followUp.title} (${followUpId.substring(0, 8)})`);
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
catch (error) {
|
|
1186
|
-
console.warn(`Failed to create follow-up task "${followUp.title}":`, error);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
else if (!jsonMode) {
|
|
1191
|
-
console.log(`\n! Follow-up depth limit reached (${depth}), skipping new follow-ups.`);
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
// STEP 6: Execute the decision
|
|
1195
|
-
const commitSha = submissionCommitSha;
|
|
1196
|
-
switch (decision.decision) {
|
|
1197
|
-
case 'approve':
|
|
1198
|
-
if (poolSlotContext) {
|
|
1199
|
-
// ── Pool slot: post-review gate + merge pipeline ──
|
|
1200
|
-
(0, git_lifecycle_js_1.postReviewGate)(effectiveProjectPath);
|
|
1201
|
-
// Refresh slot from DB before merge
|
|
1202
|
-
const freshSlot = (0, pool_js_1.getSlot)(poolSlotContext.globalDb, poolSlotContext.slot.id);
|
|
1203
|
-
if (!freshSlot) {
|
|
1204
|
-
if (!jsonMode)
|
|
1205
|
-
console.log('\n✗ Pool slot disappeared during review');
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
const mergeResult = (0, git_lifecycle_js_1.mergeToBase)(poolSlotContext.globalDb, freshSlot, task.id);
|
|
1209
|
-
if (!mergeResult.ok) {
|
|
1210
|
-
const failureResult = (0, merge_pipeline_js_1.handleMergeFailure)(db, poolSlotContext, task.id, mergeResult);
|
|
1211
|
-
if (!jsonMode) {
|
|
1212
|
-
if (failureResult.taskBlocked) {
|
|
1213
|
-
console.log(`\n✗ Task BLOCKED (${failureResult.blockStatus}): ${failureResult.reason}`);
|
|
1214
|
-
}
|
|
1215
|
-
else {
|
|
1216
|
-
console.log(`\n⟳ Merge failed, returning to pending: ${failureResult.reason}`);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
// Merge succeeded — approve the task
|
|
1222
|
-
(0, queries_js_1.approveTask)(db, task.id, 'orchestrator', decision.notes, mergeResult.mergedSha || commitSha);
|
|
1223
|
-
(0, pool_js_1.releaseSlot)(poolSlotContext.globalDb, poolSlotContext.slot.id);
|
|
1224
|
-
if (!jsonMode) {
|
|
1225
|
-
console.log(`\n✓ Task APPROVED and merged (sha: ${mergeResult.mergedSha})`);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
else {
|
|
1229
|
-
// Legacy path: direct push
|
|
1230
|
-
(0, queries_js_1.approveTask)(db, task.id, 'orchestrator', decision.notes, commitSha);
|
|
1231
|
-
if (!jsonMode) {
|
|
1232
|
-
console.log(`\n✓ Task APPROVED (confidence: ${decision.confidence})`);
|
|
1233
|
-
console.log('Pushing to git...');
|
|
1234
|
-
}
|
|
1235
|
-
if (!refreshParallelWorkstreamLease(projectPath, leaseFence)) {
|
|
1236
|
-
if (!jsonMode) {
|
|
1237
|
-
console.log('\n↺ Lease ownership lost before review push; skipping push in this runner.');
|
|
1238
|
-
}
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
const pushResult = (0, push_js_1.pushToRemote)(effectiveProjectPath, 'origin', branchName);
|
|
1242
|
-
if (!jsonMode && pushResult.success) {
|
|
1243
|
-
console.log(`Pushed successfully (${pushResult.commitHash})`);
|
|
1244
|
-
}
|
|
1245
|
-
else if (!jsonMode) {
|
|
1246
|
-
console.warn('Push failed. Will stack and retry on next completion.');
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
break;
|
|
1250
|
-
case 'reject':
|
|
1251
|
-
(0, queries_js_1.rejectTask)(db, task.id, 'orchestrator', decision.notes, commitSha);
|
|
1252
|
-
if (!jsonMode) {
|
|
1253
|
-
console.log(`\n✗ Task REJECTED (${task.rejection_count + 1}/15, confidence: ${decision.confidence})`);
|
|
1254
|
-
console.log('Returning to coder for fixes.');
|
|
1255
|
-
}
|
|
1256
|
-
break;
|
|
1257
|
-
case 'dispute':
|
|
1258
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'disputed', 'orchestrator', decision.notes, commitSha);
|
|
1259
|
-
if (!jsonMode) {
|
|
1260
|
-
console.log(`\n! Task DISPUTED (confidence: ${decision.confidence})`);
|
|
1261
|
-
console.log('Pushing current work and moving to next task.');
|
|
1262
|
-
}
|
|
1263
|
-
if (!refreshParallelWorkstreamLease(projectPath, leaseFence)) {
|
|
1264
|
-
if (!jsonMode) {
|
|
1265
|
-
console.log('\n↺ Lease ownership lost before dispute push; skipping push in this runner.');
|
|
1266
|
-
}
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
const disputePush = (0, push_js_1.pushToRemote)(effectiveProjectPath, 'origin', branchName);
|
|
1270
|
-
if (!jsonMode && disputePush.success) {
|
|
1271
|
-
console.log(`Pushed disputed work (${disputePush.commitHash})`);
|
|
1272
|
-
}
|
|
1273
|
-
break;
|
|
1274
|
-
case 'skip':
|
|
1275
|
-
(0, queries_js_1.updateTaskStatus)(db, task.id, 'skipped', 'orchestrator', decision.notes, commitSha);
|
|
1276
|
-
if (!jsonMode) {
|
|
1277
|
-
console.log(`\n⏭ Task SKIPPED (confidence: ${decision.confidence})`);
|
|
1278
|
-
}
|
|
1279
|
-
break;
|
|
1280
|
-
case 'unclear':
|
|
1281
|
-
if (!jsonMode) {
|
|
1282
|
-
console.log(`\n? Review unclear (${decision.reasoning}), will retry`);
|
|
1283
|
-
}
|
|
1284
|
-
break;
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.runReviewerPhase = exports.runCoderPhase = void 0;
|
|
13
|
+
var loop_phases_coder_js_1 = require("./loop-phases-coder.js");
|
|
14
|
+
Object.defineProperty(exports, "runCoderPhase", { enumerable: true, get: function () { return loop_phases_coder_js_1.runCoderPhase; } });
|
|
15
|
+
var loop_phases_reviewer_js_1 = require("./loop-phases-reviewer.js");
|
|
16
|
+
Object.defineProperty(exports, "runReviewerPhase", { enumerable: true, get: function () { return loop_phases_reviewer_js_1.runReviewerPhase; } });
|
|
1287
17
|
//# sourceMappingURL=loop-phases.js.map
|