@navels/neal 0.2.0 → 0.3.1

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 (114) hide show
  1. package/README.md +206 -209
  2. package/SECURITY.md +16 -19
  3. package/dist/neal/adjudicator/{blocked-adjudicator.js → consultant.js} +21 -21
  4. package/dist/neal/adjudicator/execute.js +33 -33
  5. package/dist/neal/adjudicator/final-completion.js +2 -31
  6. package/dist/neal/adjudicator/planning.js +230 -38
  7. package/dist/neal/agents/prompts.js +6 -17
  8. package/dist/neal/agents/rounds.js +43 -73
  9. package/dist/neal/agents/schemas.js +47 -13
  10. package/dist/neal/blocked-guidance.js +16 -9
  11. package/dist/neal/cli.js +2 -2
  12. package/dist/neal/commands/check.js +2 -2
  13. package/dist/neal/commands/compat.js +100 -36
  14. package/dist/neal/commands/interactive-activity.js +2 -2
  15. package/dist/neal/commands/recovery-guidance.js +20 -8
  16. package/dist/neal/commands/runtime.js +4 -2
  17. package/dist/neal/config.js +16 -7
  18. package/dist/neal/context/inline-review-context.js +13 -28
  19. package/dist/neal/context/reviewer-context.js +19 -14
  20. package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
  21. package/dist/neal/eval/reviewer-eval.js +155 -0
  22. package/dist/neal/interactive-controls.js +5 -0
  23. package/dist/neal/orchestrator/completion.js +49 -2
  24. package/dist/neal/orchestrator/failures.js +7 -0
  25. package/dist/neal/orchestrator/notifications.js +9 -9
  26. package/dist/neal/orchestrator/phases/planning.js +117 -80
  27. package/dist/neal/orchestrator/phases/recovery.js +151 -115
  28. package/dist/neal/orchestrator/phases/review.js +28 -1
  29. package/dist/neal/orchestrator/transitions.js +7 -1
  30. package/dist/neal/orchestrator.js +3 -0
  31. package/dist/neal/plan-queue.js +29 -1
  32. package/dist/neal/prompts/execute.js +7 -19
  33. package/dist/neal/prompts/planning.js +64 -55
  34. package/dist/neal/prompts/review-doctrine.js +13 -50
  35. package/dist/neal/prompts/specialized.js +7 -17
  36. package/dist/neal/prompts/specs.js +180 -32
  37. package/dist/neal/providers/agent-settings-isolation.js +35 -0
  38. package/dist/neal/providers/anthropic-claude.js +18 -1
  39. package/dist/neal/providers/detection.js +3 -7
  40. package/dist/neal/providers/git-config-isolation.js +83 -0
  41. package/dist/neal/providers/openai-codex.js +25 -3
  42. package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
  43. package/dist/neal/providers/openai-compatible.js +1078 -430
  44. package/dist/neal/providers/openrouter-routing.js +68 -0
  45. package/dist/neal/providers/pricing.js +1 -1
  46. package/dist/neal/providers/rate-card.js +6 -32
  47. package/dist/neal/providers/registry.js +6 -17
  48. package/dist/neal/providers/types.js +12 -0
  49. package/dist/neal/resume-decision.js +7 -6
  50. package/dist/neal/resume-planner.js +9 -0
  51. package/dist/neal/retrospective.js +3 -3
  52. package/dist/neal/review-debt.js +30 -0
  53. package/dist/neal/review-findings/provider.js +17 -29
  54. package/dist/neal/review.js +17 -1
  55. package/dist/neal/state-invariants.js +11 -3
  56. package/dist/neal/state-views.js +27 -5
  57. package/dist/neal/state.js +59 -15
  58. package/dist/neal/status.js +28 -1
  59. package/dist/neal/support.js +3 -3
  60. package/docs/README.md +22 -18
  61. package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
  62. package/docs/architecture.md +79 -83
  63. package/docs/automation.md +11 -11
  64. package/docs/compat.md +91 -61
  65. package/docs/compatible-models.md +145 -121
  66. package/docs/demo.md +15 -5
  67. package/docs/maintenance.md +65 -34
  68. package/docs/plan-format.md +61 -33
  69. package/docs/prompt-evals.md +96 -0
  70. package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
  71. package/docs/providers.md +279 -349
  72. package/docs/release.md +63 -81
  73. package/docs/review-convergence.md +246 -0
  74. package/docs/state-machine.md +61 -59
  75. package/docs/storage.md +40 -31
  76. package/docs/troubleshooting.md +32 -29
  77. package/examples/compat/add-edit-verify/PLAN.md +2 -0
  78. package/examples/compat/add-edit-verify/broken.diff +2 -2
  79. package/examples/compat/add-edit-verify/good.diff +2 -2
  80. package/examples/compat/add-edit-verify/src/add.js +1 -1
  81. package/examples/compat/manifest.json +25 -29
  82. package/examples/compat/sum-grep-edit/PLAN.md +33 -0
  83. package/examples/compat/sum-grep-edit/broken.diff +12 -0
  84. package/examples/compat/sum-grep-edit/good.diff +12 -0
  85. package/examples/compat/sum-grep-edit/package.json +5 -0
  86. package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
  87. package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
  88. package/examples/issue-triage-js/README.md +0 -15
  89. package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
  90. package/examples/reviewer-eval/clean-doc/change.diff +9 -0
  91. package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
  92. package/examples/reviewer-eval/clean-extract/change.diff +13 -0
  93. package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
  94. package/examples/reviewer-eval/dropped-error/change.diff +15 -0
  95. package/examples/reviewer-eval/manifest.json +45 -0
  96. package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
  97. package/examples/reviewer-eval/off-by-one/change.diff +11 -0
  98. package/{config.yml → neal.yml} +1 -1
  99. package/package.json +15 -14
  100. package/dist/neal/providers/generic-agentic.js +0 -1256
  101. package/docs/comparison.md +0 -105
  102. package/docs/issue-pipeline.md +0 -124
  103. package/examples/compat/is-even-add-test/PLAN.md +0 -30
  104. package/examples/compat/is-even-add-test/broken.diff +0 -11
  105. package/examples/compat/is-even-add-test/good.diff +0 -11
  106. package/examples/compat/is-even-add-test/package.json +0 -5
  107. package/examples/compat/is-even-add-test/src/is-even.js +0 -3
  108. package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
  109. package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
  110. package/examples/compat/reverse-grep-edit/broken.diff +0 -12
  111. package/examples/compat/reverse-grep-edit/good.diff +0 -12
  112. package/examples/compat/reverse-grep-edit/package.json +0 -5
  113. package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
  114. package/examples/compat/reverse-grep-edit/test/strings.test.js +0 -15
@@ -85,7 +85,7 @@ export async function createInitialState(init, baseCommit) {
85
85
  agentConfig: init.agentConfig,
86
86
  unattended: init.unattended ?? false,
87
87
  unattendedAutoResumeCount: 0,
88
- reviewStuckArbiterCount: 0,
88
+ consultantAttemptCount: 0,
89
89
  autoSquashOnCompletion: init.autoSquashOnCompletion ?? true,
90
90
  progressJsonPath: init.progressJsonPath,
91
91
  progressMarkdownPath: init.progressMarkdownPath,
@@ -136,6 +136,14 @@ export async function createInitialState(init, baseCommit) {
136
136
  interactiveBlockedRecovery: null,
137
137
  interactiveBlockedRecoveryHistory: [],
138
138
  pendingPlanReviewGuidance: null,
139
+ // Current-negotiation plan-review debt always starts empty; it is a
140
+ // projection of the current findings recomputed as review rounds run.
141
+ planReviewDebt: [],
142
+ // Durable inherited plan-review debt is seeded once from the queue handoff
143
+ // (execution stage) and never recomputed; default empty for every other run.
144
+ inheritedPlanReviewDebt: [...(init.inheritedPlanReviewDebt ?? [])],
145
+ // A fresh run starts running, so the blocked-reason invariant requires null.
146
+ blockerReason: null,
139
147
  status: 'running',
140
148
  };
141
149
  }
@@ -189,6 +197,7 @@ const REVIEWER_MEANINGFUL_PROGRESS_ACTIONS = ['accept', 'block_for_operator', 'r
189
197
  const REVIEW_FINDING_SOURCES = ['reviewer', 'plan_structure'];
190
198
  const REVIEW_FINDING_SEVERITIES = ['blocking', 'non_blocking'];
191
199
  const REVIEW_FINDING_STATUSES = ['open', 'fixed', 'rejected', 'deferred'];
200
+ const PLAN_REVIEWER_FINDING_CLASSES = ['plan_correctness', 'verification_hardening'];
192
201
  const MANUAL_GATE_CHECK_TYPES = ['command'];
193
202
  const MANUAL_GATE_CHECK_CWDS = ['repo', 'run_dir'];
194
203
  const MANUAL_GATE_RESUME_PHASES = ['coder_scope'];
@@ -592,15 +601,30 @@ function hydrateRecentBlock(value, fieldPath = 'recentBlocks[]') {
592
601
  recordedAt: readString(record, 'recordedAt', `${fieldPath}.recordedAt`),
593
602
  };
594
603
  }
604
+ // Tolerant read for the plan-review finding class: an absent key stays
605
+ // undefined (execute-review findings and legacy states never set it and must be
606
+ // unaffected), a recognized class is kept, and any other present value is
607
+ // coerced to the fail-safe plan_correctness rather than throwing like readEnum.
608
+ function readTolerantFindingClass(record, key) {
609
+ if (!hasOwn(record, key)) {
610
+ return undefined;
611
+ }
612
+ const value = record[key];
613
+ return typeof value === 'string' && PLAN_REVIEWER_FINDING_CLASSES.includes(value)
614
+ ? value
615
+ : 'plan_correctness';
616
+ }
595
617
  function hydrateFinding(value, fieldPath = 'findings[]') {
596
618
  const finding = requireRecord(value, fieldPath);
597
619
  const evidence = readOptionalNullableString(finding, 'evidence', `${fieldPath}.evidence`);
620
+ const findingClass = readTolerantFindingClass(finding, 'findingClass');
598
621
  return {
599
622
  id: readString(finding, 'id', `${fieldPath}.id`),
600
623
  canonicalId: readString(finding, 'canonicalId', `${fieldPath}.canonicalId`),
601
624
  round: readSafeInteger(finding, 'round', `${fieldPath}.round`),
602
625
  source: readEnum(finding, 'source', REVIEW_FINDING_SOURCES, `${fieldPath}.source`),
603
626
  severity: readEnum(finding, 'severity', REVIEW_FINDING_SEVERITIES, `${fieldPath}.severity`),
627
+ ...(findingClass !== undefined ? { findingClass } : {}),
604
628
  files: readStringArray(finding, 'files', `${fieldPath}.files`),
605
629
  claim: readString(finding, 'claim', `${fieldPath}.claim`),
606
630
  ...(evidence !== undefined ? { evidence } : {}),
@@ -647,23 +671,23 @@ function hydrateInteractiveBlockedRecoveryDirective(value, fieldPath) {
647
671
  terminalOnly: readBoolean(directive, 'terminalOnly', `${fieldPath}.terminalOnly`),
648
672
  };
649
673
  }
650
- const BLOCKED_ADJUDICATOR_TRIAGE_CATEGORIES = [
674
+ const CONSULTANT_TRIAGE_CATEGORIES = [
651
675
  'misunderstanding',
652
676
  'authorization',
653
677
  'external_precondition',
654
678
  'impossible_task',
655
679
  ];
656
- function hydrateInteractiveBlockedRecoveryAdjudicatorAdvice(record, fieldPath) {
657
- const advice = readOptionalNullableRecord(record, 'adjudicatorAdvice', `${fieldPath}.adjudicatorAdvice`);
680
+ function hydrateInteractiveBlockedRecoveryConsultantAdvice(record, fieldPath) {
681
+ const advice = readOptionalNullableRecord(record, 'consultantAdvice', `${fieldPath}.consultantAdvice`);
658
682
  if (!advice) {
659
683
  return undefined;
660
684
  }
661
685
  return {
662
- recordedAt: readString(advice, 'recordedAt', `${fieldPath}.adjudicatorAdvice.recordedAt`),
663
- recoverable: readBoolean(advice, 'recoverable', `${fieldPath}.adjudicatorAdvice.recoverable`),
664
- triageCategory: readEnum(advice, 'triageCategory', BLOCKED_ADJUDICATOR_TRIAGE_CATEGORIES, `${fieldPath}.adjudicatorAdvice.triageCategory`),
665
- resolutionDirective: readString(advice, 'resolutionDirective', `${fieldPath}.adjudicatorAdvice.resolutionDirective`),
666
- rationale: readString(advice, 'rationale', `${fieldPath}.adjudicatorAdvice.rationale`),
686
+ recordedAt: readString(advice, 'recordedAt', `${fieldPath}.consultantAdvice.recordedAt`),
687
+ recoverable: readBoolean(advice, 'recoverable', `${fieldPath}.consultantAdvice.recoverable`),
688
+ triageCategory: readEnum(advice, 'triageCategory', CONSULTANT_TRIAGE_CATEGORIES, `${fieldPath}.consultantAdvice.triageCategory`),
689
+ resolutionDirective: readString(advice, 'resolutionDirective', `${fieldPath}.consultantAdvice.resolutionDirective`),
690
+ rationale: readString(advice, 'rationale', `${fieldPath}.consultantAdvice.rationale`),
667
691
  };
668
692
  }
669
693
  function hydrateInteractiveBlockedRecoveryFields(record, fieldPath) {
@@ -671,7 +695,7 @@ function hydrateInteractiveBlockedRecoveryFields(record, fieldPath) {
671
695
  if (!INTERACTIVE_BLOCKED_RECOVERY_SOURCE_PHASES.has(sourcePhase)) {
672
696
  throwInvalidState(`${fieldPath}.sourcePhase`, `unsupported interactive blocked recovery source phase ${JSON.stringify(sourcePhase)}`);
673
697
  }
674
- const advice = hydrateInteractiveBlockedRecoveryAdjudicatorAdvice(record, fieldPath);
698
+ const advice = hydrateInteractiveBlockedRecoveryConsultantAdvice(record, fieldPath);
675
699
  return {
676
700
  enteredAt: readString(record, 'enteredAt', `${fieldPath}.enteredAt`),
677
701
  sourcePhase: sourcePhase,
@@ -681,9 +705,9 @@ function hydrateInteractiveBlockedRecoveryFields(record, fieldPath) {
681
705
  pendingDirective: hydrateInteractiveBlockedRecoveryDirective(readRequired(record, 'pendingDirective', `${fieldPath}.pendingDirective`), `${fieldPath}.pendingDirective`),
682
706
  turns: readArray(record, 'turns', `${fieldPath}.turns`).map((turn, index) => hydrateInteractiveBlockedRecoveryTurn(turn, `${fieldPath}.turns[${index}]`)),
683
707
  // Only attach the key when persisted advice exists, so a recovery record with
684
- // no adjudicator advice round-trips byte-for-byte (no `adjudicatorAdvice:
708
+ // no consultant advice round-trips byte-for-byte (no `consultantAdvice:
685
709
  // undefined` residue).
686
- ...(advice ? { adjudicatorAdvice: advice } : {}),
710
+ ...(advice ? { consultantAdvice: advice } : {}),
687
711
  };
688
712
  }
689
713
  function hydrateInteractiveBlockedRecovery(value, fieldPath = 'interactiveBlockedRecovery') {
@@ -712,7 +736,7 @@ function hydratePendingPlanReviewGuidance(record, fieldPath = 'pendingPlanReview
712
736
  const guidance = requireRecord(value, fieldPath);
713
737
  return {
714
738
  message: readString(guidance, 'message', `${fieldPath}.message`),
715
- sourcePhase: readEnum(guidance, 'sourcePhase', ['reviewer_plan'], `${fieldPath}.sourcePhase`),
739
+ sourcePhase: readEnum(guidance, 'sourcePhase', ['reviewer_plan', 'coder_plan_response', 'coder_plan_optional_response'], `${fieldPath}.sourcePhase`),
716
740
  recordedAt: readString(guidance, 'recordedAt', `${fieldPath}.recordedAt`),
717
741
  };
718
742
  }
@@ -742,8 +766,13 @@ function hydrateRound(value, fieldPath = 'rounds[]') {
742
766
  findings: readStringArray(round, 'findings', `${fieldPath}.findings`),
743
767
  };
744
768
  }
745
- function hydrateResidualReviewDebtItem(value, fieldPath) {
769
+ export function hydrateResidualReviewDebtItem(value, fieldPath) {
746
770
  const item = requireRecord(value, fieldPath);
771
+ // findingClass/originRound are declared only for plan-review debt; legacy
772
+ // execute-phase debt carries neither, so both stay optional and undefined when
773
+ // absent. findingClass reuses the tolerant reader (unknown → plan_correctness).
774
+ const findingClass = readTolerantFindingClass(item, 'findingClass');
775
+ const originRound = readOptionalSafeInteger(item, 'originRound', `${fieldPath}.originRound`);
747
776
  return {
748
777
  id: readString(item, 'id', `${fieldPath}.id`),
749
778
  canonicalId: readString(item, 'canonicalId', `${fieldPath}.canonicalId`),
@@ -754,6 +783,8 @@ function hydrateResidualReviewDebtItem(value, fieldPath) {
754
783
  requiredAction: readString(item, 'requiredAction', `${fieldPath}.requiredAction`),
755
784
  coderDisposition: readNullableString(item, 'coderDisposition', `${fieldPath}.coderDisposition`),
756
785
  coderCommit: readNullableString(item, 'coderCommit', `${fieldPath}.coderCommit`),
786
+ ...(findingClass !== undefined ? { findingClass } : {}),
787
+ ...(originRound !== undefined ? { originRound } : {}),
757
788
  };
758
789
  }
759
790
  function hydrateCompletedScope(value, fieldPath = 'completedScopes[]') {
@@ -819,7 +850,7 @@ function normalizeStateV1(parsed) {
819
850
  agentConfig: hydrateAgentConfig(readRequired(state, 'agentConfig'), 'agentConfig'),
820
851
  unattended: readOptionalBoolean(state, 'unattended') ?? false,
821
852
  unattendedAutoResumeCount: readOptionalSafeInteger(state, 'unattendedAutoResumeCount') ?? 0,
822
- reviewStuckArbiterCount: readOptionalSafeInteger(state, 'reviewStuckArbiterCount') ?? 0,
853
+ consultantAttemptCount: readOptionalSafeInteger(state, 'consultantAttemptCount') ?? 0,
823
854
  // Legacy-tolerant: states persisted before this field existed hydrate to
824
855
  // true, preserving the historical always-squash behavior on resume.
825
856
  autoSquashOnCompletion: readOptionalBoolean(state, 'autoSquashOnCompletion') ?? true,
@@ -875,9 +906,22 @@ function normalizeStateV1(parsed) {
875
906
  interactiveBlockedRecovery: hydrateInteractiveBlockedRecovery(readRequired(state, 'interactiveBlockedRecovery')),
876
907
  interactiveBlockedRecoveryHistory: readArray(state, 'interactiveBlockedRecoveryHistory').map((record, index) => hydrateInteractiveBlockedRecoveryRecord(record, `interactiveBlockedRecoveryHistory[${index}]`)),
877
908
  pendingPlanReviewGuidance: hydratePendingPlanReviewGuidance(state),
909
+ // Both plan-review debt arrays are legacy-tolerant: run states persisted
910
+ // before these fields existed hydrate to an empty array.
911
+ planReviewDebt: hydratePlanReviewDebtArray(state, 'planReviewDebt'),
912
+ inheritedPlanReviewDebt: hydratePlanReviewDebtArray(state, 'inheritedPlanReviewDebt'),
913
+ // Legacy-tolerant: run states persisted before this field existed hydrate to
914
+ // null. The blocked-reason invariant is enforced in assertValidOrchestrationState.
915
+ blockerReason: readOptionalNullableString(state, 'blockerReason') ?? null,
878
916
  status: readEnum(state, 'status', ['running', 'paused', 'done', 'blocked', 'failed']),
879
917
  };
880
918
  }
919
+ function hydratePlanReviewDebtArray(state, key) {
920
+ if (!hasOwn(state, key)) {
921
+ return [];
922
+ }
923
+ return readArray(state, key).map((item, index) => hydrateResidualReviewDebtItem(item, `${key}[${index}]`));
924
+ }
881
925
  export async function loadState(path) {
882
926
  const content = await readFile(path, 'utf8');
883
927
  const parsed = JSON.parse(content);
@@ -24,6 +24,7 @@ const PROVIDER_ERROR_KINDS = new Set([
24
24
  'structured_output_invalid',
25
25
  'permission_denied',
26
26
  'session_unavailable',
27
+ 'content_refused',
27
28
  'provider_failed',
28
29
  'unknown',
29
30
  ]);
@@ -157,6 +158,7 @@ export async function buildStatusSnapshot(args) {
157
158
  lock,
158
159
  health,
159
160
  findings: summarizeFindings(state),
161
+ planReviewDebt: summarizePlanReviewDebt(state),
160
162
  commits,
161
163
  squash,
162
164
  providerError,
@@ -245,7 +247,7 @@ export function renderHumanStatusSnapshot(snapshot) {
245
247
  if (shouldShowScope(snapshot)) {
246
248
  lines.push(`- Current scope: ${snapshot.currentScopeNumber}`);
247
249
  }
248
- lines.push(`- Health: ${snapshot.health.classification} - ${snapshot.health.reason}`, `- Findings: ${snapshot.findings.openBlocking} open blocking, ${snapshot.findings.openNonBlocking} open non-blocking (${snapshot.findings.total} total)`, `- Operator guidance: waiting=${formatBoolean(snapshot.waitingForOperatorGuidance)}, pending=${formatBoolean(snapshot.pendingOperatorGuidance)}`);
250
+ lines.push(`- Health: ${snapshot.health.classification} - ${snapshot.health.reason}`, `- Findings: ${snapshot.findings.openBlocking} open blocking, ${snapshot.findings.openNonBlocking} open non-blocking (${snapshot.findings.total} total)`, `- Plan review debt: ${formatPlanReviewDebtLine(snapshot.planReviewDebt)}`, `- Operator guidance: waiting=${formatBoolean(snapshot.waitingForOperatorGuidance)}, pending=${formatBoolean(snapshot.pendingOperatorGuidance)}`);
249
251
  if (snapshot.lastMeaningfulEvent) {
250
252
  lines.push(`- Last progress: ${snapshot.lastMeaningfulEvent.type} - ${snapshot.lastMeaningfulEvent.summary}`);
251
253
  }
@@ -337,6 +339,14 @@ function shouldShowScope(snapshot) {
337
339
  function formatBoolean(value) {
338
340
  return value ? 'yes' : 'no';
339
341
  }
342
+ function formatPlanReviewDebtLine(debt) {
343
+ if (debt.total === 0) {
344
+ return '0';
345
+ }
346
+ const rounds = [...new Set(debt.items.map((item) => item.originRound).filter((round) => round !== null))].sort((a, b) => a - b);
347
+ const roundsSuffix = rounds.length > 0 ? ` (rounds ${rounds.join(', ')})` : '';
348
+ return `${debt.total}${roundsSuffix}`;
349
+ }
340
350
  function formatLockSummary(lock) {
341
351
  switch (lock.kind) {
342
352
  case 'none':
@@ -513,6 +523,10 @@ function summarizeBlocker(state, events, invalidDerivedPlanPayloadPath, guidance
513
523
  };
514
524
  }
515
525
  const candidates = [
526
+ {
527
+ reason: nonEmptyString(state.blockerReason),
528
+ source: 'RUN_STATE.json blocker reason',
529
+ },
516
530
  {
517
531
  reason: guidance?.category === 'scope_accounting_guardrail' ? guidance.summary : null,
518
532
  source: 'blocked guidance',
@@ -903,6 +917,19 @@ function summarizeFindings(state) {
903
917
  deferred: state.findings.filter((finding) => finding.status === 'deferred').length,
904
918
  };
905
919
  }
920
+ function summarizePlanReviewDebt(state) {
921
+ const toItem = (item, inherited) => ({
922
+ canonicalId: item.canonicalId,
923
+ findingClass: item.findingClass ?? null,
924
+ originRound: item.originRound ?? null,
925
+ inherited,
926
+ });
927
+ const items = [
928
+ ...state.inheritedPlanReviewDebt.map((item) => toItem(item, true)),
929
+ ...state.planReviewDebt.map((item) => toItem(item, false)),
930
+ ];
931
+ return { total: items.length, items };
932
+ }
906
933
  function summarizeCommits(state) {
907
934
  return {
908
935
  initialBaseCommit: state.initialBaseCommit,
@@ -8,9 +8,9 @@ function appendInteractiveBlockedRecoverySection(lines, title, recovery, options
8
8
  if (recovery.pendingDirective) {
9
9
  lines.push(`- Pending terminal directive at ${recovery.pendingDirective.recordedAt}: ${recovery.pendingDirective.operatorGuidance}`);
10
10
  }
11
- if (recovery.adjudicatorAdvice) {
12
- const advice = recovery.adjudicatorAdvice;
13
- lines.push(`- Adjudicator advice at ${advice.recordedAt} (read-only, not auto-applied):`, ` - Recoverable: ${advice.recoverable ? 'yes' : 'no'}`, ` - Triage category: ${advice.triageCategory}`, ` - Suggested directive: ${advice.resolutionDirective || 'n/a'}`, ` - Rationale: ${advice.rationale}`);
11
+ if (recovery.consultantAdvice) {
12
+ const advice = recovery.consultantAdvice;
13
+ lines.push(`- Consultant advice at ${advice.recordedAt} (read-only, not auto-applied):`, ` - Recoverable: ${advice.recoverable ? 'yes' : 'no'}`, ` - Triage category: ${advice.triageCategory}`, ` - Suggested directive: ${advice.resolutionDirective || 'n/a'}`, ` - Rationale: ${advice.rationale}`);
14
14
  }
15
15
  if (recovery.turns.length === 0) {
16
16
  lines.push('- Operator guidance: pending');
package/docs/README.md CHANGED
@@ -1,22 +1,26 @@
1
- # Neal Docs
1
+ # Docs
2
2
 
3
- These docs are current product and implementation references, not an archive of
4
- historical implementation plans.
3
+ These are the current product and implementation references. Historical plans
4
+ don't belong here.
5
5
 
6
- - [architecture.md](architecture.md): one-page map of how neal is built **start here**
7
- - [providers.md](providers.md): built-in provider adapter contract and extension checklist
8
- - [maintenance.md](maintenance.md): dependency-update and versioning policy
9
- - [compat.md](compat.md): `neal compat` model-compatibility smoke test (PASS/FAIL per model/role)
10
- - [compatible-models.md](compatible-models.md): dated whitelist of `neal compat`-verified models
11
- - [plan-format.md](plan-format.md): executable plan shapes, normalization, and selected-plan Git behavior
12
- - [storage.md](storage.md): project-local `.neal/` storage contract and artifact classes
13
- - [release.md](release.md): manual release process, versioning, and SDK-update policy
14
- - [demo.md](demo.md): safe `asciinema` terminal demo recording workflow
6
+ - [architecture.md](architecture.md): how neal is built (**start here**)
7
+ - [providers.md](providers.md): provider capabilities, permissions, and adapter contracts
8
+ - [compatible-models.md](compatible-models.md): dated `neal compat` results
9
+ - [compat.md](compat.md): the model-compatibility smoke test and PASS/FAIL contract
10
+ - [plan-format.md](plan-format.md): executable plan shapes and selected-plan Git behavior
11
+ - [troubleshooting.md](troubleshooting.md): common setup, run, lock, and squash failures
12
+ - [automation.md](automation.md): exit codes, status JSON, and harness behavior
13
+ - [storage.md](storage.md): project-local `.neal/` storage and artifact classes
15
14
  - [state-machine.md](state-machine.md): persisted run and queue state invariants
16
- - [PROMPT_SPECS.md](PROMPT_SPECS.md): prompt-spec inventory and ownership boundaries
17
- - [ADJUDICATOR_INVENTORY.md](ADJUDICATOR_INVENTORY.md): shared coder/reviewer loop inventory
18
- - [../examples/issue-triage-js/README.md](../examples/issue-triage-js/README.md): dependency-free in-repo example
19
- for local testing and an optional live Neal run
15
+ - [review-convergence.md](review-convergence.md): plan-review finding classes, debt, and convergence
16
+ - [prompt-specs.md](prompt-specs.md): prompt-spec inventory and ownership boundaries
17
+ - [prompt-evals.md](prompt-evals.md): prompt versioning and reviewer-recall evals
18
+ - [adjudicator-inventory.md](adjudicator-inventory.md): shared coder/reviewer loop inventory
19
+ - [maintenance.md](maintenance.md): dependency updates and versioning
20
+ - [release.md](release.md): manual release process and SDK-update policy
21
+ - [demo.md](demo.md): safe `asciinema` recording workflow
22
+ - [../examples/issue-triage-js/README.md](../examples/issue-triage-js/README.md):
23
+ dependency-free local example and optional live neal run
20
24
 
21
- The public README carries the user-facing workflow. Keep docs here focused on
22
- contracts that help maintain or extend Neal.
25
+ The public README owns the user-facing workflow. Keep these docs focused on
26
+ contracts that help maintain or extend neal.
@@ -1,17 +1,17 @@
1
- # Neal Adjudicator Inventory
1
+ # Adjudicator inventory
2
2
 
3
3
  ## Scope
4
4
 
5
- This document is the current contract for Neal's adjudicator layer: the shared
5
+ The current contract for neal's adjudicator layer covers the shared
6
6
  coder/reviewer loop vocabulary, the typed adjudication specs, the read-only
7
- loops that reuse the same contract, and the blocked adjudicator.
7
+ loops that reuse the same contract, and the consultant.
8
8
 
9
9
  Two things are explicit:
10
10
 
11
- - Neal uses the terms `adjudicator` and `adjudication spec`.
11
+ - neal uses the terms `adjudicator` and `adjudication spec`.
12
12
  - Only recurring coder/reviewer loop mechanics belong to the shared
13
13
  adjudicator. Prompt ownership stays under `src/neal/prompts/` (see
14
- [PROMPT_SPECS.md](PROMPT_SPECS.md)) and transition semantics stay in the
14
+ [prompt-specs.md](prompt-specs.md)) and transition semantics stay in the
15
15
  orchestrator layers.
16
16
 
17
17
  The typed contract for writer-run adjudication specs lives in
@@ -22,7 +22,7 @@ are attached to each in-scope writer-run adjudication spec as `loopContract`
22
22
  metadata. Read-only `neal review` findings use the same contract-only pattern
23
23
  with artifacts under `.neal/reviews/<review-id>/`.
24
24
 
25
- ## Module Map
25
+ ## Module map
26
26
 
27
27
  | Module | Owns |
28
28
  | --- | --- |
@@ -31,10 +31,10 @@ with artifacts under `.neal/reviews/<review-id>/`.
31
31
  | `src/neal/adjudicator/planning.ts` | Planning-family context resolution plus reviewer and plan-response round adjudication for `plan_review` and `derived_plan_review` |
32
32
  | `src/neal/adjudicator/execute.ts` | Execute-family context resolution, reviewer and coder-response round adjudication, findings synthesis, convergence detection (reopened canonical, stuck window), disposition resolution |
33
33
  | `src/neal/adjudicator/final-completion.ts` | Final-completion context resolution plus summary and reviewer round adjudication |
34
- | `src/neal/adjudicator/blocked-adjudicator.ts` | Read-only triage of eligible blocked states (see Blocked Adjudicator below) |
34
+ | `src/neal/adjudicator/consultant.ts` | Read-only triage of eligible blocked states (see Consultant below) |
35
35
  | `src/neal/adjudicator/artifacts.ts` | Resolving the active adjudication contract from run state and rendering it into artifacts |
36
36
 
37
- ## Ownership Split
37
+ ## Ownership split
38
38
 
39
39
  - Shared loop contract (`contracts.ts`):
40
40
  - loop kind, side-effect policy, allowed outcomes, terminal outcomes,
@@ -42,7 +42,7 @@ with artifacts under `.neal/reviews/<review-id>/`.
42
42
  - import-time validation that every in-scope adjudication spec declares the
43
43
  shared contract fields, plus validation for the contract-only
44
44
  review-findings loop
45
- - descriptive metadata only; runtime phase routing and state mutation stay in
45
+ - descriptive metadata only. Runtime phase routing and state mutation stay in
46
46
  the transition layer
47
47
  - Adjudication spec (`specs.ts`):
48
48
  - coder and reviewer prompt-surface references, validated at import time
@@ -53,8 +53,8 @@ with artifacts under `.neal/reviews/<review-id>/`.
53
53
  - validated allowed transition outcomes declared per spec
54
54
  - Adjudicator helpers (`planning.ts`, `execute.ts`, `final-completion.ts`):
55
55
  - per-family context resolution from run state
56
- - reviewer/coder round invocation, including inline-context assembly for
57
- reviewers without repository read access
56
+ - reviewer/coder round invocation, including supplying the commit-range diff
57
+ to read-only reviewers that lack their own range-diff tool
58
58
  - execute-family findings synthesis, reopened-finding and stuck-window
59
59
  detection, and disposition resolution
60
60
  - Transition layer (`src/neal/orchestrator.ts`, `src/neal/orchestrator/run-loop.ts`,
@@ -71,7 +71,7 @@ with artifacts under `.neal/reviews/<review-id>/`.
71
71
  - `createdCommits` and final-commit consequences
72
72
  - Prompt-spec layer:
73
73
  - prompt identity, builders, and role/task ownership under
74
- `src/neal/prompts/` (see [PROMPT_SPECS.md](PROMPT_SPECS.md))
74
+ `src/neal/prompts/` (see [prompt-specs.md](prompt-specs.md))
75
75
 
76
76
  `transitionSignals` in `specs.ts` are validated allowed outcomes, not a
77
77
  dispatch table. Import-time validation checks each spec against one explicit
@@ -81,7 +81,7 @@ re-checks the resolved outcome against the active adjudication spec, and the
81
81
  transition layer still maps those outcomes explicitly in runtime code rather
82
82
  than dispatching off `transitionSignals` directly.
83
83
 
84
- ## Transition Signals
84
+ ## Transition signals
85
85
 
86
86
  | Spec | Family | Validated transition signals |
87
87
  | --- | --- | --- |
@@ -93,7 +93,7 @@ than dispatching off `transitionSignals` directly.
93
93
  The `AdjudicationTransitionSignal` union also declares `leave_adjacent`, which
94
94
  no in-scope spec or family currently uses.
95
95
 
96
- ## Loop Contracts
96
+ ## Loop contracts
97
97
 
98
98
  Every in-scope spec declares allowed outcomes
99
99
  `accepted, revise, blocked, failed, cap_reached` with terminal outcomes
@@ -106,12 +106,12 @@ Every in-scope spec declares allowed outcomes
106
106
  | `execute_review` | `execute` | `code_changes` | `state.maxRounds` (review iterations) | `implementation_scope` at `state.createdCommits` plus `state.reviewMarkdownPath` |
107
107
  | `final_completion_review` | `final_completion` | `code_changes` | `state.finalCompletionContinueExecutionMax` (continued execution) | `final_completion_review` at `FINAL_COMPLETION_REVIEW.md` and `state.finalCompletionReviewVerdict` |
108
108
 
109
- ## In-Scope Adjudication Specs
109
+ ## In-scope adjudication specs
110
110
 
111
111
  ### `plan_review`
112
112
 
113
113
  - Current loop surfaces: `runCoderPlanRound`, `runPlanReviewerRound`, `runCoderPlanResponseRound(reviewMode=plan)`
114
- - Artifact under review: top-level Neal-executable plan markdown
114
+ - Artifact under review: top-level neal-executable plan markdown
115
115
  - Prompt surfaces:
116
116
  - coder primary: `plan_author.primary` via `buildPlanningPrompt`
117
117
  - coder response: `plan_author.response` via `buildCoderPlanResponsePrompt`
@@ -120,11 +120,11 @@ Every in-scope spec declares allowed outcomes
120
120
  - coder primary: `buildCoderPlanSchema` / `validateCoderPlanPayload`
121
121
  - coder response: `buildCoderPlanResponseSchema` / `validateCoderPlanResponsePayload`
122
122
  - reviewer: `buildPlanReviewerSchema` / `PlanReviewerPayload`
123
- - provider surfaces: coder primary/response use `coder_structured_schema`; reviewer uses `structured_advisor_schema`
123
+ - provider surfaces: coder primary/response use `coder_structured_schema` and reviewer uses `structured_advisor_schema`
124
124
  - Convergence rule:
125
125
  - settle when reviewer returns no open findings and the plan remains structurally valid
126
126
  - revise when findings route back to required or optional plan response
127
- - block when coder returns structured `action=blocked`; round code still renders compatibility markers for downstream state
127
+ - block when coder returns structured `action=blocked`. Round code still renders compatibility markers for downstream state
128
128
  - Transition targets: `accept_plan`, `request_revision`, `optional_revision`, `block_for_operator`
129
129
 
130
130
  ### `derived_plan_review`
@@ -137,7 +137,7 @@ Every in-scope spec declares allowed outcomes
137
137
  - Output contracts:
138
138
  - coder response: `buildCoderPlanResponseSchema` / `validateCoderPlanResponsePayload`
139
139
  - reviewer: `buildPlanReviewerSchema` / `PlanReviewerPayload`
140
- - provider surfaces: coder response uses `coder_structured_schema`; reviewer uses `structured_advisor_schema`
140
+ - provider surfaces: coder response uses `coder_structured_schema` and reviewer uses `structured_advisor_schema`
141
141
  - Convergence rule:
142
142
  - settle when reviewer returns no open findings and the derived plan is safe to adopt
143
143
  - revise through the same planning-family response loop as ordinary plan review
@@ -157,7 +157,7 @@ Every in-scope spec declares allowed outcomes
157
157
  - coder primary: `buildCoderScopeSchema` / `validateCoderScopePayload`
158
158
  - coder response: `buildCoderResponseSchema` / `validateCoderResponsePayload`
159
159
  - reviewer: `buildReviewerSchema` / `ReviewerPayload`
160
- - provider surfaces: coder primary/response use `coder_structured_schema`; reviewer uses `structured_advisor_schema`
160
+ - provider surfaces: coder primary/response use `coder_structured_schema` and reviewer uses `structured_advisor_schema`
161
161
  - Convergence rule:
162
162
  - settle when the reviewer returns no blocking findings and
163
163
  `meaningfulProgressAction === accept`, including top-level scopes already
@@ -165,13 +165,13 @@ Every in-scope spec declares allowed outcomes
165
165
  `advance_parent` is downgraded to `accept` with an explanatory rationale)
166
166
  - `advance_parent` is a distinct empty-derived-scope parent-advancement
167
167
  signal: it settles by finalizing the parent objective only when
168
- deterministic classification finds it eligible; an eligible empty-derived
168
+ deterministic classification finds it eligible. An eligible empty-derived
169
169
  classification can also upgrade a findings-free `block_for_operator` to
170
170
  `advance_parent`
171
171
  - revise when findings reopen `coder_response` or `coder_optional_response`
172
172
  - block when reviewer returns `block_for_operator`, `replace_plan`, or an
173
173
  unsafe `advance_parent`, or coder returns structured `action=blocked` /
174
- `action=split_plan`; round code still renders compatibility markers for
174
+ `action=split_plan`. Round code still renders compatibility markers for
175
175
  downstream state
176
176
  - Transition targets: `accept_scope`, `request_revision`, `optional_revision`, `block_for_operator`, `replace_plan`, `advance_parent`
177
177
  - Public review note:
@@ -182,7 +182,7 @@ Every in-scope spec declares allowed outcomes
182
182
  ### `final_completion_review`
183
183
 
184
184
  - Current loop surfaces: `runCoderFinalCompletionSummaryRound`, `runReviewerFinalCompletionRound`
185
- - Artifact under review: whole-plan completion packet assembled after the terminal execute scope settles, including aggregate review context for `initialBaseCommit..finalCommit` when Neal can read that range
185
+ - Artifact under review: whole-plan completion packet assembled after the terminal execute scope settles, including aggregate review context for `initialBaseCommit..finalCommit` when neal can read that range
186
186
  - Prompt surfaces:
187
187
  - coder: `completion_coder.final_completion` via `buildFinalCompletionSummaryPrompt`
188
188
  - reviewer: `completion_reviewer.final_completion` via `buildFinalCompletionReviewerPrompt`
@@ -192,7 +192,7 @@ Every in-scope spec declares allowed outcomes
192
192
  - provider surfaces: coder and reviewer both use `structured_advisor_schema`
193
193
  - Convergence rule:
194
194
  - settle when reviewer returns one of the three whole-plan decisions
195
- - no coder-response revision round exists; `continue_execution` hands control
195
+ - no coder-response revision round exists. `continue_execution` hands control
196
196
  back to execute transitions rather than mutating the adjudicator loop
197
197
  - Transition targets: `accept_complete`, `continue_execution`, `block_for_operator`
198
198
  - Aggregate review context:
@@ -200,7 +200,7 @@ Every in-scope spec declares allowed outcomes
200
200
  `initialBaseCommit` and resolved final commit, then records commit
201
201
  subjects, diff stat, and changed files when available
202
202
  - if the aggregate range cannot be read, the packet records an explicit
203
- unavailable reason; the reviewer prompt treats that as a completion-review
203
+ unavailable reason. The reviewer prompt treats that as a completion-review
204
204
  evidence gap rather than proof of correctness
205
205
  - the final completion reviewer shares the adversarial falsification,
206
206
  verification-skepticism, regression-preservation, and
@@ -208,7 +208,7 @@ Every in-scope spec declares allowed outcomes
208
208
  `src/neal/prompts/review-doctrine.ts`, but keeps the final-completion
209
209
  verdict schema instead of the ordinary scope-review findings schema
210
210
 
211
- ## Contract-Only Read-Only Loops
211
+ ## Contract-only read-only loops
212
212
 
213
213
  ### `review`
214
214
 
@@ -224,7 +224,7 @@ Every in-scope spec declares allowed outcomes
224
224
  - settle when reviewer returns `accepted` with final markdown
225
225
  - revise when reviewer returns concrete findings
226
226
  - block when reviewer returns `blocked`, provider validation fails, or
227
- protected writer state changes; `cap_reached` when the configured
227
+ protected writer state changes. `cap_reached` when the configured
228
228
  review-round cap is reached without acceptance
229
229
  - Loop contract (`REVIEW_FINDINGS_ADJUDICATED_LOOP_CONTRACT` in `contracts.ts`):
230
230
  - loop kind: `review`
@@ -233,54 +233,55 @@ Every in-scope spec declares allowed outcomes
233
233
  - terminal artifact: `.neal/reviews/<review-id>/REVIEW_FINAL.md`
234
234
  - State ownership:
235
235
  - review artifacts live under `.neal/reviews/<review-id>/`
236
- - review never becomes a writer-run command path; a read-only guard asserts
236
+ - review never becomes a writer-run command path. A read-only guard asserts
237
237
  that `.neal/current.json`, `.neal/current-queue.json`, and run-local
238
238
  `RUN_STATE.json` files are unchanged after the loop
239
239
 
240
- ## Blocked Adjudicator
240
+ ## Consultant
241
241
 
242
- `src/neal/adjudicator/blocked-adjudicator.ts` (`runBlockedAdjudicator`) is a
242
+ `src/neal/adjudicator/consultant.ts` (`runConsultant`) is a
243
243
  read-only triage step for blocked writer runs. It decides whether a block is an
244
- autonomously recoverable misunderstanding resolvable within the existing
245
- scope with no new authorization, external state, or scope expansion or a
246
- genuine wall (`authorization`, `external_precondition`, `impossible_task`)
244
+ autonomously recoverable misunderstanding (resolvable within the existing
245
+ scope with no new authorization, external state, or scope expansion) or a
246
+ genuine blocker (`authorization`, `external_precondition`, `impossible_task`)
247
247
  that must escalate to a human. It is not an adjudication spec: it makes no
248
- commits and no file edits, and only returns a `BlockedAdjudicatorVerdict`.
248
+ commits and no file edits, and only returns a `ConsultantVerdict`.
249
249
 
250
250
  Gates, enforced at the recovery chokepoint
251
- (`src/neal/orchestrator/phases/recovery.ts`); any failed gate falls through to
252
- generic recovery with no adjudicator invocation:
251
+ (`src/neal/orchestrator/phases/recovery.ts`). Any failed gate falls through to
252
+ generic recovery with no consultant invocation:
253
253
 
254
- - Source-phase eligibility: `ADJUDICATOR_ELIGIBLE_SOURCE_PHASES` is
254
+ - Source-phase eligibility: `CONSULTANT_ELIGIBLE_SOURCE_PHASES` is
255
255
  `reviewer_scope`, `reviewer_plan`, `coder_scope`, `coder_response`,
256
256
  `coder_optional_response`. Reviewer phases are eligible only for structural
257
- `review_stuck:` reasons; any coder block on the coder phases is eligible.
258
- - Per-scope budget: `state.reviewStuckArbiterCount`, bounded by
259
- `neal.review_stuck_arbiter_max_attempts` (default `1`; `0` disables) and
257
+ `review_stuck:` reasons. Any coder block on the coder phases is eligible.
258
+ - Per-scope budget: `state.consultantAttemptCount`, bounded by
259
+ `neal.consultant_max_attempts` (default `1`, `0` disables) and
260
260
  reset to `0` at scope boundaries. One invocation consumes one unit whether it
261
- auto-acts (unattended) or only produces advice (attended, where the verdict
262
- is surfaced as operator advice and never auto-applied).
261
+ auto-applies a recoverable verdict (both run modes) or, on a non-recoverable
262
+ verdict, finalizes terminally (unattended) or surfaces the verdict as operator
263
+ advice and yields (attended).
263
264
  - Anti-thrash window: a block that repeats a `state.recentBlocks` record with
264
265
  the same scope identity (`scopeNumber` + `derivedScopeIndex`), the same
265
- `sourcePhase`, and the same normalized blocker key with no new evidence
266
+ `sourcePhase`, the same normalized blocker key, and no new evidence
266
267
  short-circuits to a non-recoverable `impossible_task` verdict without
267
268
  running an LLM round. The chokepoint is the sole writer of
268
- `state.recentBlocks`; the adjudicator module's window helpers are pure.
269
+ `state.recentBlocks`. The consultant module's window helpers are pure.
269
270
 
270
- Verdict schema constraints (`buildBlockedAdjudicatorSchema` /
271
- `validateBlockedAdjudicatorVerdictPayload` in `src/neal/agents/schemas.ts`):
271
+ Verdict schema constraints (`buildConsultantSchema` /
272
+ `validateConsultantVerdictPayload` in `src/neal/agents/schemas.ts`):
272
273
  `recoverable=true` is valid only with `triageCategory=misunderstanding` plus a
273
- non-empty `resolutionDirective`; `recoverable=false` must not pair with
274
- `misunderstanding`; `rationale` must be non-empty; `targetCanonicalIds` is
274
+ non-empty `resolutionDirective`. `recoverable=false` must not pair with
275
+ `misunderstanding`. `rationale` must be non-empty. `targetCanonicalIds` is
275
276
  optional and defaults to `[]`. A malformed verdict is rejected so it can never
276
277
  drive an autonomous recovery.
277
278
 
278
- Prompt and round: `buildBlockedAdjudicatorPrompt` in
279
+ Prompt and round: `buildConsultantPrompt` in
279
280
  `src/neal/agents/prompts.ts` (a single no-read-safe variant that judges
280
- entirely from Neal-inlined context) run through `runBlockedAdjudicatorRound`
281
+ entirely from neal-inlined context) run through `runConsultantRound`
281
282
  in `src/neal/agents/rounds.ts`.
282
283
 
283
- ## Adjacent Or Non-Adjudicator Flows
284
+ ## Adjacent or non-adjudicator flows
284
285
 
285
286
  - `interactive_blocked_recovery` (declared in `ADJUDICATION_ADJACENT_FLOWS`):
286
287
  - current entrypoint: `runInteractiveBlockedRecoveryPhase`
@@ -288,23 +289,23 @@ in `src/neal/agents/rounds.ts`.
288
289
  than forming a pure coder/reviewer adjudication loop, so this remains
289
290
  transition-layer-owned
290
291
 
291
- ## Spec Family Mapping
292
+ ## Spec family mapping
292
293
 
293
294
  - Planning family: `plan_review`, `derived_plan_review`
294
295
  - Execute family: `execute_review` (meaningful-progress remains a capability of
295
296
  this family, not a separate adjudication spec)
296
297
  - Final-completion family: `final_completion_review`
297
298
  - Contract-only read-only loops: `review`
298
- - Adjacent read-only triage: the blocked adjudicator (not a spec)
299
+ - Adjacent read-only triage: the consultant (not a spec)
299
300
 
300
301
  Shared reviewer doctrine does not collapse these families. It is prompt wording
301
- reused across execute-scope review, plan review, and final completion review;
302
- schemas, allowed transition outcomes, and artifact/context packets remain
302
+ reused across execute-scope review, plan review, and final completion review.
303
+ Schemas, allowed transition outcomes, and artifact/context packets remain
303
304
  family-specific.
304
305
 
305
306
  New loop variants should extend the adjudication-spec family or remain
306
- explicitly adjacent; they should not reintroduce bespoke coder/reviewer loop
307
+ explicitly adjacent. They should not reintroduce custom coder/reviewer loop
307
308
  wiring inside `src/neal/orchestrator.ts`. When adding or changing a spec, keep
308
309
  `transitionSignals`, the family runtime contract, and the loop contract in
309
- sync `specs.ts` and `contracts.ts` validate all three at import time, so
310
+ sync. `specs.ts` and `contracts.ts` validate all three at import time, so
310
311
  drift fails fast in tests and at startup.