steroids-cli 0.9.30 → 0.9.32

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 (51) hide show
  1. package/dist/commands/loop-phases.d.ts +1 -1
  2. package/dist/commands/loop-phases.d.ts.map +1 -1
  3. package/dist/commands/loop-phases.js +404 -67
  4. package/dist/commands/loop-phases.js.map +1 -1
  5. package/dist/database/queries.d.ts +20 -0
  6. package/dist/database/queries.d.ts.map +1 -1
  7. package/dist/database/queries.js +63 -0
  8. package/dist/database/queries.js.map +1 -1
  9. package/dist/git/status.d.ts +10 -0
  10. package/dist/git/status.d.ts.map +1 -1
  11. package/dist/git/status.js +66 -0
  12. package/dist/git/status.js.map +1 -1
  13. package/dist/git/submission-resolution.d.ts +26 -0
  14. package/dist/git/submission-resolution.d.ts.map +1 -0
  15. package/dist/git/submission-resolution.js +213 -0
  16. package/dist/git/submission-resolution.js.map +1 -0
  17. package/dist/orchestrator/coordinator.d.ts +2 -0
  18. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  19. package/dist/orchestrator/coordinator.js +17 -1
  20. package/dist/orchestrator/coordinator.js.map +1 -1
  21. package/dist/orchestrator/post-coder.d.ts.map +1 -1
  22. package/dist/orchestrator/post-coder.js +82 -5
  23. package/dist/orchestrator/post-coder.js.map +1 -1
  24. package/dist/orchestrator/reviewer.d.ts.map +1 -1
  25. package/dist/orchestrator/reviewer.js +69 -41
  26. package/dist/orchestrator/reviewer.js.map +1 -1
  27. package/dist/orchestrator/schemas.d.ts.map +1 -1
  28. package/dist/orchestrator/schemas.js +12 -0
  29. package/dist/orchestrator/schemas.js.map +1 -1
  30. package/dist/orchestrator/types.d.ts +3 -0
  31. package/dist/orchestrator/types.d.ts.map +1 -1
  32. package/dist/prompts/coder.d.ts.map +1 -1
  33. package/dist/prompts/coder.js +65 -2
  34. package/dist/prompts/coder.js.map +1 -1
  35. package/dist/prompts/prompt-helpers.d.ts.map +1 -1
  36. package/dist/prompts/prompt-helpers.js +15 -4
  37. package/dist/prompts/prompt-helpers.js.map +1 -1
  38. package/dist/prompts/reviewer.d.ts +4 -3
  39. package/dist/prompts/reviewer.d.ts.map +1 -1
  40. package/dist/prompts/reviewer.js +75 -14
  41. package/dist/prompts/reviewer.js.map +1 -1
  42. package/dist/runners/global-db.d.ts.map +1 -1
  43. package/dist/runners/global-db.js +7 -3
  44. package/dist/runners/global-db.js.map +1 -1
  45. package/dist/runners/orchestrator-loop.d.ts.map +1 -1
  46. package/dist/runners/orchestrator-loop.js +2 -2
  47. package/dist/runners/orchestrator-loop.js.map +1 -1
  48. package/dist/runners/wakeup.d.ts.map +1 -1
  49. package/dist/runners/wakeup.js +142 -2
  50. package/dist/runners/wakeup.js.map +1 -1
  51. package/package.json +1 -1
@@ -21,6 +21,6 @@ export interface CreditExhaustionResult {
21
21
  message: string;
22
22
  retryAfterMs?: number;
23
23
  }
24
- export declare function runCoderPhase(db: ReturnType<typeof openDatabase>['db'], task: ReturnType<typeof getTask>, projectPath: string, action: 'start' | 'resume', jsonMode?: boolean, coordinatorCache?: Map<string, CoordinatorResult>, coordinatorThresholds?: number[], leaseFence?: LeaseFenceContext): Promise<CreditExhaustionResult | void>;
24
+ export declare function runCoderPhase(db: ReturnType<typeof openDatabase>['db'], task: ReturnType<typeof getTask>, projectPath: string, action: 'start' | 'resume', jsonMode?: boolean, coordinatorCache?: Map<string, CoordinatorResult>, coordinatorThresholds?: number[], leaseFence?: LeaseFenceContext, branchName?: string): Promise<CreditExhaustionResult | void>;
25
25
  export declare function runReviewerPhase(db: ReturnType<typeof openDatabase>['db'], task: ReturnType<typeof getTask>, projectPath: string, jsonMode?: boolean, coordinatorResult?: CoordinatorResult, branchName?: string, leaseFence?: LeaseFenceContext): Promise<CreditExhaustionResult | void>;
26
26
  //# sourceMappingURL=loop-phases.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loop-phases.d.ts","sourceRoot":"","sources":["../../src/commands/loop-phases.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,OAAO,EAWR,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAU9D,OAAO,EAA8C,KAAK,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAwBpH,OAAO,EAAE,KAAK,iBAAiB,EAAE,CAAC;AAElC,UAAU,iBAAiB;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA8CD;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,yBAAyB,GAAG,YAAY,CAAC;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,GAAG,UAAU,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAkLD,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,IAAI,CAAC,EACzC,IAAI,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,EAChC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,OAAO,GAAG,QAAQ,EAC1B,QAAQ,UAAQ,EAChB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACjD,qBAAqB,CAAC,EAAE,MAAM,EAAE,EAChC,UAAU,CAAC,EAAE,iBAAiB,GAC7B,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAwSxC;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,IAAI,CAAC,EACzC,IAAI,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,EAChC,WAAW,EAAE,MAAM,EACnB,QAAQ,UAAQ,EAChB,iBAAiB,CAAC,EAAE,iBAAiB,EACrC,UAAU,GAAE,MAAe,EAC3B,UAAU,CAAC,EAAE,iBAAiB,GAC7B,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAgbxC"}
1
+ {"version":3,"file":"loop-phases.d.ts","sourceRoot":"","sources":["../../src/commands/loop-phases.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,OAAO,EAaR,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAU9D,OAAO,EAA8C,KAAK,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AA2BpH,OAAO,EAAE,KAAK,iBAAiB,EAAE,CAAC;AAElC,UAAU,iBAAiB;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA8CD;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,yBAAyB,GAAG,YAAY,CAAC;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,GAAG,UAAU,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAoTD,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,IAAI,CAAC,EACzC,IAAI,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,EAChC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,OAAO,GAAG,QAAQ,EAC1B,QAAQ,UAAQ,EAChB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACjD,qBAAqB,CAAC,EAAE,MAAM,EAAE,EAChC,UAAU,CAAC,EAAE,iBAAiB,EAC9B,UAAU,SAAS,GAClB,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAikBxC;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,IAAI,CAAC,EACzC,IAAI,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,EAChC,WAAW,EAAE,MAAM,EACnB,QAAQ,UAAQ,EAChB,iBAAiB,CAAC,EAAE,iBAAiB,EACrC,UAAU,GAAE,MAAe,EAC3B,UAAU,CAAC,EAAE,iBAAiB,GAC7B,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAwcxC"}
@@ -13,6 +13,7 @@ const reviewer_js_1 = require("../orchestrator/reviewer.js");
13
13
  const coordinator_js_1 = require("../orchestrator/coordinator.js");
14
14
  const push_js_1 = require("../git/push.js");
15
15
  const status_js_1 = require("../git/status.js");
16
+ const submission_resolution_js_1 = require("../git/submission-resolution.js");
16
17
  const invoke_js_1 = require("../orchestrator/invoke.js");
17
18
  const fallback_handler_js_1 = require("../orchestrator/fallback-handler.js");
18
19
  const loader_js_1 = require("../config/loader.js");
@@ -136,9 +137,58 @@ async function checkNonRetryableProviderFailure(result, role, projectPath, revie
136
137
  }
137
138
  return null;
138
139
  }
140
+ function summarizeErrorMessage(error) {
141
+ const raw = error instanceof Error ? error.message : String(error);
142
+ return raw.replace(/\s+/g, ' ').trim().slice(0, 220);
143
+ }
144
+ /**
145
+ * Classify orchestrator invocation failures so we can distinguish transient parse
146
+ * noise from hard provider/config failures (auth/model/availability).
147
+ */
148
+ async function classifyOrchestratorFailure(error, projectPath) {
149
+ const message = summarizeErrorMessage(error);
150
+ const lower = message.toLowerCase();
151
+ if (lower.includes('orchestrator ai provider not configured')) {
152
+ return { type: 'orchestrator_unconfigured', message, retryable: false };
153
+ }
154
+ if (lower.includes("provider '") && lower.includes('is not available')) {
155
+ return { type: 'provider_unavailable', message, retryable: false };
156
+ }
157
+ const config = (0, loader_js_1.loadConfig)(projectPath);
158
+ const providerName = config.ai?.orchestrator?.provider;
159
+ if (!providerName) {
160
+ return { type: 'orchestrator_unconfigured', message, retryable: false };
161
+ }
162
+ const registry = await (0, registry_js_1.getProviderRegistry)();
163
+ const provider = registry.tryGet(providerName);
164
+ if (!provider) {
165
+ return { type: 'provider_unavailable', message, retryable: false };
166
+ }
167
+ const syntheticFailure = {
168
+ success: false,
169
+ exitCode: 1,
170
+ stdout: '',
171
+ stderr: message,
172
+ duration: 0,
173
+ timedOut: false,
174
+ };
175
+ const classified = provider.classifyResult(syntheticFailure);
176
+ if (!classified) {
177
+ return null;
178
+ }
179
+ return {
180
+ type: classified.type,
181
+ message: classified.message || message,
182
+ retryable: classified.retryable,
183
+ };
184
+ }
139
185
  const MAX_ORCHESTRATOR_PARSE_RETRIES = 3;
186
+ const MAX_CONTRACT_VIOLATION_RETRIES = 3;
140
187
  const CODER_PARSE_FALLBACK_MARKER = '[retry] FALLBACK: Orchestrator failed, defaulting to retry';
141
188
  const REVIEWER_PARSE_FALLBACK_MARKER = '[unclear] FALLBACK: Orchestrator failed, retrying review';
189
+ const CONTRACT_CHECKLIST_MARKER = '[contract:checklist]';
190
+ const CONTRACT_REJECTION_RESPONSE_MARKER = '[contract:rejection_response]';
191
+ const MUST_IMPLEMENT_MARKER = '[must_implement]';
142
192
  function countConsecutiveOrchestratorFallbackEntries(db, taskId, marker) {
143
193
  const audit = (0, queries_js_1.getTaskAudit)(db, taskId);
144
194
  let count = 0;
@@ -173,6 +223,34 @@ function countConsecutiveUnclearEntries(db, taskId) {
173
223
  }
174
224
  return count;
175
225
  }
226
+ function countConsecutiveTaggedOrchestratorEntries(db, taskId, requiredTag, tagFamilyPrefix) {
227
+ const audit = (0, queries_js_1.getTaskAudit)(db, taskId);
228
+ let count = 0;
229
+ for (let i = audit.length - 1; i >= 0; i--) {
230
+ const entry = audit[i];
231
+ if (entry.actor !== 'orchestrator') {
232
+ continue; // tolerate non-orchestrator audit noise
233
+ }
234
+ const notes = entry.notes ?? '';
235
+ if (notes.includes(requiredTag)) {
236
+ count += 1;
237
+ continue;
238
+ }
239
+ if (tagFamilyPrefix && notes.includes(tagFamilyPrefix)) {
240
+ break; // same family, different category = sequence ended
241
+ }
242
+ break;
243
+ }
244
+ return count;
245
+ }
246
+ function countLatestOpenRejectionItems(notes) {
247
+ if (!notes)
248
+ return 0;
249
+ return notes
250
+ .split('\n')
251
+ .filter(line => /^\s*[-*]\s*\[\s\]\s+/.test(line))
252
+ .length;
253
+ }
176
254
  function hasCoderCompletionSignal(output) {
177
255
  const lower = output.toLowerCase();
178
256
  if (/\b(?:not|no)\s+(?:task\s+)?(?:is\s+)?(?:complete|complete[d]?|finished|done|ready)\b/.test(lower)) {
@@ -180,57 +258,117 @@ function hasCoderCompletionSignal(output) {
180
258
  }
181
259
  return /\b(?:task\s+)?(?:is\s+)?(?:complete|complete[d]?|implemented|finished|done|ready for review|ready)\b/.test(lower);
182
260
  }
183
- async function runCoderPhase(db, task, projectPath, action, jsonMode = false, coordinatorCache, coordinatorThresholds, leaseFence) {
261
+ function extractSubmissionCommitToken(output) {
262
+ const match = output.match(/^\s*SUBMISSION_COMMIT\s*:\s*([0-9a-fA-F]{7,40})\b/im);
263
+ return match?.[1] ?? null;
264
+ }
265
+ function resolveCoderSubmittedCommitSha(projectPath, coderOutput, options = {}) {
266
+ const tokenSha = extractSubmissionCommitToken(coderOutput);
267
+ if (tokenSha && (0, status_js_1.isCommitReachableWithFetch)(projectPath, tokenSha, { forceFetch: true })) {
268
+ try {
269
+ return (0, node_child_process_1.execSync)(`git rev-parse ${tokenSha}^{commit}`, {
270
+ cwd: projectPath,
271
+ encoding: 'utf-8',
272
+ stdio: ['pipe', 'pipe', 'pipe'],
273
+ }).trim();
274
+ }
275
+ catch {
276
+ return tokenSha;
277
+ }
278
+ }
279
+ if (options.requireExplicitToken) {
280
+ return undefined;
281
+ }
282
+ return (0, status_js_1.getCurrentCommitSha)(projectPath) || undefined;
283
+ }
284
+ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, coordinatorCache, coordinatorThresholds, leaseFence, branchName = 'main') {
184
285
  if (!task)
185
286
  return;
186
287
  refreshParallelWorkstreamLease(projectPath, leaseFence);
187
288
  let coordinatorGuidance;
188
289
  const thresholds = coordinatorThresholds || [2, 5, 9];
290
+ const persistedMustImplement = (0, queries_js_1.getLatestMustImplementGuidance)(db, task.id);
291
+ const activeMustImplement = persistedMustImplement &&
292
+ task.status === 'in_progress' &&
293
+ task.rejection_count >= persistedMustImplement.rejection_count_watermark
294
+ ? persistedMustImplement
295
+ : null;
189
296
  // Run coordinator at rejection thresholds (same as before)
190
297
  const shouldInvokeCoordinator = thresholds.includes(task.rejection_count);
191
298
  const cachedResult = coordinatorCache?.get(task.id);
192
- if (shouldInvokeCoordinator) {
299
+ if (activeMustImplement) {
300
+ coordinatorGuidance = activeMustImplement.guidance;
301
+ coordinatorCache?.set(task.id, {
302
+ success: true,
303
+ decision: 'guide_coder',
304
+ guidance: activeMustImplement.guidance,
305
+ });
193
306
  if (!jsonMode) {
194
- console.log(`\n>>> Task has ${task.rejection_count} rejections (threshold hit) - invoking COORDINATOR...\n`);
307
+ console.log(`\nActive MUST_IMPLEMENT override detected (rc=${activeMustImplement.rejection_count_watermark})`);
195
308
  }
196
- try {
197
- const rejectionHistory = (0, queries_js_1.getTaskRejections)(db, task.id);
198
- const coordExtra = {};
199
- if (task.section_id) {
200
- const allSectionTasks = (0, queries_js_1.listTasks)(db, { sectionId: task.section_id });
201
- coordExtra.sectionTasks = allSectionTasks.map(t => ({
202
- id: t.id, title: t.title, status: t.status,
203
- }));
204
- }
205
- coordExtra.submissionNotes = (0, queries_js_1.getLatestSubmissionNotes)(db, task.id);
206
- const modified = (0, status_js_1.getModifiedFiles)(projectPath);
207
- if (modified.length > 0) {
208
- coordExtra.gitDiffSummary = modified.join('\n');
209
- }
210
- if (cachedResult) {
211
- coordExtra.previousGuidance = cachedResult.guidance;
212
- }
213
- const coordResult = await (0, coordinator_js_1.invokeCoordinator)(task, rejectionHistory, projectPath, coordExtra);
214
- if (coordResult) {
215
- coordinatorGuidance = coordResult.guidance;
216
- coordinatorCache?.set(task.id, coordResult);
217
- (0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'coordinator', {
218
- actorType: 'orchestrator',
219
- notes: `[${coordResult.decision}] ${coordResult.guidance}`,
220
- });
221
- if (!jsonMode) {
222
- console.log(`\nCoordinator decision: ${coordResult.decision}`);
223
- console.log('Coordinator guidance stored for both coder and reviewer.');
224
- }
309
+ }
310
+ if (shouldInvokeCoordinator) {
311
+ if (activeMustImplement &&
312
+ task.rejection_count === activeMustImplement.rejection_count_watermark) {
313
+ if (!jsonMode) {
314
+ console.log('\nSkipping coordinator reinvocation in same rejection cycle due to active MUST_IMPLEMENT override');
225
315
  }
226
316
  }
227
- catch (error) {
317
+ else {
228
318
  if (!jsonMode) {
229
- console.warn('Coordinator invocation failed, continuing without guidance:', error);
319
+ console.log(`\n>>> Task has ${task.rejection_count} rejections (threshold hit) - invoking COORDINATOR...\n`);
320
+ }
321
+ try {
322
+ const rejectionHistory = (0, queries_js_1.getTaskRejections)(db, task.id);
323
+ const coordExtra = {};
324
+ if (task.section_id) {
325
+ const allSectionTasks = (0, queries_js_1.listTasks)(db, { sectionId: task.section_id });
326
+ coordExtra.sectionTasks = allSectionTasks.map(t => ({
327
+ id: t.id, title: t.title, status: t.status,
328
+ }));
329
+ }
330
+ coordExtra.submissionNotes = (0, queries_js_1.getLatestSubmissionNotes)(db, task.id);
331
+ const modified = (0, status_js_1.getModifiedFiles)(projectPath);
332
+ if (modified.length > 0) {
333
+ coordExtra.gitDiffSummary = modified.join('\n');
334
+ }
335
+ if (cachedResult) {
336
+ coordExtra.previousGuidance = cachedResult.guidance;
337
+ }
338
+ if (activeMustImplement) {
339
+ coordExtra.lockedMustImplementGuidance = activeMustImplement.guidance;
340
+ coordExtra.lockedMustImplementWatermark = activeMustImplement.rejection_count_watermark;
341
+ }
342
+ const coordResult = await (0, coordinator_js_1.invokeCoordinator)(task, rejectionHistory, projectPath, coordExtra);
343
+ if (coordResult) {
344
+ const mustKeepOverride = activeMustImplement &&
345
+ task.rejection_count > activeMustImplement.rejection_count_watermark;
346
+ const normalizedGuidance = mustKeepOverride && !coordResult.guidance.includes('MUST_IMPLEMENT:')
347
+ ? `${activeMustImplement.guidance}\n\nAdditional coordinator guidance:\n${coordResult.guidance}`
348
+ : coordResult.guidance;
349
+ coordinatorGuidance = normalizedGuidance;
350
+ coordinatorCache?.set(task.id, {
351
+ ...coordResult,
352
+ guidance: normalizedGuidance,
353
+ });
354
+ (0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'coordinator', {
355
+ actorType: 'orchestrator',
356
+ notes: `[${coordResult.decision}] ${normalizedGuidance}`,
357
+ });
358
+ if (!jsonMode) {
359
+ console.log(`\nCoordinator decision: ${coordResult.decision}`);
360
+ console.log('Coordinator guidance stored for both coder and reviewer.');
361
+ }
362
+ }
363
+ }
364
+ catch (error) {
365
+ if (!jsonMode) {
366
+ console.warn('Coordinator invocation failed, continuing without guidance:', error);
367
+ }
230
368
  }
231
369
  }
232
370
  }
233
- else if (cachedResult) {
371
+ else if (cachedResult && !activeMustImplement) {
234
372
  coordinatorGuidance = cachedResult.guidance;
235
373
  if (!jsonMode && task.rejection_count >= 2) {
236
374
  console.log(`\nReusing cached coordinator guidance (decision: ${cachedResult.decision})`);
@@ -282,6 +420,8 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
282
420
  const lastRejectionNotes = task.rejection_count > 0
283
421
  ? (0, queries_js_1.getTaskRejections)(db, task.id).slice(-1)[0]?.notes ?? undefined
284
422
  : undefined;
423
+ const requiresExplicitSubmissionCommit = (lastRejectionNotes ?? '').includes('[commit_recovery]');
424
+ const rejectionItemCount = countLatestOpenRejectionItems(lastRejectionNotes);
285
425
  const context = {
286
426
  task: {
287
427
  id: task.id,
@@ -289,6 +429,7 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
289
429
  description: task.title, // Use title as description for now
290
430
  rejection_notes: lastRejectionNotes,
291
431
  rejection_count: task.rejection_count,
432
+ rejection_item_count: rejectionItemCount,
292
433
  },
293
434
  coder_output: {
294
435
  stdout: coderResult.stdout,
@@ -306,40 +447,57 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
306
447
  }
307
448
  catch (error) {
308
449
  console.error('Orchestrator invocation failed:', error);
309
- // Check if coder seems finished even if orchestrator failed
310
- const isTaskComplete = hasCoderCompletionSignal(coderResult.stdout);
311
- // Only count as having work if there are actual relevant uncommitted changes,
312
- // changed files, or recent commits
313
- const hasWork = hasRelevantChanges;
314
- if (isTaskComplete && hasWork) {
450
+ const orchestratorFailure = await classifyOrchestratorFailure(error, projectPath);
451
+ if (orchestratorFailure && !orchestratorFailure.retryable) {
315
452
  orchestratorOutput = JSON.stringify({
316
- action: has_uncommitted ? 'stage_commit_submit' : 'submit',
317
- reasoning: 'FALLBACK: Orchestrator failed but coder signaled completion',
318
- commits: commits.map(c => c.sha),
319
- next_status: 'review',
320
- metadata: {
321
- files_changed: files_changed.length,
322
- confidence: 'low',
323
- exit_clean: true,
324
- has_commits: commits.length > 0,
325
- }
326
- });
327
- }
328
- else {
329
- // Fallback to safe default: retry
330
- orchestratorOutput = JSON.stringify({
331
- action: 'retry',
332
- reasoning: 'FALLBACK: Orchestrator failed, defaulting to retry',
453
+ action: 'error',
454
+ reasoning: `FALLBACK: Non-retryable orchestrator failure (${orchestratorFailure.type})`,
333
455
  commits: [],
334
- next_status: 'in_progress',
456
+ next_status: 'failed',
335
457
  metadata: {
336
458
  files_changed: 0,
337
459
  confidence: 'low',
338
- exit_clean: true,
460
+ exit_clean: false,
339
461
  has_commits: false,
340
462
  }
341
463
  });
342
464
  }
465
+ else {
466
+ // Check if coder seems finished even if orchestrator failed
467
+ const isTaskComplete = hasCoderCompletionSignal(coderResult.stdout);
468
+ // Only count as having work if there are actual relevant uncommitted changes,
469
+ // changed files, or recent commits
470
+ const hasWork = hasRelevantChanges;
471
+ if (isTaskComplete && hasWork) {
472
+ orchestratorOutput = JSON.stringify({
473
+ action: has_uncommitted ? 'stage_commit_submit' : 'submit',
474
+ reasoning: 'FALLBACK: Orchestrator failed but coder signaled completion',
475
+ commits: commits.map(c => c.sha),
476
+ next_status: 'review',
477
+ metadata: {
478
+ files_changed: files_changed.length,
479
+ confidence: 'low',
480
+ exit_clean: true,
481
+ has_commits: commits.length > 0,
482
+ }
483
+ });
484
+ }
485
+ else {
486
+ // Fallback to safe default: retry
487
+ orchestratorOutput = JSON.stringify({
488
+ action: 'retry',
489
+ reasoning: 'FALLBACK: Orchestrator failed, defaulting to retry',
490
+ commits: [],
491
+ next_status: 'in_progress',
492
+ metadata: {
493
+ files_changed: 0,
494
+ confidence: 'low',
495
+ exit_clean: true,
496
+ has_commits: false,
497
+ }
498
+ });
499
+ }
500
+ }
343
501
  }
344
502
  // STEP 5: Parse orchestrator output with fallback
345
503
  const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
@@ -391,6 +549,90 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
391
549
  }
392
550
  }
393
551
  }
552
+ // Contract-violation handling (structured-first, legacy-prefix fallback).
553
+ const legacyChecklistViolation = /^CHECKLIST_REQUIRED:/i.test(decision.reasoning || '');
554
+ const legacyRejectionResponseViolation = /^REJECTION_RESPONSE_REQUIRED:/i.test(decision.reasoning || '');
555
+ const contractViolation = decision.contract_violation
556
+ ?? (legacyChecklistViolation ? 'checklist_required' : null)
557
+ ?? (legacyRejectionResponseViolation ? 'rejection_response_required' : null);
558
+ if (contractViolation) {
559
+ const marker = contractViolation === 'checklist_required'
560
+ ? CONTRACT_CHECKLIST_MARKER
561
+ : CONTRACT_REJECTION_RESPONSE_MARKER;
562
+ const reasonText = (decision.reasoning || '')
563
+ .replace(/^CHECKLIST_REQUIRED:\s*/i, '')
564
+ .replace(/^REJECTION_RESPONSE_REQUIRED:\s*/i, '')
565
+ .trim();
566
+ const cleanReason = reasonText || 'Required output contract not satisfied';
567
+ const consecutiveContractViolations = countConsecutiveTaggedOrchestratorEntries(db, task.id, marker, '[contract:') + 1;
568
+ if (consecutiveContractViolations >= MAX_CONTRACT_VIOLATION_RETRIES) {
569
+ decision = {
570
+ ...decision,
571
+ action: 'error',
572
+ next_status: 'failed',
573
+ contract_violation: contractViolation,
574
+ reasoning: `${marker} ${cleanReason} (retry_limit ${consecutiveContractViolations}/${MAX_CONTRACT_VIOLATION_RETRIES})`,
575
+ metadata: {
576
+ ...decision.metadata,
577
+ confidence: 'low',
578
+ exit_clean: false,
579
+ },
580
+ };
581
+ }
582
+ else {
583
+ decision = {
584
+ ...decision,
585
+ action: 'retry',
586
+ next_status: 'in_progress',
587
+ contract_violation: contractViolation,
588
+ reasoning: `${marker} ${cleanReason} (retry ${consecutiveContractViolations}/${MAX_CONTRACT_VIOLATION_RETRIES})`,
589
+ metadata: {
590
+ ...decision.metadata,
591
+ confidence: 'medium',
592
+ },
593
+ };
594
+ }
595
+ }
596
+ // Enforce orchestrator authority over weak/unsupported WONT_FIX claims.
597
+ const structuredOverrideItems = Array.isArray(decision.wont_fix_override_items)
598
+ ? decision.wont_fix_override_items.filter(item => typeof item === 'string' && item.trim().length > 0)
599
+ : [];
600
+ const legacyWontFixOverrideMatch = (decision.reasoning || '').match(/WONT_FIX_OVERRIDE:\s*([\s\S]+)/i);
601
+ const fallbackLegacyItems = legacyWontFixOverrideMatch?.[1]
602
+ ? legacyWontFixOverrideMatch[1]
603
+ .split('\n')
604
+ .map(line => line.trim())
605
+ .filter(Boolean)
606
+ : [];
607
+ const overrideItems = structuredOverrideItems.length > 0 ? structuredOverrideItems : fallbackLegacyItems;
608
+ if (overrideItems.length > 0) {
609
+ const mandatoryLines = overrideItems.map((item, idx) => `${idx + 1}. ${item}`);
610
+ const mandatoryGuidance = `MUST_IMPLEMENT:
611
+ ${mandatoryLines.join('\n')}
612
+
613
+ This is a mandatory orchestrator override. Implement these changes before resubmitting.
614
+ Only use WONT_FIX if you provide exceptional technical evidence and the orchestrator explicitly accepts it.`;
615
+ const persistedNote = `${MUST_IMPLEMENT_MARKER}[rc=${task.rejection_count}] ${mandatoryGuidance}`;
616
+ (0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'coordinator', {
617
+ actorType: 'orchestrator',
618
+ notes: persistedNote,
619
+ });
620
+ coordinatorCache?.set(task.id, {
621
+ success: true,
622
+ decision: 'guide_coder',
623
+ guidance: mandatoryGuidance,
624
+ });
625
+ decision = {
626
+ ...decision,
627
+ action: 'retry',
628
+ next_status: 'in_progress',
629
+ reasoning: `${MUST_IMPLEMENT_MARKER} WONT_FIX override applied`,
630
+ metadata: {
631
+ ...decision.metadata,
632
+ confidence: 'medium',
633
+ },
634
+ };
635
+ }
394
636
  // STEP 6: Log orchestrator decision for audit trail
395
637
  (0, queries_js_1.addAuditEntry)(db, task.id, task.status, decision.next_status, 'orchestrator', {
396
638
  actorType: 'orchestrator',
@@ -399,14 +641,76 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
399
641
  // STEP 7: Execute the decision
400
642
  switch (decision.action) {
401
643
  case 'submit':
402
- (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', decision.reasoning);
644
+ {
645
+ const submissionCommitSha = resolveCoderSubmittedCommitSha(projectPath, coderResult.stdout, {
646
+ requireExplicitToken: requiresExplicitSubmissionCommit,
647
+ });
648
+ if (!submissionCommitSha && requiresExplicitSubmissionCommit) {
649
+ (0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'orchestrator', {
650
+ actorType: 'orchestrator',
651
+ notes: '[retry] Awaiting explicit SUBMISSION_COMMIT token for commit recovery',
652
+ });
653
+ if (!jsonMode) {
654
+ console.log('\n⟳ Waiting for explicit SUBMISSION_COMMIT token from coder');
655
+ }
656
+ break;
657
+ }
658
+ if (!submissionCommitSha || !(0, status_js_1.isCommitReachable)(projectPath, submissionCommitSha)) {
659
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: cannot submit to review without a valid commit hash');
660
+ if (!jsonMode) {
661
+ console.log('\n✗ Task failed (submission commit missing or not in workspace)');
662
+ }
663
+ break;
664
+ }
665
+ if (leaseFence?.parallelSessionId) {
666
+ const pushResult = (0, push_js_1.pushToRemote)(projectPath, 'origin', branchName);
667
+ if (!pushResult.success) {
668
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', `Task failed: cannot publish submission commit ${submissionCommitSha} to ${branchName} before review`);
669
+ if (!jsonMode) {
670
+ console.log('\n✗ Task failed (unable to push submission commit to branch for review)');
671
+ }
672
+ break;
673
+ }
674
+ }
675
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', decision.reasoning, submissionCommitSha);
676
+ }
403
677
  if (!jsonMode) {
404
678
  console.log(`\n✓ Coder complete, submitted to review (confidence: ${decision.metadata.confidence})`);
405
679
  }
406
680
  break;
407
681
  case 'stage_commit_submit':
408
682
  if (!has_uncommitted) {
409
- (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', 'Auto-commit skipped: no uncommitted changes');
683
+ const submissionCommitSha = resolveCoderSubmittedCommitSha(projectPath, coderResult.stdout, {
684
+ requireExplicitToken: requiresExplicitSubmissionCommit,
685
+ });
686
+ if (!submissionCommitSha && requiresExplicitSubmissionCommit) {
687
+ (0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'orchestrator', {
688
+ actorType: 'orchestrator',
689
+ notes: '[retry] Awaiting explicit SUBMISSION_COMMIT token for commit recovery',
690
+ });
691
+ if (!jsonMode) {
692
+ console.log('\n⟳ Waiting for explicit SUBMISSION_COMMIT token from coder');
693
+ }
694
+ break;
695
+ }
696
+ if (!submissionCommitSha || !(0, status_js_1.isCommitReachable)(projectPath, submissionCommitSha)) {
697
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: cannot submit to review without a valid commit hash');
698
+ if (!jsonMode) {
699
+ console.log('\n✗ Task failed (submission commit missing or not in workspace)');
700
+ }
701
+ break;
702
+ }
703
+ if (leaseFence?.parallelSessionId) {
704
+ const pushResult = (0, push_js_1.pushToRemote)(projectPath, 'origin', branchName);
705
+ if (!pushResult.success) {
706
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', `Task failed: cannot publish submission commit ${submissionCommitSha} to ${branchName} before review`);
707
+ if (!jsonMode) {
708
+ console.log('\n✗ Task failed (unable to push submission commit to branch for review)');
709
+ }
710
+ break;
711
+ }
712
+ }
713
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', 'Auto-commit skipped: no uncommitted changes', submissionCommitSha);
410
714
  if (!jsonMode) {
411
715
  console.log('\n✓ Auto-commit skipped (no uncommitted files) and submitted to review');
412
716
  }
@@ -421,7 +725,25 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
421
725
  cwd: projectPath,
422
726
  stdio: 'pipe'
423
727
  });
424
- (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', `Auto-committed and submitted (${decision.reasoning})`);
728
+ const submissionCommitSha = (0, status_js_1.getCurrentCommitSha)(projectPath) || undefined;
729
+ if (!submissionCommitSha || !(0, status_js_1.isCommitReachable)(projectPath, submissionCommitSha)) {
730
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: auto-committed but commit hash is not in current workspace');
731
+ if (!jsonMode) {
732
+ console.log('\n✗ Task failed (auto-commit hash not in workspace)');
733
+ }
734
+ break;
735
+ }
736
+ if (leaseFence?.parallelSessionId) {
737
+ const pushResult = (0, push_js_1.pushToRemote)(projectPath, 'origin', branchName);
738
+ if (!pushResult.success) {
739
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', `Task failed: cannot publish submission commit ${submissionCommitSha} to ${branchName} before review`);
740
+ if (!jsonMode) {
741
+ console.log('\n✗ Task failed (unable to push submission commit to branch for review)');
742
+ }
743
+ break;
744
+ }
745
+ }
746
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', `Auto-committed and submitted (${decision.reasoning})`, submissionCommitSha);
425
747
  if (!jsonMode) {
426
748
  console.log(`\n✓ Auto-committed and submitted to review (confidence: ${decision.metadata.confidence})`);
427
749
  }
@@ -455,6 +777,17 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
455
777
  const config = (0, loader_js_1.loadConfig)(projectPath);
456
778
  const multiReviewEnabled = (0, reviewer_js_1.isMultiReviewEnabled)(config);
457
779
  const strict = config.ai?.review?.strict ?? true;
780
+ const submissionResolution = (0, submission_resolution_js_1.resolveSubmissionCommitWithRecovery)(projectPath, (0, queries_js_1.getSubmissionCommitShas)(db, task.id));
781
+ if (submissionResolution.status !== 'resolved') {
782
+ const attemptsText = submissionResolution.attempts.join(' | ') || 'none';
783
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'in_progress', 'orchestrator', `[commit_recovery] Missing reachable submission hash (${submissionResolution.reason}; attempts: ${attemptsText}). ` +
784
+ `Treating task as resubmission. Coder must output exact line: SUBMISSION_COMMIT: <sha> for the commit that implements the task.`);
785
+ if (!jsonMode) {
786
+ console.log('\n⟳ Reviewer hash missing; returning task to coder for hash resubmission');
787
+ }
788
+ return;
789
+ }
790
+ const submissionCommitSha = submissionResolution.sha;
458
791
  let reviewerResult;
459
792
  let reviewerResults = [];
460
793
  // STEP 1: Invoke reviewer(s)
@@ -692,13 +1025,17 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
692
1025
  }
693
1026
  catch (error) {
694
1027
  console.error('Orchestrator invocation failed:', error);
1028
+ const orchestratorFailure = await classifyOrchestratorFailure(error, projectPath);
695
1029
  const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
696
1030
  const reviewerStdout = reviewerResult?.stdout ?? '';
697
1031
  const explicitDecision = handler.extractExplicitReviewerDecision(reviewerStdout);
1032
+ const failureReason = orchestratorFailure
1033
+ ? `${orchestratorFailure.type}: ${orchestratorFailure.message}`
1034
+ : summarizeErrorMessage(error);
698
1035
  if (explicitDecision) {
699
1036
  decision = {
700
1037
  decision: explicitDecision,
701
- reasoning: `FALLBACK: Orchestrator failed but reviewer explicitly signaled ${explicitDecision.toUpperCase()}`,
1038
+ reasoning: `FALLBACK: Orchestrator failed (${failureReason}) but reviewer explicitly signaled ${explicitDecision.toUpperCase()}`,
702
1039
  notes: reviewerStdout,
703
1040
  next_status: explicitDecision === 'approve' ? 'completed' :
704
1041
  explicitDecision === 'reject' ? 'in_progress' :
@@ -715,7 +1052,7 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
715
1052
  else {
716
1053
  decision = {
717
1054
  decision: 'unclear',
718
- reasoning: 'FALLBACK: Orchestrator failed, retrying review',
1055
+ reasoning: `FALLBACK: Orchestrator failed (${failureReason}), retrying review`,
719
1056
  notes: 'Review unclear, retrying',
720
1057
  next_status: 'review',
721
1058
  metadata: {
@@ -776,7 +1113,7 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
776
1113
  description: followUp.description,
777
1114
  sectionId: task.section_id,
778
1115
  referenceTaskId: task.id,
779
- referenceCommit: commit_sha || undefined,
1116
+ referenceCommit: submissionCommitSha,
780
1117
  requiresPromotion,
781
1118
  depth: nextDepth,
782
1119
  });
@@ -795,7 +1132,7 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
795
1132
  }
796
1133
  }
797
1134
  // STEP 6: Execute the decision
798
- const commitSha = commit_sha || undefined;
1135
+ const commitSha = submissionCommitSha;
799
1136
  switch (decision.decision) {
800
1137
  case 'approve':
801
1138
  (0, queries_js_1.approveTask)(db, task.id, 'orchestrator', decision.notes, commitSha);