@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
@@ -0,0 +1,68 @@
1
+ /**
2
+ * OpenRouter request routing for the OpenAI-compatible adapter
3
+ * (`openai-compatible`).
4
+ *
5
+ * OpenRouter serves a single model slug from many independent backends whose
6
+ * capabilities differ — some don't support structured outputs (`json_schema`)
7
+ * at all, others are lower-fidelity quantizations. OpenRouter picks a backend
8
+ * per request, so a slug that drove neal's loop yesterday can fail today with
9
+ * no slug change: it just got routed to a backend that can't honor the
10
+ * parameters neal sends. That is exactly the drift that broke the coder's
11
+ * structured-output cells in the live smoke.
12
+ *
13
+ * `require_parameters: true` tells OpenRouter to only route to backends that
14
+ * support every parameter in the request. Since neal's coder sends
15
+ * `response_format: { type: 'json_schema' }`, this excludes the backends that
16
+ * can't do structured output and keeps a slug's behavior stable across
17
+ * requests — so when the compat whitelist says a slug works, it keeps working.
18
+ *
19
+ * It is applied as a default (no config knob) whenever the endpoint is
20
+ * OpenRouter, and is a no-op for every other OpenAI-compatible endpoint, which
21
+ * would reject or ignore the OpenRouter-specific `provider` field.
22
+ */
23
+ const OPENROUTER_HOST = 'openrouter.ai';
24
+ export function isOpenRouterBaseUrl(baseUrl) {
25
+ let host;
26
+ try {
27
+ host = new URL(baseUrl).host.toLowerCase();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ return host === OPENROUTER_HOST || host.endsWith(`.${OPENROUTER_HOST}`);
33
+ }
34
+ // Add `provider.require_parameters: true` to a chat-completion request body,
35
+ // leaving an already-present `provider` block untouched (a caller that set its
36
+ // own routing wins). Anything that isn't a JSON object body passes through
37
+ // unchanged, so a non-chat request or an opaque body is never corrupted.
38
+ export function injectRequireParameters(init) {
39
+ if (!init || typeof init.body !== 'string') {
40
+ return init;
41
+ }
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(init.body);
45
+ }
46
+ catch {
47
+ return init;
48
+ }
49
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
50
+ return init;
51
+ }
52
+ const body = parsed;
53
+ if ('provider' in body) {
54
+ return init;
55
+ }
56
+ return { ...init, body: JSON.stringify({ ...body, provider: { require_parameters: true } }) };
57
+ }
58
+ /**
59
+ * Wrap `inner` so requests to OpenRouter carry `provider.require_parameters:
60
+ * true`. Returns `inner` unchanged for non-OpenRouter base URLs so no other
61
+ * endpoint sees the OpenRouter-specific field.
62
+ */
63
+ export function withOpenRouterRouting(baseUrl, inner) {
64
+ if (!isOpenRouterBaseUrl(baseUrl)) {
65
+ return inner;
66
+ }
67
+ return (input, init) => inner(input, injectRequireParameters(init));
68
+ }
@@ -1,5 +1,5 @@
1
1
  // Shared pricing type and rate math for token-only provider adapters
2
- // (openai-compatible, generic-agentic, and openai-codex). Those adapters report
2
+ // (openai-compatible and openai-codex). Those adapters report
3
3
  // token counts but not dollars. Cost is resolved per bucket in this order:
4
4
  // operator-configured per-million rates under
5
5
  // `providers.openai_compatible.pricing` (the override tier), else the vendored
@@ -89,9 +89,6 @@ export const RATE_CARD = {
89
89
  'azure/eu/gpt-4o-2024-08-06': { inputCostPerToken: 0.00000275, outputCostPerToken: 0.000011, cacheReadInputTokenCost: 0.000001375 },
90
90
  'azure/eu/gpt-4o-2024-11-20': { inputCostPerToken: 0.00000275, outputCostPerToken: 0.000011 },
91
91
  'azure/eu/gpt-4o-mini-2024-07-18': { inputCostPerToken: 1.65e-7, outputCostPerToken: 6.6e-7, cacheReadInputTokenCost: 8.3e-8 },
92
- 'azure/eu/gpt-4o-mini-realtime-preview-2024-12-17': { inputCostPerToken: 6.6e-7, outputCostPerToken: 0.00000264, cacheReadInputTokenCost: 3.3e-7 },
93
- 'azure/eu/gpt-4o-realtime-preview-2024-10-01': { inputCostPerToken: 0.0000055, outputCostPerToken: 0.000022, cacheReadInputTokenCost: 0.00000275 },
94
- 'azure/eu/gpt-4o-realtime-preview-2024-12-17': { inputCostPerToken: 0.0000055, outputCostPerToken: 0.000022, cacheReadInputTokenCost: 0.00000275 },
95
92
  'azure/eu/gpt-5-2025-08-07': { inputCostPerToken: 0.000001375, outputCostPerToken: 0.000011, cacheReadInputTokenCost: 1.375e-7 },
96
93
  'azure/eu/gpt-5-mini-2025-08-07': { inputCostPerToken: 2.75e-7, outputCostPerToken: 0.0000022, cacheReadInputTokenCost: 2.75e-8 },
97
94
  'azure/eu/gpt-5-nano-2025-08-07': { inputCostPerToken: 5.5e-8, outputCostPerToken: 4.4e-7, cacheReadInputTokenCost: 5.5e-9 },
@@ -154,9 +151,6 @@ export const RATE_CARD = {
154
151
  'azure/gpt-4o-mini': { inputCostPerToken: 1.65e-7, outputCostPerToken: 6.6e-7, cacheReadInputTokenCost: 7.5e-8 },
155
152
  'azure/gpt-4o-mini-2024-07-18': { inputCostPerToken: 1.65e-7, outputCostPerToken: 6.6e-7, cacheReadInputTokenCost: 7.5e-8 },
156
153
  'azure/gpt-4o-mini-audio-preview-2024-12-17': { inputCostPerToken: 0.0000025, outputCostPerToken: 0.00001 },
157
- 'azure/gpt-4o-mini-realtime-preview-2024-12-17': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024, cacheReadInputTokenCost: 3e-7 },
158
- 'azure/gpt-4o-realtime-preview-2024-10-01': { inputCostPerToken: 0.000005, outputCostPerToken: 0.00002, cacheReadInputTokenCost: 0.0000025 },
159
- 'azure/gpt-4o-realtime-preview-2024-12-17': { inputCostPerToken: 0.000005, outputCostPerToken: 0.00002, cacheReadInputTokenCost: 0.0000025 },
160
154
  'azure/gpt-5': { inputCostPerToken: 0.00000125, outputCostPerToken: 0.00001, cacheReadInputTokenCost: 1.25e-7 },
161
155
  'azure/gpt-5-2025-08-07': { inputCostPerToken: 0.00000125, outputCostPerToken: 0.00001, cacheReadInputTokenCost: 1.25e-7 },
162
156
  'azure/gpt-5-chat': { inputCostPerToken: 0.00000125, outputCostPerToken: 0.00001, cacheReadInputTokenCost: 1.25e-7 },
@@ -204,9 +198,6 @@ export const RATE_CARD = {
204
198
  'azure/gpt-audio-1.5-2026-02-23': { inputCostPerToken: 0.0000025, outputCostPerToken: 0.00001 },
205
199
  'azure/gpt-audio-2025-08-28': { inputCostPerToken: 0.0000025, outputCostPerToken: 0.00001 },
206
200
  'azure/gpt-audio-mini-2025-10-06': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024 },
207
- 'azure/gpt-realtime-1.5-2026-02-23': { inputCostPerToken: 0.000004, outputCostPerToken: 0.000016, cacheReadInputTokenCost: 0.000004 },
208
- 'azure/gpt-realtime-2025-08-28': { inputCostPerToken: 0.000004, outputCostPerToken: 0.000016, cacheReadInputTokenCost: 0.000004 },
209
- 'azure/gpt-realtime-mini-2025-10-06': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024, cacheReadInputTokenCost: 6e-8 },
210
201
  'azure/mistral-large-2402': { inputCostPerToken: 0.000008, outputCostPerToken: 0.000024 },
211
202
  'azure/mistral-large-latest': { inputCostPerToken: 0.000008, outputCostPerToken: 0.000024 },
212
203
  'azure/o1': { inputCostPerToken: 0.000015, outputCostPerToken: 0.00006, cacheReadInputTokenCost: 0.0000075 },
@@ -230,9 +221,6 @@ export const RATE_CARD = {
230
221
  'azure/us/gpt-4o-2024-08-06': { inputCostPerToken: 0.00000275, outputCostPerToken: 0.000011, cacheReadInputTokenCost: 0.000001375 },
231
222
  'azure/us/gpt-4o-2024-11-20': { inputCostPerToken: 0.00000275, outputCostPerToken: 0.000011 },
232
223
  'azure/us/gpt-4o-mini-2024-07-18': { inputCostPerToken: 1.65e-7, outputCostPerToken: 6.6e-7, cacheReadInputTokenCost: 8.3e-8 },
233
- 'azure/us/gpt-4o-mini-realtime-preview-2024-12-17': { inputCostPerToken: 6.6e-7, outputCostPerToken: 0.00000264, cacheReadInputTokenCost: 3.3e-7 },
234
- 'azure/us/gpt-4o-realtime-preview-2024-10-01': { inputCostPerToken: 0.0000055, outputCostPerToken: 0.000022, cacheReadInputTokenCost: 0.00000275 },
235
- 'azure/us/gpt-4o-realtime-preview-2024-12-17': { inputCostPerToken: 0.0000055, outputCostPerToken: 0.000022, cacheReadInputTokenCost: 0.00000275 },
236
224
  'azure/us/gpt-5-2025-08-07': { inputCostPerToken: 0.000001375, outputCostPerToken: 0.000011, cacheReadInputTokenCost: 1.375e-7 },
237
225
  'azure/us/gpt-5-mini-2025-08-07': { inputCostPerToken: 2.75e-7, outputCostPerToken: 0.0000022, cacheReadInputTokenCost: 2.75e-8 },
238
226
  'azure/us/gpt-5-nano-2025-08-07': { inputCostPerToken: 5.5e-8, outputCostPerToken: 4.4e-7, cacheReadInputTokenCost: 5.5e-9 },
@@ -807,7 +795,7 @@ export const RATE_CARD = {
807
795
  'fireworks_ai/accounts/fireworks/models/glm-4p6': { inputCostPerToken: 5.5e-7, outputCostPerToken: 0.00000219 },
808
796
  'fireworks_ai/accounts/fireworks/models/glm-4p7': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000022, cacheReadInputTokenCost: 3e-7 },
809
797
  'fireworks_ai/accounts/fireworks/models/glm-5p1': { inputCostPerToken: 0.0000014, outputCostPerToken: 0.0000044, cacheReadInputTokenCost: 2.6e-7 },
810
- 'fireworks_ai/accounts/fireworks/models/glm-5p2': { inputCostPerToken: 0.0000014, outputCostPerToken: 0.0000044, cacheReadInputTokenCost: 2.6e-7 },
798
+ 'fireworks_ai/accounts/fireworks/models/glm-5p2': { inputCostPerToken: 0.0000014, outputCostPerToken: 0.0000044, cacheReadInputTokenCost: 1.4e-7 },
811
799
  'fireworks_ai/accounts/fireworks/models/gpt-oss-120b': { inputCostPerToken: 1.5e-7, outputCostPerToken: 6e-7, cacheReadInputTokenCost: 1.5e-8 },
812
800
  'fireworks_ai/accounts/fireworks/models/gpt-oss-20b': { inputCostPerToken: 7e-8, outputCostPerToken: 3e-7, cacheReadInputTokenCost: 3.5e-8 },
813
801
  'fireworks_ai/accounts/fireworks/models/gpt-oss-safeguard-120b': { inputCostPerToken: 0.0000012, outputCostPerToken: 0.0000012 },
@@ -988,7 +976,7 @@ export const RATE_CARD = {
988
976
  'fireworks_ai/glm-4p7': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000022, cacheReadInputTokenCost: 3e-7 },
989
977
  'fireworks_ai/glm-5p1': { inputCostPerToken: 0.0000014, outputCostPerToken: 0.0000044, cacheReadInputTokenCost: 2.6e-7 },
990
978
  'fireworks_ai/glm-5p1-fast': { inputCostPerToken: 0.0000028, outputCostPerToken: 0.0000088, cacheReadInputTokenCost: 5.2e-7 },
991
- 'fireworks_ai/glm-5p2': { inputCostPerToken: 0.0000014, outputCostPerToken: 0.0000044, cacheReadInputTokenCost: 2.6e-7 },
979
+ 'fireworks_ai/glm-5p2': { inputCostPerToken: 0.0000014, outputCostPerToken: 0.0000044, cacheReadInputTokenCost: 1.4e-7 },
992
980
  'fireworks_ai/gpt-oss-120b': { inputCostPerToken: 1.5e-7, outputCostPerToken: 6e-7, cacheReadInputTokenCost: 1.5e-8 },
993
981
  'fireworks_ai/gpt-oss-20b': { inputCostPerToken: 7e-8, outputCostPerToken: 3e-7, cacheReadInputTokenCost: 3.5e-8 },
994
982
  'fireworks_ai/kimi-k2p5': { inputCostPerToken: 6e-7, outputCostPerToken: 0.000003, cacheReadInputTokenCost: 1e-7 },
@@ -1149,13 +1137,8 @@ export const RATE_CARD = {
1149
1137
  'gpt-4o-mini-2024-07-18': { inputCostPerToken: 1.5e-7, outputCostPerToken: 6e-7, cacheReadInputTokenCost: 7.5e-8 },
1150
1138
  'gpt-4o-mini-audio-preview': { inputCostPerToken: 1.5e-7, outputCostPerToken: 6e-7 },
1151
1139
  'gpt-4o-mini-audio-preview-2024-12-17': { inputCostPerToken: 1.5e-7, outputCostPerToken: 6e-7 },
1152
- 'gpt-4o-mini-realtime-preview': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024, cacheReadInputTokenCost: 3e-7 },
1153
- 'gpt-4o-mini-realtime-preview-2024-12-17': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024, cacheReadInputTokenCost: 3e-7 },
1154
1140
  'gpt-4o-mini-search-preview': { inputCostPerToken: 1.5e-7, outputCostPerToken: 6e-7, cacheReadInputTokenCost: 7.5e-8 },
1155
1141
  'gpt-4o-mini-search-preview-2025-03-11': { inputCostPerToken: 1.5e-7, outputCostPerToken: 6e-7, cacheReadInputTokenCost: 7.5e-8 },
1156
- 'gpt-4o-realtime-preview': { inputCostPerToken: 0.000005, outputCostPerToken: 0.00002, cacheReadInputTokenCost: 0.0000025 },
1157
- 'gpt-4o-realtime-preview-2024-12-17': { inputCostPerToken: 0.000005, outputCostPerToken: 0.00002, cacheReadInputTokenCost: 0.0000025 },
1158
- 'gpt-4o-realtime-preview-2025-06-03': { inputCostPerToken: 0.000005, outputCostPerToken: 0.00002, cacheReadInputTokenCost: 0.0000025 },
1159
1142
  'gpt-4o-search-preview': { inputCostPerToken: 0.0000025, outputCostPerToken: 0.00001, cacheReadInputTokenCost: 0.00000125 },
1160
1143
  'gpt-4o-search-preview-2025-03-11': { inputCostPerToken: 0.0000025, outputCostPerToken: 0.00001, cacheReadInputTokenCost: 0.00000125 },
1161
1144
  'gpt-5': { inputCostPerToken: 0.00000125, outputCostPerToken: 0.00001, cacheReadInputTokenCost: 1.25e-7 },
@@ -1207,15 +1190,6 @@ export const RATE_CARD = {
1207
1190
  'gpt-audio-mini': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024 },
1208
1191
  'gpt-audio-mini-2025-10-06': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024 },
1209
1192
  'gpt-audio-mini-2025-12-15': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024 },
1210
- 'gpt-realtime': { inputCostPerToken: 0.000004, outputCostPerToken: 0.000016, cacheReadInputTokenCost: 4e-7 },
1211
- 'gpt-realtime-1.5': { inputCostPerToken: 0.000004, outputCostPerToken: 0.000016, cacheReadInputTokenCost: 4e-7 },
1212
- 'gpt-realtime-2': { inputCostPerToken: 0.000004, outputCostPerToken: 0.000016, cacheReadInputTokenCost: 4e-7 },
1213
- 'gpt-realtime-2.1': { inputCostPerToken: 0.000004, outputCostPerToken: 0.000024, cacheReadInputTokenCost: 4e-7 },
1214
- 'gpt-realtime-2.1-mini': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024, cacheReadInputTokenCost: 6e-8 },
1215
- 'gpt-realtime-2025-08-28': { inputCostPerToken: 0.000004, outputCostPerToken: 0.000016, cacheReadInputTokenCost: 4e-7 },
1216
- 'gpt-realtime-mini': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024 },
1217
- 'gpt-realtime-mini-2025-10-06': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024, cacheReadInputTokenCost: 6e-8 },
1218
- 'gpt-realtime-mini-2025-12-15': { inputCostPerToken: 6e-7, outputCostPerToken: 0.0000024, cacheReadInputTokenCost: 6e-8 },
1219
1193
  'gradient_ai/anthropic-claude-3-opus': { inputCostPerToken: 0.000015, outputCostPerToken: 0.000075 },
1220
1194
  'gradient_ai/anthropic-claude-3.5-haiku': { inputCostPerToken: 8e-7, outputCostPerToken: 0.000004 },
1221
1195
  'gradient_ai/anthropic-claude-3.5-sonnet': { inputCostPerToken: 0.000003, outputCostPerToken: 0.000015 },
@@ -2292,10 +2266,10 @@ export const RATE_CARD = {
2292
2266
  'zai/glm-5.1': { inputCostPerToken: 0.0000014, outputCostPerToken: 0.0000044, cacheReadInputTokenCost: 2.6e-7 },
2293
2267
  };
2294
2268
  export const RATE_CARD_SOURCE = {
2295
- url: 'https://raw.githubusercontent.com/BerriAI/litellm/f1f33f560f7a39e86f7a2e5b26b9fa032f9dcaba/model_prices_and_context_window.json',
2296
- commit: 'f1f33f560f7a39e86f7a2e5b26b9fa032f9dcaba',
2297
- retrievedAt: '2026-07-15',
2298
- sha256: '33cedefa26df0ea38c526cd8939dd420b69ce68efd0e78f091871781ccac3bde',
2269
+ url: 'https://raw.githubusercontent.com/BerriAI/litellm/d9661222492a098555f40cb8b50014054bea5ab8/model_prices_and_context_window.json',
2270
+ commit: 'd9661222492a098555f40cb8b50014054bea5ab8',
2271
+ retrievedAt: '2026-07-18',
2272
+ sha256: '8d5fdb443371f8334c28cb0ff64bf0f36ad135ec6f766eec767cc62d3b7f8092',
2299
2273
  predicate: 'key != sample_spec; finite non-negative input & output cost-per-token; mode in {chat,completion,responses} or absent',
2300
2274
  note: 'community-maintained list prices; rate-computed cost is an estimate',
2301
2275
  };
@@ -1,7 +1,6 @@
1
1
  import { anthropicClaudeProviderDefinition } from './anthropic-claude.js';
2
- import { genericAgenticProviderDefinition } from './generic-agentic.js';
3
- import { openAICodexProviderDefinition } from './openai-codex.js';
4
2
  import { openAICompatibleProviderDefinition } from './openai-compatible.js';
3
+ import { openAICodexProviderDefinition } from './openai-codex.js';
5
4
  // Computed lazily instead of as a top-level constant: openai-compatible.ts
6
5
  // imports config.ts, which imports this module, so the definition bindings may
7
6
  // still be in their temporal dead zone while this module body evaluates.
@@ -12,7 +11,6 @@ function getBuiltInProviderDefinitions() {
12
11
  openAICodexProviderDefinition,
13
12
  anthropicClaudeProviderDefinition,
14
13
  openAICompatibleProviderDefinition,
15
- genericAgenticProviderDefinition,
16
14
  ];
17
15
  }
18
16
  const providerCapabilityOverrides = new Map();
@@ -108,15 +106,6 @@ function assertRoleCapabilities(args) {
108
106
  missingCapability: 'read_tool_access',
109
107
  });
110
108
  }
111
- if (args.requirementContext.requireReadToolAccessOrInlineReviewerContext &&
112
- !args.capabilities.toolAccess.read &&
113
- !(args.capabilities.supported && args.capabilities.supportsStructuredOutput)) {
114
- throwProviderCapabilityError({
115
- requirementContext: args.requirementContext,
116
- config: args.config,
117
- missingCapability: 'read_tool_access_or_inline_reviewer_context',
118
- });
119
- }
120
109
  if (args.requirementContext.requireWriteToolAccess && !args.capabilities.toolAccess.write) {
121
110
  throwProviderCapabilityError({
122
111
  requirementContext: args.requirementContext,
@@ -161,9 +150,9 @@ function assertRoleCapabilities(args) {
161
150
  // Registry invariant: every *supported* structured-advisor (reviewer)
162
151
  // capability must be read-only. Reviewers inspect work and return verdicts;
163
152
  // they never mutate the checkout or run shell commands. Only `write`/`shell`
164
- // are constrained here — `read` is unconstrained (a reviewer may read the
165
- // checkout directly, or be a no-read inline-context reviewer). The `coder`
166
- // capability is intentionally not checked: coders keep write/shell.
153
+ // are constrained here — `read` is not constrained by this assertion (the
154
+ // writer-run capability gates separately require reviewer read access). The
155
+ // `coder` capability is intentionally not checked: coders keep write/shell.
167
156
  export function assertStructuredAdvisorReadOnly(definition) {
168
157
  const capability = definition.capabilities['structured-advisor'];
169
158
  if (!capability.supported) {
@@ -280,11 +269,11 @@ export function assertAgentConfigSupportsWriterRun(agentConfig, options = {}) {
280
269
  role: 'reviewer',
281
270
  context,
282
271
  reason: 'Neal writer runs need a reviewer provider that can inspect work and return structured verdicts',
283
- requireReadToolAccessOrInlineReviewerContext: true,
272
+ requireReadToolAccess: true,
284
273
  requireStructuredOutput: true,
285
274
  reasons: {
286
275
  structured_advisor_adapter: 'reviewer, plan-review, support, and final-completion verdicts run through the structured-advisor adapter',
287
- read_tool_access_or_inline_reviewer_context: 'reviewer rounds must either inspect repository state, diffs, and run artifacts directly with read tools, or support structured output so Neal can supply inline reviewer context (full diff, plan document, run artifacts) in the prompt',
276
+ read_tool_access: 'reviewer rounds must inspect repository state, diffs, and run artifacts directly with read tools',
288
277
  structured_output: 'reviewer rounds return schema-validated structured verdicts',
289
278
  model_override: 'a non-null reviewer model override is configured for this writer run',
290
279
  },
@@ -1,3 +1,15 @@
1
+ // Detects an OpenAI content-safety refusal in a provider-authored error or
2
+ // exception message. OpenAI flags security-adjacent requests with a stable
3
+ // notice ("This content was flagged for possible cybersecurity risk ... join
4
+ // the Trusted Access for Cyber ..."); either substring is specific enough that
5
+ // ordinary review findings mentioning "security" do not trip it. Callers must
6
+ // only pass error-channel text (an embedded gateway error message or an
7
+ // HTTP/transport exception message), never assistant review content.
8
+ export function isContentSafetyRefusalMessage(message) {
9
+ const text = message.toLowerCase();
10
+ return (text.includes('flagged for possible cybersecurity risk') ||
11
+ text.includes('trusted access for cyber'));
12
+ }
1
13
  export class NealProviderError extends Error {
2
14
  provider;
3
15
  role;
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { planResumeActions } from './resume-planner.js';
4
- import { getInteractiveRecoveryView } from './state-views.js';
4
+ import { getInteractiveRecoveryView, getPlanReviewGuidanceOriginPhase } from './state-views.js';
5
5
  export function decideResumeAction(args) {
6
6
  const resumeCommand = formatResumeCommand(args.selectedRunId);
7
7
  const messageCommand = formatMessageCommand(args.selectedRunId);
@@ -86,11 +86,12 @@ export function decideResumeAction(args) {
86
86
  };
87
87
  }
88
88
  function isWaitingForPlanReviewGuidance(state) {
89
- return (state.topLevelMode === 'plan' &&
90
- state.status === 'blocked' &&
91
- state.phase === 'blocked' &&
92
- state.blockedFromPhase === 'reviewer_plan' &&
93
- state.pendingPlanReviewGuidance === null);
89
+ // Shared discriminator: reviewer_plan plus coder-authored *response* blocks
90
+ // (which carry a durable blockerReason); dirty-worktree safety blocks at the
91
+ // same response phase have blockerReason null and are excluded. Evaluated before
92
+ // planResumeActions in decideResumeAction, so this needs_message decision takes
93
+ // precedence over a bare-resume restore of a genuine coder-authored block.
94
+ return getPlanReviewGuidanceOriginPhase(state) !== null;
94
95
  }
95
96
  function hasPendingPlanReviewGuidance(state) {
96
97
  return state.topLevelMode === 'plan' && state.pendingPlanReviewGuidance !== null;
@@ -132,11 +132,13 @@ function applyPlannedStatusAction(state, action) {
132
132
  ...state,
133
133
  phase: action.phase,
134
134
  status: 'running',
135
+ blockerReason: null,
135
136
  };
136
137
  case 'normalize_stopped_status':
137
138
  return {
138
139
  ...state,
139
140
  status: 'running',
141
+ blockerReason: null,
140
142
  };
141
143
  default:
142
144
  return state;
@@ -173,6 +175,7 @@ function applyPlannedDerivedPlanAction(state, action) {
173
175
  phase: action.phase,
174
176
  status: 'running',
175
177
  blockedFromPhase: null,
178
+ blockerReason: null,
176
179
  };
177
180
  case 'block_rejected_abandoned_derived_plan':
178
181
  return {
@@ -197,6 +200,10 @@ async function restoreResumableBlockedPhase(state, statePath, logger, action) {
197
200
  ...state,
198
201
  phase: action.phase,
199
202
  status: 'running',
203
+ // A coder-authored plan-stage response block persists a durable blockerReason;
204
+ // the resume planner is its first blocked->running writer, so clear it here so
205
+ // the reason never outlives its block (and the state satisfies the invariant).
206
+ blockerReason: null,
200
207
  });
201
208
  await logger?.event('run.resumed_from_blocked', {
202
209
  statePath,
@@ -210,6 +217,7 @@ async function normalizeStoppedStatus(state, statePath, logger, action) {
210
217
  const nextState = await saveState(statePath, {
211
218
  ...state,
212
219
  status: 'running',
220
+ blockerReason: null,
213
221
  });
214
222
  await logger?.event('run.status_normalized_on_resume', {
215
223
  statePath,
@@ -228,6 +236,7 @@ async function promoteUnexecutedDerivedPlan(state, statePath, logger, action) {
228
236
  phase: action.phase,
229
237
  status: 'running',
230
238
  blockedFromPhase: null,
239
+ blockerReason: null,
231
240
  });
232
241
  await logger?.event(action.kind === 'promote_accepted_derived_plan'
233
242
  ? 'run.promoted_accepted_derived_plan_on_resume'
@@ -248,7 +248,7 @@ function summarizeBlocker(state) {
248
248
  const persistedBlocker = latestCompletedScope?.blocker?.trim() || null;
249
249
  const recovery = state.interactiveBlockedRecovery;
250
250
  const recoveryBlocker = recovery?.blockedReason?.trim() || null;
251
- const advice = recovery?.adjudicatorAdvice ?? null;
251
+ const advice = recovery?.consultantAdvice ?? null;
252
252
  const lines = [];
253
253
  if (persistedBlocker) {
254
254
  lines.push(`- Final blocker: ${persistedBlocker}`);
@@ -257,9 +257,9 @@ function summarizeBlocker(state) {
257
257
  lines.push(`- Recovery blocker: ${recoveryBlocker}`);
258
258
  }
259
259
  if (advice) {
260
- lines.push(`- Adjudicator triage: ${advice.triageCategory} (recoverable: ${advice.recoverable ? 'yes' : 'no'})`);
260
+ lines.push(`- Consultant triage: ${advice.triageCategory} (recoverable: ${advice.recoverable ? 'yes' : 'no'})`);
261
261
  if (advice.resolutionDirective.trim()) {
262
- lines.push(`- Adjudicator suggested directive: ${advice.resolutionDirective.trim()}`);
262
+ lines.push(`- Consultant suggested directive: ${advice.resolutionDirective.trim()}`);
263
263
  }
264
264
  }
265
265
  if (lines.length === 0) {
@@ -16,3 +16,33 @@ export function toResidualReviewDebt(findings) {
16
16
  coderCommit: finding.coderCommit,
17
17
  }));
18
18
  }
19
+ // Plan-review debt is a canonical-keyed *projection* of the current findings,
20
+ // never an accumulator: for each canonicalId whose latest-round finding is a
21
+ // deferred verification-hardening finding, emit exactly one debt item (latest
22
+ // round wins). A canonical that later reopens (its latest finding goes back to
23
+ // open) or is fixed/rejected simply drops out of the projection, so the debt
24
+ // lifecycle is automatic — no stale or duplicate entries and no removal rule.
25
+ export function toPlanReviewDebt(findings) {
26
+ const latestByCanonical = new Map();
27
+ for (const finding of findings) {
28
+ const existing = latestByCanonical.get(finding.canonicalId);
29
+ if (!existing || finding.round >= existing.round) {
30
+ latestByCanonical.set(finding.canonicalId, finding);
31
+ }
32
+ }
33
+ return [...latestByCanonical.values()]
34
+ .filter((finding) => finding.status === 'deferred' && finding.findingClass === 'verification_hardening')
35
+ .map((finding) => ({
36
+ id: finding.id,
37
+ canonicalId: finding.canonicalId,
38
+ status: 'deferred',
39
+ files: [...finding.files],
40
+ claim: finding.claim,
41
+ evidence: finding.evidence ?? null,
42
+ requiredAction: finding.requiredAction,
43
+ coderDisposition: finding.coderDisposition,
44
+ coderCommit: finding.coderCommit,
45
+ findingClass: finding.findingClass,
46
+ originRound: finding.round,
47
+ }));
48
+ }
@@ -1,6 +1,6 @@
1
1
  import { getAgentTurnRetryLimit, getAgentTurnStartupTimeoutMs, getApiRetryLimit, getDefaultCoderModel, getDefaultCoderProvider, getDefaultReviewerModel, getDefaultReviewerProvider, getInactivityTimeoutMs, } from '../config.js';
2
2
  import { runCoderStructuredPrompt } from '../agents/structured-coder.js';
3
- import { createInlineSection, getReviewerDoctrineAccessMode, readOnlyReviewerNeedsInlinedDiff, renderInlineReviewerContext, reviewerNeedsInlineContext, } from '../context/inline-review-context.js';
3
+ import { getReviewerDoctrineAccessMode, readOnlyReviewerNeedsInlinedDiff, } from '../context/inline-review-context.js';
4
4
  import { runWithAgentTurnLiveness } from '../providers/liveness.js';
5
5
  import { assertProviderSupportsCoder, assertProviderSupportsStructuredAdvisor, getStructuredAdvisorAdapter, } from '../providers/registry.js';
6
6
  import { createProviderTelemetrySink } from '../providers/telemetry.js';
@@ -47,25 +47,21 @@ class AgentReviewFindingsProviderAdapter {
47
47
  async reviewDraft(args) {
48
48
  const reviewer = getStructuredAdvisorAdapter(this.args.agentConfig.reviewer);
49
49
  const schema = buildReviewFindingsReviewSchema();
50
- // The base adjudication prompt only previews the diff. A reviewer without
51
- // repository read access gets the full selected diff and the draft
52
- // findings artifact inlined so it can adjudicate entirely from the
53
- // prompt. A read-only reviewer (read tools, no shell) that exposes its own
54
- // commit-range diff tool gets the read-only range-inspection section
55
- // directing it to the git_diff tool with the exact resolved revisions; a
56
- // read-only reviewer without a commit-range diff tool (native Claude/Codex)
57
- // instead gets the full selected diff inlined and no git_diff-tool
58
- // instruction, because it can neither run git commands, call a git_diff
59
- // tool, nor rely on the truncated preview. Tool-access reviewers keep the
60
- // built prompt byte-identical.
50
+ // The base adjudication prompt only previews the diff. A read-only
51
+ // reviewer (read tools, no shell) that exposes its own commit-range diff
52
+ // tool gets the read-only range-inspection section directing it to the
53
+ // git_diff tool with the exact resolved revisions; a read-only reviewer
54
+ // without a commit-range diff tool (native Claude/Codex) instead gets the
55
+ // full selected diff inlined and no git_diff-tool instruction, because it
56
+ // can neither run git commands, call a git_diff tool, nor rely on the
57
+ // truncated preview. Tool-access reviewers keep the built prompt
58
+ // byte-identical.
61
59
  const accessMode = getReviewerDoctrineAccessMode(this.args.agentConfig.reviewer);
62
- const prompt = reviewerNeedsInlineContext(this.args.agentConfig.reviewer)
63
- ? `${args.prompt}\n\n${renderInlineReviewerContext(buildReviewFindingsReviewInlineContext(args))}`
64
- : accessMode === 'read-only'
65
- ? readOnlyReviewerNeedsInlinedDiff(this.args.agentConfig.reviewer)
66
- ? `${args.prompt}\n\n${buildReviewFindingsInlinedDiffSection(args.context)}`
67
- : `${args.prompt}\n\n${buildReviewFindingsReadOnlyInspectionSection(args.context)}`
68
- : args.prompt;
60
+ const prompt = accessMode === 'read-only'
61
+ ? readOnlyReviewerNeedsInlinedDiff(this.args.agentConfig.reviewer)
62
+ ? `${args.prompt}\n\n${buildReviewFindingsInlinedDiffSection(args.context)}`
63
+ : `${args.prompt}\n\n${buildReviewFindingsReadOnlyInspectionSection(args.context)}`
64
+ : args.prompt;
69
65
  const result = await runWithAgentTurnLiveness({
70
66
  provider: this.args.agentConfig.reviewer.provider,
71
67
  role: 'structured-advisor',
@@ -102,14 +98,6 @@ class AgentReviewFindingsProviderAdapter {
102
98
  return validateReviewFindingsReview(result.structured);
103
99
  }
104
100
  }
105
- function buildReviewFindingsReviewInlineContext(args) {
106
- return {
107
- sections: [
108
- createInlineSection(`Full selected diff for range ${args.context.externalBaseCommit}..${args.context.externalHeadCommit}`, args.context.diff || '(empty diff)'),
109
- createInlineSection('Draft findings artifact under adjudication', JSON.stringify(args.draft, null, 2)),
110
- ],
111
- };
112
- }
113
101
  function buildReviewFindingsProtocolSpec(args) {
114
102
  return {
115
103
  protocol: 'neal-json-block-v1',
@@ -160,11 +148,11 @@ function assertReviewAgentCapabilities(agentConfig) {
160
148
  role: 'reviewer',
161
149
  context: 'neal review',
162
150
  reason: 'neal review uses the configured reviewer provider to adjudicate and accept or revise the findings artifact',
163
- requireReadToolAccessOrInlineReviewerContext: true,
151
+ requireReadToolAccess: true,
164
152
  requireStructuredOutput: true,
165
153
  reasons: {
166
154
  structured_advisor_adapter: 'review findings adjudication runs through the configured structured-advisor adapter',
167
- read_tool_access_or_inline_reviewer_context: 'the reviewer must either inspect the draft, selected diff, and repository context directly with read tools, or support structured output so Neal can inline the draft findings artifact and the full selected diff into the adjudication prompt',
155
+ read_tool_access: 'the reviewer must inspect the draft, selected diff, and repository context directly with read tools',
168
156
  structured_output: 'the reviewer verdict must be schema-validated before Neal accepts the findings artifact',
169
157
  model_override: 'a non-null reviewer model override is configured for review adjudication',
170
158
  },
@@ -126,12 +126,28 @@ export function renderReviewMarkdown(state) {
126
126
  for (const round of rounds) {
127
127
  lines.push('', `## Round ${round} Findings`);
128
128
  for (const finding of state.findings.filter((item) => item.round === round)) {
129
- lines.push('', `### ${finding.id}`, `- Canonical ID: ${finding.canonicalId}`, `- Source: ${finding.source}`, `- Severity: ${finding.severity}`, `- Status: ${finding.status}`, `- Files: ${finding.files.join(', ') || 'n/a'}`, `- Claim: ${finding.claim}`, `- Evidence: ${finding.evidence?.trim() || 'n/a'}`, `- Required action: ${finding.requiredAction}`, `- Round summary: ${finding.roundSummary}`, `- Coder disposition: ${finding.coderDisposition ?? 'pending'}`, `- Coder commit: ${finding.coderCommit ?? 'pending'}`);
129
+ lines.push('', `### ${finding.id}`, `- Canonical ID: ${finding.canonicalId}`, `- Source: ${finding.source}`, `- Severity: ${finding.severity}`,
130
+ // 'n/a' keeps execute-review findings (which carry no class)
131
+ // semantically unchanged rather than mislabeling them plan_correctness.
132
+ `- Finding class: ${finding.findingClass ?? 'n/a'}`, `- Status: ${finding.status}`, `- Files: ${finding.files.join(', ') || 'n/a'}`, `- Claim: ${finding.claim}`, `- Evidence: ${finding.evidence?.trim() || 'n/a'}`, `- Required action: ${finding.requiredAction}`, `- Round summary: ${finding.roundSummary}`, `- Coder disposition: ${finding.coderDisposition ?? 'pending'}`, `- Coder commit: ${finding.coderCommit ?? 'pending'}`);
130
133
  }
131
134
  }
132
135
  }
136
+ lines.push('', '## Plan Review Debt');
137
+ appendPlanReviewDebtGroup(lines, 'Inherited', state.inheritedPlanReviewDebt);
138
+ appendPlanReviewDebtGroup(lines, 'Current', state.planReviewDebt);
133
139
  return `${lines.join('\n')}\n`;
134
140
  }
141
+ function appendPlanReviewDebtGroup(lines, label, items) {
142
+ lines.push('', `### ${label}`);
143
+ if (items.length === 0) {
144
+ lines.push('- none');
145
+ return;
146
+ }
147
+ for (const item of items) {
148
+ lines.push(`- ${item.canonicalId}: findingClass=${item.findingClass ?? 'n/a'}; originRound=${item.originRound ?? 'n/a'}; claim=${item.claim}; requiredAction=${item.requiredAction}; coderDisposition=${item.coderDisposition ?? 'none'}`);
149
+ }
150
+ }
135
151
  export async function writeReviewMarkdown(path, state) {
136
152
  await writeTextAtomic(path, renderReviewMarkdown(state));
137
153
  }
@@ -307,8 +307,10 @@ function assertValidPendingPlanReviewGuidance(state, context) {
307
307
  throwStateInvariant(context, 'pendingPlanReviewGuidance', `expected null for ${formatInvalidValue(state.topLevelMode)} mode`);
308
308
  }
309
309
  assertNonEmptyString(guidance.message, 'pendingPlanReviewGuidance.message', context);
310
- if (guidance.sourcePhase !== 'reviewer_plan') {
311
- throwStateInvariant(context, 'pendingPlanReviewGuidance.sourcePhase', `expected "reviewer_plan", received ${formatInvalidValue(guidance.sourcePhase)}`);
310
+ if (guidance.sourcePhase !== 'reviewer_plan' &&
311
+ guidance.sourcePhase !== 'coder_plan_response' &&
312
+ guidance.sourcePhase !== 'coder_plan_optional_response') {
313
+ throwStateInvariant(context, 'pendingPlanReviewGuidance.sourcePhase', `expected one of "reviewer_plan", "coder_plan_response", "coder_plan_optional_response", received ${formatInvalidValue(guidance.sourcePhase)}`);
312
314
  }
313
315
  if (typeof guidance.recordedAt !== 'string') {
314
316
  throwStateInvariant(context, 'pendingPlanReviewGuidance.recordedAt', `expected string, received ${formatInvalidValue(guidance.recordedAt)}`);
@@ -466,10 +468,16 @@ export function assertValidOrchestrationState(state, context) {
466
468
  if (activeStatusInTerminalPhase) {
467
469
  throwStateInvariant(context, 'status', `cannot be ${JSON.stringify(state.status)} while phase is terminal ${JSON.stringify(state.phase)}`);
468
470
  }
471
+ // The durable coder-authored plan-stage blocker reason must never outlive its
472
+ // block: any return to a non-blocked status (running/paused/done/failed) must
473
+ // have cleared it. This catches a missed clear at the next save/load.
474
+ if (state.status !== 'blocked' && state.blockerReason !== null) {
475
+ throwStateInvariant(context, 'blockerReason', `expected null when status is ${JSON.stringify(state.status)}, received ${formatInvalidValue(state.blockerReason)}`);
476
+ }
469
477
  assertSafeIntegerAtLeast(state.currentScopeNumber, 'currentScopeNumber', 0, context);
470
478
  assertSafeIntegerAtLeast(state.coderRetryCount, 'coderRetryCount', 0, context);
471
479
  assertSafeIntegerAtLeast(state.unattendedAutoResumeCount, 'unattendedAutoResumeCount', 0, context);
472
- assertSafeIntegerAtLeast(state.reviewStuckArbiterCount, 'reviewStuckArbiterCount', 0, context);
480
+ assertSafeIntegerAtLeast(state.consultantAttemptCount, 'consultantAttemptCount', 0, context);
473
481
  assertSafeIntegerAtLeast(state.finalCompletionContinueExecutionCount, 'finalCompletionContinueExecutionCount', 0, context);
474
482
  assertSafeIntegerAtLeast(state.splitPlanCountForCurrentScope, 'splitPlanCountForCurrentScope', 0, context);
475
483
  assertSafeIntegerAtLeast(state.derivedPlanDepth, 'derivedPlanDepth', 0, context);
@@ -117,13 +117,35 @@ export function getInteractiveRecoveryView(state) {
117
117
  pendingOperatorGuidance,
118
118
  };
119
119
  }
120
+ // The plan-stage blocked origin that a top-level plan run can answer via
121
+ // `neal resume --message`, or null when the run is not in such a wait. The
122
+ // reviewer-plan message-resume path is always eligible. A coder-authored
123
+ // *response* block (coder_plan_response / coder_plan_optional_response) is
124
+ // eligible only when it carries the durable `blockerReason`: Scope 6 sets that
125
+ // reason exclusively for coder_authored landings and leaves it null for a
126
+ // dirty-worktree safety block, which lands at the same phase but must stay a
127
+ // normal blocked state (no --message route, no waiting-for-guidance status). The
128
+ // initial `coder_plan` authoring block is deliberately excluded.
129
+ export function getPlanReviewGuidanceOriginPhase(state) {
130
+ if (state.topLevelMode !== 'plan' ||
131
+ state.status !== 'blocked' ||
132
+ state.phase !== 'blocked' ||
133
+ state.pendingPlanReviewGuidance !== null) {
134
+ return null;
135
+ }
136
+ if (state.blockedFromPhase === 'reviewer_plan') {
137
+ return 'reviewer_plan';
138
+ }
139
+ if ((state.blockedFromPhase === 'coder_plan_response' ||
140
+ state.blockedFromPhase === 'coder_plan_optional_response') &&
141
+ state.blockerReason !== null) {
142
+ return state.blockedFromPhase;
143
+ }
144
+ return null;
145
+ }
120
146
  export function getPlanReviewGuidanceView(state) {
121
147
  const pendingOperatorGuidance = state.topLevelMode === 'plan' && state.pendingPlanReviewGuidance !== null;
122
- const waitingForOperatorGuidance = state.topLevelMode === 'plan' &&
123
- state.status === 'blocked' &&
124
- state.phase === 'blocked' &&
125
- state.blockedFromPhase === 'reviewer_plan' &&
126
- state.pendingPlanReviewGuidance === null;
148
+ const waitingForOperatorGuidance = getPlanReviewGuidanceOriginPhase(state) !== null;
127
149
  return {
128
150
  kind: 'plan_review_guidance',
129
151
  waitingForOperatorGuidance,