steroids-cli 0.9.29 → 0.9.31

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 (39) hide show
  1. package/dist/commands/loop-phases.d.ts.map +1 -1
  2. package/dist/commands/loop-phases.js +321 -66
  3. package/dist/commands/loop-phases.js.map +1 -1
  4. package/dist/database/queries.d.ts +16 -0
  5. package/dist/database/queries.d.ts.map +1 -1
  6. package/dist/database/queries.js +47 -0
  7. package/dist/database/queries.js.map +1 -1
  8. package/dist/orchestrator/coordinator.d.ts +2 -0
  9. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  10. package/dist/orchestrator/coordinator.js +17 -1
  11. package/dist/orchestrator/coordinator.js.map +1 -1
  12. package/dist/orchestrator/post-coder.d.ts.map +1 -1
  13. package/dist/orchestrator/post-coder.js +82 -5
  14. package/dist/orchestrator/post-coder.js.map +1 -1
  15. package/dist/orchestrator/reviewer.d.ts.map +1 -1
  16. package/dist/orchestrator/reviewer.js +56 -41
  17. package/dist/orchestrator/reviewer.js.map +1 -1
  18. package/dist/orchestrator/schemas.d.ts.map +1 -1
  19. package/dist/orchestrator/schemas.js +12 -0
  20. package/dist/orchestrator/schemas.js.map +1 -1
  21. package/dist/orchestrator/types.d.ts +3 -0
  22. package/dist/orchestrator/types.d.ts.map +1 -1
  23. package/dist/prompts/coder.d.ts.map +1 -1
  24. package/dist/prompts/coder.js +65 -2
  25. package/dist/prompts/coder.js.map +1 -1
  26. package/dist/prompts/prompt-helpers.d.ts.map +1 -1
  27. package/dist/prompts/prompt-helpers.js +15 -4
  28. package/dist/prompts/prompt-helpers.js.map +1 -1
  29. package/dist/prompts/reviewer.d.ts +2 -3
  30. package/dist/prompts/reviewer.d.ts.map +1 -1
  31. package/dist/prompts/reviewer.js +9 -5
  32. package/dist/prompts/reviewer.js.map +1 -1
  33. package/dist/runners/global-db.d.ts.map +1 -1
  34. package/dist/runners/global-db.js +7 -3
  35. package/dist/runners/global-db.js.map +1 -1
  36. package/dist/runners/wakeup.d.ts.map +1 -1
  37. package/dist/runners/wakeup.js +142 -2
  38. package/dist/runners/wakeup.js.map +1 -1
  39. package/package.json +1 -1
@@ -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;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;AAsRD,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,CAwfxC;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,CAkcxC"}
@@ -136,9 +136,58 @@ async function checkNonRetryableProviderFailure(result, role, projectPath, revie
136
136
  }
137
137
  return null;
138
138
  }
139
+ function summarizeErrorMessage(error) {
140
+ const raw = error instanceof Error ? error.message : String(error);
141
+ return raw.replace(/\s+/g, ' ').trim().slice(0, 220);
142
+ }
143
+ /**
144
+ * Classify orchestrator invocation failures so we can distinguish transient parse
145
+ * noise from hard provider/config failures (auth/model/availability).
146
+ */
147
+ async function classifyOrchestratorFailure(error, projectPath) {
148
+ const message = summarizeErrorMessage(error);
149
+ const lower = message.toLowerCase();
150
+ if (lower.includes('orchestrator ai provider not configured')) {
151
+ return { type: 'orchestrator_unconfigured', message, retryable: false };
152
+ }
153
+ if (lower.includes("provider '") && lower.includes('is not available')) {
154
+ return { type: 'provider_unavailable', message, retryable: false };
155
+ }
156
+ const config = (0, loader_js_1.loadConfig)(projectPath);
157
+ const providerName = config.ai?.orchestrator?.provider;
158
+ if (!providerName) {
159
+ return { type: 'orchestrator_unconfigured', message, retryable: false };
160
+ }
161
+ const registry = await (0, registry_js_1.getProviderRegistry)();
162
+ const provider = registry.tryGet(providerName);
163
+ if (!provider) {
164
+ return { type: 'provider_unavailable', message, retryable: false };
165
+ }
166
+ const syntheticFailure = {
167
+ success: false,
168
+ exitCode: 1,
169
+ stdout: '',
170
+ stderr: message,
171
+ duration: 0,
172
+ timedOut: false,
173
+ };
174
+ const classified = provider.classifyResult(syntheticFailure);
175
+ if (!classified) {
176
+ return null;
177
+ }
178
+ return {
179
+ type: classified.type,
180
+ message: classified.message || message,
181
+ retryable: classified.retryable,
182
+ };
183
+ }
139
184
  const MAX_ORCHESTRATOR_PARSE_RETRIES = 3;
185
+ const MAX_CONTRACT_VIOLATION_RETRIES = 3;
140
186
  const CODER_PARSE_FALLBACK_MARKER = '[retry] FALLBACK: Orchestrator failed, defaulting to retry';
141
187
  const REVIEWER_PARSE_FALLBACK_MARKER = '[unclear] FALLBACK: Orchestrator failed, retrying review';
188
+ const CONTRACT_CHECKLIST_MARKER = '[contract:checklist]';
189
+ const CONTRACT_REJECTION_RESPONSE_MARKER = '[contract:rejection_response]';
190
+ const MUST_IMPLEMENT_MARKER = '[must_implement]';
142
191
  function countConsecutiveOrchestratorFallbackEntries(db, taskId, marker) {
143
192
  const audit = (0, queries_js_1.getTaskAudit)(db, taskId);
144
193
  let count = 0;
@@ -173,6 +222,34 @@ function countConsecutiveUnclearEntries(db, taskId) {
173
222
  }
174
223
  return count;
175
224
  }
225
+ function countConsecutiveTaggedOrchestratorEntries(db, taskId, requiredTag, tagFamilyPrefix) {
226
+ const audit = (0, queries_js_1.getTaskAudit)(db, taskId);
227
+ let count = 0;
228
+ for (let i = audit.length - 1; i >= 0; i--) {
229
+ const entry = audit[i];
230
+ if (entry.actor !== 'orchestrator') {
231
+ continue; // tolerate non-orchestrator audit noise
232
+ }
233
+ const notes = entry.notes ?? '';
234
+ if (notes.includes(requiredTag)) {
235
+ count += 1;
236
+ continue;
237
+ }
238
+ if (tagFamilyPrefix && notes.includes(tagFamilyPrefix)) {
239
+ break; // same family, different category = sequence ended
240
+ }
241
+ break;
242
+ }
243
+ return count;
244
+ }
245
+ function countLatestOpenRejectionItems(notes) {
246
+ if (!notes)
247
+ return 0;
248
+ return notes
249
+ .split('\n')
250
+ .filter(line => /^\s*[-*]\s*\[\s\]\s+/.test(line))
251
+ .length;
252
+ }
176
253
  function hasCoderCompletionSignal(output) {
177
254
  const lower = output.toLowerCase();
178
255
  if (/\b(?:not|no)\s+(?:task\s+)?(?:is\s+)?(?:complete|complete[d]?|finished|done|ready)\b/.test(lower)) {
@@ -186,51 +263,88 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
186
263
  refreshParallelWorkstreamLease(projectPath, leaseFence);
187
264
  let coordinatorGuidance;
188
265
  const thresholds = coordinatorThresholds || [2, 5, 9];
266
+ const persistedMustImplement = (0, queries_js_1.getLatestMustImplementGuidance)(db, task.id);
267
+ const activeMustImplement = persistedMustImplement &&
268
+ task.status === 'in_progress' &&
269
+ task.rejection_count >= persistedMustImplement.rejection_count_watermark
270
+ ? persistedMustImplement
271
+ : null;
189
272
  // Run coordinator at rejection thresholds (same as before)
190
273
  const shouldInvokeCoordinator = thresholds.includes(task.rejection_count);
191
274
  const cachedResult = coordinatorCache?.get(task.id);
192
- if (shouldInvokeCoordinator) {
275
+ if (activeMustImplement) {
276
+ coordinatorGuidance = activeMustImplement.guidance;
277
+ coordinatorCache?.set(task.id, {
278
+ success: true,
279
+ decision: 'guide_coder',
280
+ guidance: activeMustImplement.guidance,
281
+ });
193
282
  if (!jsonMode) {
194
- console.log(`\n>>> Task has ${task.rejection_count} rejections (threshold hit) - invoking COORDINATOR...\n`);
283
+ console.log(`\nActive MUST_IMPLEMENT override detected (rc=${activeMustImplement.rejection_count_watermark})`);
195
284
  }
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
- }
285
+ }
286
+ if (shouldInvokeCoordinator) {
287
+ if (activeMustImplement &&
288
+ task.rejection_count === activeMustImplement.rejection_count_watermark) {
289
+ if (!jsonMode) {
290
+ console.log('\nSkipping coordinator reinvocation in same rejection cycle due to active MUST_IMPLEMENT override');
225
291
  }
226
292
  }
227
- catch (error) {
293
+ else {
228
294
  if (!jsonMode) {
229
- console.warn('Coordinator invocation failed, continuing without guidance:', error);
295
+ console.log(`\n>>> Task has ${task.rejection_count} rejections (threshold hit) - invoking COORDINATOR...\n`);
296
+ }
297
+ try {
298
+ const rejectionHistory = (0, queries_js_1.getTaskRejections)(db, task.id);
299
+ const coordExtra = {};
300
+ if (task.section_id) {
301
+ const allSectionTasks = (0, queries_js_1.listTasks)(db, { sectionId: task.section_id });
302
+ coordExtra.sectionTasks = allSectionTasks.map(t => ({
303
+ id: t.id, title: t.title, status: t.status,
304
+ }));
305
+ }
306
+ coordExtra.submissionNotes = (0, queries_js_1.getLatestSubmissionNotes)(db, task.id);
307
+ const modified = (0, status_js_1.getModifiedFiles)(projectPath);
308
+ if (modified.length > 0) {
309
+ coordExtra.gitDiffSummary = modified.join('\n');
310
+ }
311
+ if (cachedResult) {
312
+ coordExtra.previousGuidance = cachedResult.guidance;
313
+ }
314
+ if (activeMustImplement) {
315
+ coordExtra.lockedMustImplementGuidance = activeMustImplement.guidance;
316
+ coordExtra.lockedMustImplementWatermark = activeMustImplement.rejection_count_watermark;
317
+ }
318
+ const coordResult = await (0, coordinator_js_1.invokeCoordinator)(task, rejectionHistory, projectPath, coordExtra);
319
+ if (coordResult) {
320
+ const mustKeepOverride = activeMustImplement &&
321
+ task.rejection_count > activeMustImplement.rejection_count_watermark;
322
+ const normalizedGuidance = mustKeepOverride && !coordResult.guidance.includes('MUST_IMPLEMENT:')
323
+ ? `${activeMustImplement.guidance}\n\nAdditional coordinator guidance:\n${coordResult.guidance}`
324
+ : coordResult.guidance;
325
+ coordinatorGuidance = normalizedGuidance;
326
+ coordinatorCache?.set(task.id, {
327
+ ...coordResult,
328
+ guidance: normalizedGuidance,
329
+ });
330
+ (0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'coordinator', {
331
+ actorType: 'orchestrator',
332
+ notes: `[${coordResult.decision}] ${normalizedGuidance}`,
333
+ });
334
+ if (!jsonMode) {
335
+ console.log(`\nCoordinator decision: ${coordResult.decision}`);
336
+ console.log('Coordinator guidance stored for both coder and reviewer.');
337
+ }
338
+ }
339
+ }
340
+ catch (error) {
341
+ if (!jsonMode) {
342
+ console.warn('Coordinator invocation failed, continuing without guidance:', error);
343
+ }
230
344
  }
231
345
  }
232
346
  }
233
- else if (cachedResult) {
347
+ else if (cachedResult && !activeMustImplement) {
234
348
  coordinatorGuidance = cachedResult.guidance;
235
349
  if (!jsonMode && task.rejection_count >= 2) {
236
350
  console.log(`\nReusing cached coordinator guidance (decision: ${cachedResult.decision})`);
@@ -282,6 +396,7 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
282
396
  const lastRejectionNotes = task.rejection_count > 0
283
397
  ? (0, queries_js_1.getTaskRejections)(db, task.id).slice(-1)[0]?.notes ?? undefined
284
398
  : undefined;
399
+ const rejectionItemCount = countLatestOpenRejectionItems(lastRejectionNotes);
285
400
  const context = {
286
401
  task: {
287
402
  id: task.id,
@@ -289,6 +404,7 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
289
404
  description: task.title, // Use title as description for now
290
405
  rejection_notes: lastRejectionNotes,
291
406
  rejection_count: task.rejection_count,
407
+ rejection_item_count: rejectionItemCount,
292
408
  },
293
409
  coder_output: {
294
410
  stdout: coderResult.stdout,
@@ -306,40 +422,57 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
306
422
  }
307
423
  catch (error) {
308
424
  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) {
425
+ const orchestratorFailure = await classifyOrchestratorFailure(error, projectPath);
426
+ if (orchestratorFailure && !orchestratorFailure.retryable) {
315
427
  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',
428
+ action: 'error',
429
+ reasoning: `FALLBACK: Non-retryable orchestrator failure (${orchestratorFailure.type})`,
333
430
  commits: [],
334
- next_status: 'in_progress',
431
+ next_status: 'failed',
335
432
  metadata: {
336
433
  files_changed: 0,
337
434
  confidence: 'low',
338
- exit_clean: true,
435
+ exit_clean: false,
339
436
  has_commits: false,
340
437
  }
341
438
  });
342
439
  }
440
+ else {
441
+ // Check if coder seems finished even if orchestrator failed
442
+ const isTaskComplete = hasCoderCompletionSignal(coderResult.stdout);
443
+ // Only count as having work if there are actual relevant uncommitted changes,
444
+ // changed files, or recent commits
445
+ const hasWork = hasRelevantChanges;
446
+ if (isTaskComplete && hasWork) {
447
+ orchestratorOutput = JSON.stringify({
448
+ action: has_uncommitted ? 'stage_commit_submit' : 'submit',
449
+ reasoning: 'FALLBACK: Orchestrator failed but coder signaled completion',
450
+ commits: commits.map(c => c.sha),
451
+ next_status: 'review',
452
+ metadata: {
453
+ files_changed: files_changed.length,
454
+ confidence: 'low',
455
+ exit_clean: true,
456
+ has_commits: commits.length > 0,
457
+ }
458
+ });
459
+ }
460
+ else {
461
+ // Fallback to safe default: retry
462
+ orchestratorOutput = JSON.stringify({
463
+ action: 'retry',
464
+ reasoning: 'FALLBACK: Orchestrator failed, defaulting to retry',
465
+ commits: [],
466
+ next_status: 'in_progress',
467
+ metadata: {
468
+ files_changed: 0,
469
+ confidence: 'low',
470
+ exit_clean: true,
471
+ has_commits: false,
472
+ }
473
+ });
474
+ }
475
+ }
343
476
  }
344
477
  // STEP 5: Parse orchestrator output with fallback
345
478
  const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
@@ -391,6 +524,90 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
391
524
  }
392
525
  }
393
526
  }
527
+ // Contract-violation handling (structured-first, legacy-prefix fallback).
528
+ const legacyChecklistViolation = /^CHECKLIST_REQUIRED:/i.test(decision.reasoning || '');
529
+ const legacyRejectionResponseViolation = /^REJECTION_RESPONSE_REQUIRED:/i.test(decision.reasoning || '');
530
+ const contractViolation = decision.contract_violation
531
+ ?? (legacyChecklistViolation ? 'checklist_required' : null)
532
+ ?? (legacyRejectionResponseViolation ? 'rejection_response_required' : null);
533
+ if (contractViolation) {
534
+ const marker = contractViolation === 'checklist_required'
535
+ ? CONTRACT_CHECKLIST_MARKER
536
+ : CONTRACT_REJECTION_RESPONSE_MARKER;
537
+ const reasonText = (decision.reasoning || '')
538
+ .replace(/^CHECKLIST_REQUIRED:\s*/i, '')
539
+ .replace(/^REJECTION_RESPONSE_REQUIRED:\s*/i, '')
540
+ .trim();
541
+ const cleanReason = reasonText || 'Required output contract not satisfied';
542
+ const consecutiveContractViolations = countConsecutiveTaggedOrchestratorEntries(db, task.id, marker, '[contract:') + 1;
543
+ if (consecutiveContractViolations >= MAX_CONTRACT_VIOLATION_RETRIES) {
544
+ decision = {
545
+ ...decision,
546
+ action: 'error',
547
+ next_status: 'failed',
548
+ contract_violation: contractViolation,
549
+ reasoning: `${marker} ${cleanReason} (retry_limit ${consecutiveContractViolations}/${MAX_CONTRACT_VIOLATION_RETRIES})`,
550
+ metadata: {
551
+ ...decision.metadata,
552
+ confidence: 'low',
553
+ exit_clean: false,
554
+ },
555
+ };
556
+ }
557
+ else {
558
+ decision = {
559
+ ...decision,
560
+ action: 'retry',
561
+ next_status: 'in_progress',
562
+ contract_violation: contractViolation,
563
+ reasoning: `${marker} ${cleanReason} (retry ${consecutiveContractViolations}/${MAX_CONTRACT_VIOLATION_RETRIES})`,
564
+ metadata: {
565
+ ...decision.metadata,
566
+ confidence: 'medium',
567
+ },
568
+ };
569
+ }
570
+ }
571
+ // Enforce orchestrator authority over weak/unsupported WONT_FIX claims.
572
+ const structuredOverrideItems = Array.isArray(decision.wont_fix_override_items)
573
+ ? decision.wont_fix_override_items.filter(item => typeof item === 'string' && item.trim().length > 0)
574
+ : [];
575
+ const legacyWontFixOverrideMatch = (decision.reasoning || '').match(/WONT_FIX_OVERRIDE:\s*([\s\S]+)/i);
576
+ const fallbackLegacyItems = legacyWontFixOverrideMatch?.[1]
577
+ ? legacyWontFixOverrideMatch[1]
578
+ .split('\n')
579
+ .map(line => line.trim())
580
+ .filter(Boolean)
581
+ : [];
582
+ const overrideItems = structuredOverrideItems.length > 0 ? structuredOverrideItems : fallbackLegacyItems;
583
+ if (overrideItems.length > 0) {
584
+ const mandatoryLines = overrideItems.map((item, idx) => `${idx + 1}. ${item}`);
585
+ const mandatoryGuidance = `MUST_IMPLEMENT:
586
+ ${mandatoryLines.join('\n')}
587
+
588
+ This is a mandatory orchestrator override. Implement these changes before resubmitting.
589
+ Only use WONT_FIX if you provide exceptional technical evidence and the orchestrator explicitly accepts it.`;
590
+ const persistedNote = `${MUST_IMPLEMENT_MARKER}[rc=${task.rejection_count}] ${mandatoryGuidance}`;
591
+ (0, queries_js_1.addAuditEntry)(db, task.id, task.status, task.status, 'coordinator', {
592
+ actorType: 'orchestrator',
593
+ notes: persistedNote,
594
+ });
595
+ coordinatorCache?.set(task.id, {
596
+ success: true,
597
+ decision: 'guide_coder',
598
+ guidance: mandatoryGuidance,
599
+ });
600
+ decision = {
601
+ ...decision,
602
+ action: 'retry',
603
+ next_status: 'in_progress',
604
+ reasoning: `${MUST_IMPLEMENT_MARKER} WONT_FIX override applied`,
605
+ metadata: {
606
+ ...decision.metadata,
607
+ confidence: 'medium',
608
+ },
609
+ };
610
+ }
394
611
  // STEP 6: Log orchestrator decision for audit trail
395
612
  (0, queries_js_1.addAuditEntry)(db, task.id, task.status, decision.next_status, 'orchestrator', {
396
613
  actorType: 'orchestrator',
@@ -399,14 +616,32 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
399
616
  // STEP 7: Execute the decision
400
617
  switch (decision.action) {
401
618
  case 'submit':
402
- (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', decision.reasoning);
619
+ {
620
+ const submissionCommitSha = (0, status_js_1.getCurrentCommitSha)(projectPath) || undefined;
621
+ if (!submissionCommitSha) {
622
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: cannot submit to review without a commit hash');
623
+ if (!jsonMode) {
624
+ console.log('\n✗ Task failed (missing commit hash on submit)');
625
+ }
626
+ break;
627
+ }
628
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', decision.reasoning, submissionCommitSha);
629
+ }
403
630
  if (!jsonMode) {
404
631
  console.log(`\n✓ Coder complete, submitted to review (confidence: ${decision.metadata.confidence})`);
405
632
  }
406
633
  break;
407
634
  case 'stage_commit_submit':
408
635
  if (!has_uncommitted) {
409
- (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', 'Auto-commit skipped: no uncommitted changes');
636
+ const submissionCommitSha = (0, status_js_1.getCurrentCommitSha)(projectPath) || undefined;
637
+ if (!submissionCommitSha) {
638
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: cannot submit to review without a commit hash');
639
+ if (!jsonMode) {
640
+ console.log('\n✗ Task failed (missing commit hash on auto-commit skip path)');
641
+ }
642
+ break;
643
+ }
644
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', 'Auto-commit skipped: no uncommitted changes', submissionCommitSha);
410
645
  if (!jsonMode) {
411
646
  console.log('\n✓ Auto-commit skipped (no uncommitted files) and submitted to review');
412
647
  }
@@ -421,7 +656,15 @@ async function runCoderPhase(db, task, projectPath, action, jsonMode = false, co
421
656
  cwd: projectPath,
422
657
  stdio: 'pipe'
423
658
  });
424
- (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', `Auto-committed and submitted (${decision.reasoning})`);
659
+ const submissionCommitSha = (0, status_js_1.getCurrentCommitSha)(projectPath) || undefined;
660
+ if (!submissionCommitSha) {
661
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: auto-commit succeeded but commit hash could not be resolved');
662
+ if (!jsonMode) {
663
+ console.log('\n✗ Task failed (auto-commit hash could not be resolved)');
664
+ }
665
+ break;
666
+ }
667
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'review', 'orchestrator', `Auto-committed and submitted (${decision.reasoning})`, submissionCommitSha);
425
668
  if (!jsonMode) {
426
669
  console.log(`\n✓ Auto-committed and submitted to review (confidence: ${decision.metadata.confidence})`);
427
670
  }
@@ -455,6 +698,14 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
455
698
  const config = (0, loader_js_1.loadConfig)(projectPath);
456
699
  const multiReviewEnabled = (0, reviewer_js_1.isMultiReviewEnabled)(config);
457
700
  const strict = config.ai?.review?.strict ?? true;
701
+ const submissionCommitSha = (0, queries_js_1.getLatestSubmissionCommitSha)(db, task.id);
702
+ if (!submissionCommitSha) {
703
+ (0, queries_js_1.updateTaskStatus)(db, task.id, 'failed', 'orchestrator', 'Task failed: submission commit hash missing for review phase');
704
+ if (!jsonMode) {
705
+ console.log('\n✗ Task failed (missing submission commit hash before review)');
706
+ }
707
+ return;
708
+ }
458
709
  let reviewerResult;
459
710
  let reviewerResults = [];
460
711
  // STEP 1: Invoke reviewer(s)
@@ -692,13 +943,17 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
692
943
  }
693
944
  catch (error) {
694
945
  console.error('Orchestrator invocation failed:', error);
946
+ const orchestratorFailure = await classifyOrchestratorFailure(error, projectPath);
695
947
  const handler = new fallback_handler_js_1.OrchestrationFallbackHandler();
696
948
  const reviewerStdout = reviewerResult?.stdout ?? '';
697
949
  const explicitDecision = handler.extractExplicitReviewerDecision(reviewerStdout);
950
+ const failureReason = orchestratorFailure
951
+ ? `${orchestratorFailure.type}: ${orchestratorFailure.message}`
952
+ : summarizeErrorMessage(error);
698
953
  if (explicitDecision) {
699
954
  decision = {
700
955
  decision: explicitDecision,
701
- reasoning: `FALLBACK: Orchestrator failed but reviewer explicitly signaled ${explicitDecision.toUpperCase()}`,
956
+ reasoning: `FALLBACK: Orchestrator failed (${failureReason}) but reviewer explicitly signaled ${explicitDecision.toUpperCase()}`,
702
957
  notes: reviewerStdout,
703
958
  next_status: explicitDecision === 'approve' ? 'completed' :
704
959
  explicitDecision === 'reject' ? 'in_progress' :
@@ -715,7 +970,7 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
715
970
  else {
716
971
  decision = {
717
972
  decision: 'unclear',
718
- reasoning: 'FALLBACK: Orchestrator failed, retrying review',
973
+ reasoning: `FALLBACK: Orchestrator failed (${failureReason}), retrying review`,
719
974
  notes: 'Review unclear, retrying',
720
975
  next_status: 'review',
721
976
  metadata: {
@@ -776,7 +1031,7 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
776
1031
  description: followUp.description,
777
1032
  sectionId: task.section_id,
778
1033
  referenceTaskId: task.id,
779
- referenceCommit: commit_sha || undefined,
1034
+ referenceCommit: submissionCommitSha,
780
1035
  requiresPromotion,
781
1036
  depth: nextDepth,
782
1037
  });
@@ -795,7 +1050,7 @@ async function runReviewerPhase(db, task, projectPath, jsonMode = false, coordin
795
1050
  }
796
1051
  }
797
1052
  // STEP 6: Execute the decision
798
- const commitSha = commit_sha || undefined;
1053
+ const commitSha = submissionCommitSha;
799
1054
  switch (decision.decision) {
800
1055
  case 'approve':
801
1056
  (0, queries_js_1.approveTask)(db, task.id, 'orchestrator', decision.notes, commitSha);