karajan-code 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +5 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +174 -93
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/intent-guard.js +123 -0
  18. package/src/guards/output-guard.js +158 -0
  19. package/src/guards/perf-guard.js +126 -0
  20. package/src/guards/policy-resolver.js +3 -3
  21. package/src/mcp/orphan-guard.js +1 -2
  22. package/src/mcp/progress.js +4 -3
  23. package/src/mcp/run-kj.js +1 -0
  24. package/src/mcp/server-handlers.js +242 -253
  25. package/src/mcp/server.js +4 -3
  26. package/src/mcp/tools.js +2 -0
  27. package/src/orchestrator/agent-fallback.js +1 -3
  28. package/src/orchestrator/iteration-stages.js +206 -170
  29. package/src/orchestrator/pre-loop-stages.js +200 -34
  30. package/src/orchestrator/solomon-rules.js +2 -2
  31. package/src/orchestrator.js +902 -746
  32. package/src/planning-game/adapter.js +23 -20
  33. package/src/planning-game/architect-adrs.js +45 -0
  34. package/src/planning-game/client.js +15 -1
  35. package/src/planning-game/decomposition.js +7 -5
  36. package/src/prompts/architect.js +88 -0
  37. package/src/prompts/discover.js +54 -53
  38. package/src/prompts/planner.js +53 -33
  39. package/src/prompts/triage.js +8 -16
  40. package/src/review/parser.js +18 -19
  41. package/src/review/profiles.js +2 -2
  42. package/src/review/schema.js +3 -3
  43. package/src/review/scope-filter.js +3 -4
  44. package/src/roles/architect-role.js +122 -0
  45. package/src/roles/commiter-role.js +2 -2
  46. package/src/roles/discover-role.js +59 -67
  47. package/src/roles/index.js +1 -0
  48. package/src/roles/planner-role.js +54 -38
  49. package/src/roles/refactorer-role.js +8 -7
  50. package/src/roles/researcher-role.js +6 -7
  51. package/src/roles/reviewer-role.js +4 -5
  52. package/src/roles/security-role.js +3 -4
  53. package/src/roles/solomon-role.js +6 -18
  54. package/src/roles/sonar-role.js +5 -1
  55. package/src/roles/tester-role.js +8 -5
  56. package/src/roles/triage-role.js +2 -2
  57. package/src/session-cleanup.js +29 -24
  58. package/src/session-store.js +1 -1
  59. package/src/sonar/api.js +1 -1
  60. package/src/sonar/manager.js +1 -1
  61. package/src/sonar/project-key.js +5 -5
  62. package/src/sonar/scanner.js +34 -65
  63. package/src/utils/display.js +312 -272
  64. package/src/utils/git.js +3 -3
  65. package/src/utils/logger.js +6 -1
  66. package/src/utils/model-selector.js +5 -5
  67. package/src/utils/process.js +80 -102
  68. package/src/utils/rate-limit-detector.js +13 -13
  69. package/src/utils/run-log.js +55 -52
  70. package/templates/kj.config.yml +33 -0
  71. package/templates/roles/architect.md +62 -0
  72. package/templates/roles/planner.md +1 -0
@@ -2,7 +2,7 @@ import { createAgent } from "../agents/index.js";
2
2
  import { CoderRole } from "../roles/coder-role.js";
3
3
  import { RefactorerRole } from "../roles/refactorer-role.js";
4
4
  import { SonarRole } from "../roles/sonar-role.js";
5
- import { addCheckpoint, markSessionStatus, saveSession, pauseSession } from "../session-store.js";
5
+ import { addCheckpoint, markSessionStatus, saveSession } from "../session-store.js";
6
6
  import { generateDiff, getUntrackedFiles } from "../review/diff-generator.js";
7
7
  import { evaluateTddPolicy } from "../review/tdd-policy.js";
8
8
  import { validateReviewResult } from "../review/schema.js";
@@ -195,6 +195,65 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
195
195
  );
196
196
  }
197
197
 
198
+ function handleSolomonAction(solomonResult, session, contextPrefix) {
199
+ if (solomonResult.action === "pause") {
200
+ return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: `${contextPrefix}_fail_fast` } };
201
+ }
202
+ if (solomonResult.action === "subtask") {
203
+ return { action: "pause", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: `${contextPrefix}_subtask` } };
204
+ }
205
+ return null;
206
+ }
207
+
208
+ async function handleSolomonContinue(solomonResult, session, counterField) {
209
+ if (solomonResult.action !== "continue") return false;
210
+ if (solomonResult.humanGuidance) {
211
+ session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
212
+ }
213
+ session[counterField] = 0;
214
+ await saveSession(session);
215
+ return true;
216
+ }
217
+
218
+ async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, session, iteration, askQuestion }) {
219
+ session.last_reviewer_feedback = tddEval.message;
220
+ session.repeated_issue_count += 1;
221
+ await saveSession(session);
222
+
223
+ if (session.repeated_issue_count < config.session.fail_fast_repeats) {
224
+ return { action: "continue" };
225
+ }
226
+
227
+ emitProgress(
228
+ emitter,
229
+ makeEvent("solomon:escalate", { ...eventBase, stage: "tdd" }, {
230
+ message: `TDD sub-loop limit reached (${session.repeated_issue_count}/${config.session.fail_fast_repeats})`,
231
+ detail: { subloop: "tdd", retryCount: session.repeated_issue_count, reason: tddEval.reason }
232
+ })
233
+ );
234
+
235
+ const solomonResult = await invokeSolomon({
236
+ config, logger, emitter, eventBase, stage: "tdd", askQuestion, session, iteration,
237
+ conflict: {
238
+ stage: "tdd",
239
+ task: session.task,
240
+ iterationCount: session.repeated_issue_count,
241
+ maxIterations: config.session.fail_fast_repeats,
242
+ reason: tddEval.reason,
243
+ sourceFiles: tddEval.sourceFiles,
244
+ testFiles: tddEval.testFiles,
245
+ history: [{ agent: "tdd-policy", feedback: tddEval.message }]
246
+ }
247
+ });
248
+
249
+ const actionResult = handleSolomonAction(solomonResult, session, "tdd");
250
+ if (actionResult) return actionResult;
251
+ const continued = await handleSolomonContinue(solomonResult, session, "repeated_issue_count");
252
+ if (continued) return { action: "continue" };
253
+
254
+ return { action: "continue" };
255
+ }
256
+
198
257
  export async function runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, askQuestion }) {
199
258
  logger.setContext({ iteration, stage: "tdd" });
200
259
  const tddDiff = await generateDiff({ baseRef: session.session_start_sha });
@@ -224,51 +283,75 @@ export async function runTddCheckStage({ config, logger, emitter, eventBase, ses
224
283
  );
225
284
 
226
285
  if (!tddEval.ok) {
227
- session.last_reviewer_feedback = tddEval.message;
228
- session.repeated_issue_count += 1;
229
- await saveSession(session);
230
- if (session.repeated_issue_count >= config.session.fail_fast_repeats) {
231
- emitProgress(
232
- emitter,
233
- makeEvent("solomon:escalate", { ...eventBase, stage: "tdd" }, {
234
- message: `TDD sub-loop limit reached (${session.repeated_issue_count}/${config.session.fail_fast_repeats})`,
235
- detail: { subloop: "tdd", retryCount: session.repeated_issue_count, reason: tddEval.reason }
236
- })
237
- );
238
-
239
- const solomonResult = await invokeSolomon({
240
- config, logger, emitter, eventBase, stage: "tdd", askQuestion, session, iteration,
241
- conflict: {
242
- stage: "tdd",
243
- task: session.task,
244
- iterationCount: session.repeated_issue_count,
245
- maxIterations: config.session.fail_fast_repeats,
246
- reason: tddEval.reason,
247
- sourceFiles: tddEval.sourceFiles,
248
- testFiles: tddEval.testFiles,
249
- history: [{ agent: "tdd-policy", feedback: tddEval.message }]
250
- }
251
- });
286
+ return handleTddFailure({ tddEval, config, logger, emitter, eventBase, session, iteration, askQuestion });
287
+ }
252
288
 
253
- if (solomonResult.action === "pause") {
254
- return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "tdd_fail_fast" } };
255
- }
256
- if (solomonResult.action === "continue") {
257
- if (solomonResult.humanGuidance) {
258
- session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
259
- }
260
- session.repeated_issue_count = 0;
261
- await saveSession(session);
262
- return { action: "continue" };
263
- }
264
- if (solomonResult.action === "subtask") {
265
- return { action: "pause", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "tdd_subtask" } };
266
- }
289
+ return { action: "ok" };
290
+ }
291
+
292
+ async function handleSonarStalled({ repeatDetector, logger, emitter, eventBase, session, budgetSummary }) {
293
+ const repeatCounts = repeatDetector.getRepeatCounts();
294
+ const message = `No progress: SonarQube issues repeated ${repeatCounts.sonar} times.`;
295
+ logger.warn(message);
296
+ await markSessionStatus(session, "stalled");
297
+ const repeatState = repeatDetector.isStalled();
298
+ emitProgress(
299
+ emitter,
300
+ makeEvent("session:end", { ...eventBase, stage: "sonar" }, {
301
+ status: "stalled",
302
+ message,
303
+ detail: { reason: repeatState.reason, repeats: repeatCounts.sonar, budget: budgetSummary() }
304
+ })
305
+ );
306
+ return { action: "stalled", result: { approved: false, sessionId: session.id, reason: "stalled" } };
307
+ }
308
+
309
+ async function handleSonarRetryLimit({ config, logger, emitter, eventBase, session, iteration, askQuestion, task, maxSonarRetries, sonarResult }) {
310
+ emitProgress(
311
+ emitter,
312
+ makeEvent("solomon:escalate", { ...eventBase, stage: "sonar" }, {
313
+ message: `Sonar sub-loop limit reached (${session.sonar_retry_count}/${maxSonarRetries})`,
314
+ detail: { subloop: "sonar", retryCount: session.sonar_retry_count, limit: maxSonarRetries, gateStatus: sonarResult.gateStatus }
315
+ })
316
+ );
317
+
318
+ const solomonResult = await invokeSolomon({
319
+ config, logger, emitter, eventBase, stage: "sonar", askQuestion, session, iteration,
320
+ conflict: {
321
+ stage: "sonar",
322
+ task,
323
+ iterationCount: session.sonar_retry_count,
324
+ maxIterations: maxSonarRetries,
325
+ history: [{ agent: "sonar", feedback: session.last_sonar_summary }]
267
326
  }
268
- return { action: "continue" };
327
+ });
328
+
329
+ const actionResult = handleSolomonAction(solomonResult, session, "sonar");
330
+ if (actionResult) return actionResult;
331
+ const continued = await handleSolomonContinue(solomonResult, session, "sonar_retry_count");
332
+ if (continued) return { action: "continue" };
333
+
334
+ return null;
335
+ }
336
+
337
+ async function handleSonarBlocking({ sonarResult, config, logger, emitter, eventBase, session, iteration, repeatDetector, budgetSummary, askQuestion, task }) {
338
+ repeatDetector.addIteration(sonarResult.issues, []);
339
+ const repeatState = repeatDetector.isStalled();
340
+ if (repeatState.stalled) {
341
+ return handleSonarStalled({ repeatDetector, logger, emitter, eventBase, session, budgetSummary });
269
342
  }
270
343
 
271
- return { action: "ok" };
344
+ session.last_reviewer_feedback = `Sonar gate blocking (${sonarResult.gateStatus}). Resolve critical findings first.`;
345
+ session.sonar_retry_count = (session.sonar_retry_count || 0) + 1;
346
+ await saveSession(session);
347
+ const maxSonarRetries = config.session.max_sonar_retries ?? config.session.fail_fast_repeats;
348
+
349
+ if (session.sonar_retry_count >= maxSonarRetries) {
350
+ const result = await handleSonarRetryLimit({ config, logger, emitter, eventBase, session, iteration, askQuestion, task, maxSonarRetries, sonarResult });
351
+ if (result) return result;
352
+ }
353
+
354
+ return { action: "continue" };
272
355
  }
273
356
 
274
357
  export async function runSonarStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, repeatDetector, budgetSummary, sonarState, askQuestion, task }) {
@@ -326,64 +409,7 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
326
409
  );
327
410
 
328
411
  if (sonarResult.blocking) {
329
- repeatDetector.addIteration(sonarResult.issues, []);
330
- const repeatState = repeatDetector.isStalled();
331
- if (repeatState.stalled) {
332
- const repeatCounts = repeatDetector.getRepeatCounts();
333
- const message = `No progress: SonarQube issues repeated ${repeatCounts.sonar} times.`;
334
- logger.warn(message);
335
- await markSessionStatus(session, "stalled");
336
- emitProgress(
337
- emitter,
338
- makeEvent("session:end", { ...eventBase, stage: "sonar" }, {
339
- status: "stalled",
340
- message,
341
- detail: { reason: repeatState.reason, repeats: repeatCounts.sonar, budget: budgetSummary() }
342
- })
343
- );
344
- return { action: "stalled", result: { approved: false, sessionId: session.id, reason: "stalled" } };
345
- }
346
-
347
- session.last_reviewer_feedback = `Sonar gate blocking (${sonarResult.gateStatus}). Resolve critical findings first.`;
348
- session.sonar_retry_count = (session.sonar_retry_count || 0) + 1;
349
- await saveSession(session);
350
- const maxSonarRetries = config.session.max_sonar_retries ?? config.session.fail_fast_repeats;
351
- if (session.sonar_retry_count >= maxSonarRetries) {
352
- emitProgress(
353
- emitter,
354
- makeEvent("solomon:escalate", { ...eventBase, stage: "sonar" }, {
355
- message: `Sonar sub-loop limit reached (${session.sonar_retry_count}/${maxSonarRetries})`,
356
- detail: { subloop: "sonar", retryCount: session.sonar_retry_count, limit: maxSonarRetries, gateStatus: sonarResult.gateStatus }
357
- })
358
- );
359
-
360
- const solomonResult = await invokeSolomon({
361
- config, logger, emitter, eventBase, stage: "sonar", askQuestion, session, iteration,
362
- conflict: {
363
- stage: "sonar",
364
- task,
365
- iterationCount: session.sonar_retry_count,
366
- maxIterations: maxSonarRetries,
367
- history: [{ agent: "sonar", feedback: session.last_sonar_summary }]
368
- }
369
- });
370
-
371
- if (solomonResult.action === "pause") {
372
- return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "sonar_fail_fast" } };
373
- }
374
- if (solomonResult.action === "continue") {
375
- if (solomonResult.humanGuidance) {
376
- session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
377
- }
378
- session.sonar_retry_count = 0;
379
- await saveSession(session);
380
- return { action: "continue" };
381
- }
382
- if (solomonResult.action === "subtask") {
383
- return { action: "pause", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "sonar_subtask" } };
384
- }
385
- }
386
- return { action: "continue" };
412
+ return handleSonarBlocking({ sonarResult, config, logger, emitter, eventBase, session, iteration, repeatDetector, budgetSummary, askQuestion, task });
387
413
  }
388
414
 
389
415
  // Sonar passed — reset retry counter
@@ -401,6 +427,78 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
401
427
  return { action: "ok", stageResult };
402
428
  }
403
429
 
430
+ async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, repeatDetector }) {
431
+ logger.warn(`Reviewer stalled (${repeatCounts.reviewer} repeats). Invoking Solomon mediation.`);
432
+ emitProgress(
433
+ emitter,
434
+ makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
435
+ message: `Reviewer stalled — Solomon mediating`,
436
+ detail: { repeats: repeatCounts.reviewer, reason: repeatState.reason }
437
+ })
438
+ );
439
+
440
+ const solomonResult = await invokeSolomon({
441
+ config, logger, emitter, eventBase, stage: "reviewer", askQuestion, session, iteration,
442
+ conflict: {
443
+ stage: "reviewer",
444
+ task,
445
+ iterationCount: repeatCounts.reviewer,
446
+ maxIterations: config.session?.fail_fast_repeats ?? 2,
447
+ stalledReason: repeatState.reason,
448
+ blockingIssues: review.blocking_issues,
449
+ history: [{ agent: "reviewer", feedback: review.blocking_issues.map(x => x.description).join("; ") }]
450
+ }
451
+ });
452
+
453
+ if (solomonResult.action === "pause") {
454
+ await markSessionStatus(session, "stalled");
455
+ return { review, stalled: true, stalledResult: { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_stalled" } };
456
+ }
457
+ if (solomonResult.action === "continue") {
458
+ repeatDetector.reviewer = { lastHash: null, repeatCount: 0 };
459
+ if (solomonResult.humanGuidance) {
460
+ session.last_reviewer_feedback = `Solomon/user guidance: ${solomonResult.humanGuidance}`;
461
+ await saveSession(session);
462
+ }
463
+ return { review };
464
+ }
465
+ if (solomonResult.action === "subtask") {
466
+ return { review, stalled: true, stalledResult: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "reviewer_subtask" } };
467
+ }
468
+
469
+ // Fallback
470
+ const message = `Manual intervention required: reviewer issues repeated ${repeatCounts.reviewer} times.`;
471
+ await markSessionStatus(session, "stalled");
472
+ emitProgress(
473
+ emitter,
474
+ makeEvent("session:end", { ...eventBase, stage: "reviewer" }, {
475
+ status: "stalled",
476
+ message,
477
+ detail: { reason: repeatState.reason, repeats: repeatCounts.reviewer, budget: budgetSummary() }
478
+ })
479
+ );
480
+ return { review, stalled: true, stalledResult: { approved: false, sessionId: session.id, reason: "stalled" } };
481
+ }
482
+
483
+ async function handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary }) {
484
+ repeatDetector.addIteration([], review.blocking_issues);
485
+ const repeatState = repeatDetector.isStalled();
486
+ if (!repeatState.stalled) return null;
487
+
488
+ const repeatCounts = repeatDetector.getRepeatCounts();
489
+ return handleReviewerStalledSolomon({ review, repeatCounts, repeatState, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, repeatDetector });
490
+ }
491
+
492
+ async function fetchReviewDiff(session, logger) {
493
+ if (session.becaria_pr_number) {
494
+ const { getPrDiff } = await import("../becaria/pr-diff.js");
495
+ const diff = await getPrDiff(session.becaria_pr_number);
496
+ logger.info(`Reviewer reading PR diff #${session.becaria_pr_number}`);
497
+ return diff;
498
+ }
499
+ return generateDiff({ baseRef: session.session_start_sha });
500
+ }
501
+
404
502
  export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
405
503
  logger.setContext({ iteration, stage: "reviewer" });
406
504
  emitProgress(
@@ -411,14 +509,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
411
509
  })
412
510
  );
413
511
 
414
- let diff;
415
- if (session.becaria_pr_number) {
416
- const { getPrDiff } = await import("../becaria/pr-diff.js");
417
- diff = await getPrDiff(session.becaria_pr_number);
418
- logger.info(`Reviewer reading PR diff #${session.becaria_pr_number}`);
419
- } else {
420
- diff = await generateDiff({ baseRef: session.session_start_sha });
421
- }
512
+ const diff = await fetchReviewDiff(session, logger);
422
513
  const reviewerOnOutput = ({ stream, line }) => {
423
514
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
424
515
  message: line,
@@ -447,7 +538,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
447
538
  reviewerStall.stop();
448
539
  }
449
540
 
450
- if (!reviewerExec.execResult || !reviewerExec.execResult.ok) {
541
+ if (!reviewerExec.execResult?.ok) {
451
542
  const lastAttempt = reviewerExec.attempts.at(-1);
452
543
  const details =
453
544
  lastAttempt?.result?.error ||
@@ -493,17 +584,17 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
493
584
  summary: reviewResult.raw_summary || "",
494
585
  confidence: reviewResult.confidence ?? 0
495
586
  });
496
- } catch (parseErr) {
497
- logger.warn(`Reviewer output validation failed: ${parseErr.message}`);
587
+ } catch (error_) {
588
+ logger.warn(`Reviewer output validation failed: ${error_.message}`);
498
589
  review = {
499
590
  approved: false,
500
591
  blocking_issues: [{
501
592
  id: "PARSE_ERROR",
502
593
  severity: "high",
503
- description: `Reviewer output could not be parsed: ${parseErr.message}`
594
+ description: `Reviewer output could not be parsed: ${error_.message}`
504
595
  }],
505
596
  non_blocking_suggestions: [],
506
- summary: `Parse error: ${parseErr.message}`,
597
+ summary: `Parse error: ${error_.message}`,
507
598
  confidence: 0
508
599
  };
509
600
  }
@@ -565,63 +656,8 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
565
656
  );
566
657
 
567
658
  if (!review.approved) {
568
- repeatDetector.addIteration([], review.blocking_issues);
569
- const repeatState = repeatDetector.isStalled();
570
- if (repeatState.stalled) {
571
- const repeatCounts = repeatDetector.getRepeatCounts();
572
-
573
- // --- Solomon mediation for stalled reviewer ---
574
- logger.warn(`Reviewer stalled (${repeatCounts.reviewer} repeats). Invoking Solomon mediation.`);
575
- emitProgress(
576
- emitter,
577
- makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
578
- message: `Reviewer stalled — Solomon mediating`,
579
- detail: { repeats: repeatCounts.reviewer, reason: repeatState.reason }
580
- })
581
- );
582
-
583
- const solomonResult = await invokeSolomon({
584
- config, logger, emitter, eventBase, stage: "reviewer", askQuestion, session, iteration,
585
- conflict: {
586
- stage: "reviewer",
587
- task,
588
- iterationCount: repeatCounts.reviewer,
589
- maxIterations: config.session?.fail_fast_repeats ?? 2,
590
- stalledReason: repeatState.reason,
591
- blockingIssues: review.blocking_issues,
592
- history: [{ agent: "reviewer", feedback: review.blocking_issues.map(x => x.description).join("; ") }]
593
- }
594
- });
595
-
596
- if (solomonResult.action === "pause") {
597
- await markSessionStatus(session, "stalled");
598
- return { review, stalled: true, stalledResult: { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_stalled" } };
599
- }
600
- if (solomonResult.action === "continue") {
601
- repeatDetector.reviewer = { lastHash: null, repeatCount: 0 };
602
- if (solomonResult.humanGuidance) {
603
- session.last_reviewer_feedback = `Solomon/user guidance: ${solomonResult.humanGuidance}`;
604
- await saveSession(session);
605
- }
606
- return { review };
607
- }
608
- if (solomonResult.action === "subtask") {
609
- return { review, stalled: true, stalledResult: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "reviewer_subtask" } };
610
- }
611
-
612
- // Fallback
613
- const message = `Manual intervention required: reviewer issues repeated ${repeatCounts.reviewer} times.`;
614
- await markSessionStatus(session, "stalled");
615
- emitProgress(
616
- emitter,
617
- makeEvent("session:end", { ...eventBase, stage: "reviewer" }, {
618
- status: "stalled",
619
- message,
620
- detail: { reason: repeatState.reason, repeats: repeatCounts.reviewer, budget: budgetSummary() }
621
- })
622
- );
623
- return { review, stalled: true, stalledResult: { approved: false, sessionId: session.id, reason: "stalled" } };
624
- }
659
+ const rejectionResult = await handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary });
660
+ if (rejectionResult) return rejectionResult;
625
661
  }
626
662
 
627
663
  return { review };