@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.
- package/README.md +206 -209
- package/SECURITY.md +16 -19
- package/dist/neal/adjudicator/{blocked-adjudicator.js → consultant.js} +21 -21
- package/dist/neal/adjudicator/execute.js +33 -33
- package/dist/neal/adjudicator/final-completion.js +2 -31
- package/dist/neal/adjudicator/planning.js +230 -38
- package/dist/neal/agents/prompts.js +6 -17
- package/dist/neal/agents/rounds.js +43 -73
- package/dist/neal/agents/schemas.js +47 -13
- package/dist/neal/blocked-guidance.js +16 -9
- package/dist/neal/cli.js +2 -2
- package/dist/neal/commands/check.js +2 -2
- package/dist/neal/commands/compat.js +100 -36
- package/dist/neal/commands/interactive-activity.js +2 -2
- package/dist/neal/commands/recovery-guidance.js +20 -8
- package/dist/neal/commands/runtime.js +4 -2
- package/dist/neal/config.js +16 -7
- package/dist/neal/context/inline-review-context.js +13 -28
- package/dist/neal/context/reviewer-context.js +19 -14
- package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
- package/dist/neal/eval/reviewer-eval.js +155 -0
- package/dist/neal/interactive-controls.js +5 -0
- package/dist/neal/orchestrator/completion.js +49 -2
- package/dist/neal/orchestrator/failures.js +7 -0
- package/dist/neal/orchestrator/notifications.js +9 -9
- package/dist/neal/orchestrator/phases/planning.js +117 -80
- package/dist/neal/orchestrator/phases/recovery.js +151 -115
- package/dist/neal/orchestrator/phases/review.js +28 -1
- package/dist/neal/orchestrator/transitions.js +7 -1
- package/dist/neal/orchestrator.js +3 -0
- package/dist/neal/plan-queue.js +29 -1
- package/dist/neal/prompts/execute.js +7 -19
- package/dist/neal/prompts/planning.js +64 -55
- package/dist/neal/prompts/review-doctrine.js +13 -50
- package/dist/neal/prompts/specialized.js +7 -17
- package/dist/neal/prompts/specs.js +180 -32
- package/dist/neal/providers/agent-settings-isolation.js +35 -0
- package/dist/neal/providers/anthropic-claude.js +18 -1
- package/dist/neal/providers/detection.js +3 -7
- package/dist/neal/providers/git-config-isolation.js +83 -0
- package/dist/neal/providers/openai-codex.js +25 -3
- package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
- package/dist/neal/providers/openai-compatible.js +1078 -430
- package/dist/neal/providers/openrouter-routing.js +68 -0
- package/dist/neal/providers/pricing.js +1 -1
- package/dist/neal/providers/rate-card.js +6 -32
- package/dist/neal/providers/registry.js +6 -17
- package/dist/neal/providers/types.js +12 -0
- package/dist/neal/resume-decision.js +7 -6
- package/dist/neal/resume-planner.js +9 -0
- package/dist/neal/retrospective.js +3 -3
- package/dist/neal/review-debt.js +30 -0
- package/dist/neal/review-findings/provider.js +17 -29
- package/dist/neal/review.js +17 -1
- package/dist/neal/state-invariants.js +11 -3
- package/dist/neal/state-views.js +27 -5
- package/dist/neal/state.js +59 -15
- package/dist/neal/status.js +28 -1
- package/dist/neal/support.js +3 -3
- package/docs/README.md +22 -18
- package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
- package/docs/architecture.md +79 -83
- package/docs/automation.md +11 -11
- package/docs/compat.md +91 -61
- package/docs/compatible-models.md +145 -121
- package/docs/demo.md +15 -5
- package/docs/maintenance.md +65 -34
- package/docs/plan-format.md +61 -33
- package/docs/prompt-evals.md +96 -0
- package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
- package/docs/providers.md +279 -349
- package/docs/release.md +63 -81
- package/docs/review-convergence.md +246 -0
- package/docs/state-machine.md +61 -59
- package/docs/storage.md +40 -31
- package/docs/troubleshooting.md +32 -29
- package/examples/compat/add-edit-verify/PLAN.md +2 -0
- package/examples/compat/add-edit-verify/broken.diff +2 -2
- package/examples/compat/add-edit-verify/good.diff +2 -2
- package/examples/compat/add-edit-verify/src/add.js +1 -1
- package/examples/compat/manifest.json +25 -29
- package/examples/compat/sum-grep-edit/PLAN.md +33 -0
- package/examples/compat/sum-grep-edit/broken.diff +12 -0
- package/examples/compat/sum-grep-edit/good.diff +12 -0
- package/examples/compat/sum-grep-edit/package.json +5 -0
- package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
- package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
- package/examples/issue-triage-js/README.md +0 -15
- package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
- package/examples/reviewer-eval/clean-doc/change.diff +9 -0
- package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
- package/examples/reviewer-eval/clean-extract/change.diff +13 -0
- package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
- package/examples/reviewer-eval/dropped-error/change.diff +15 -0
- package/examples/reviewer-eval/manifest.json +45 -0
- package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
- package/examples/reviewer-eval/off-by-one/change.diff +11 -0
- package/{config.yml → neal.yml} +1 -1
- package/package.json +15 -14
- package/dist/neal/providers/generic-agentic.js +0 -1256
- package/docs/comparison.md +0 -105
- package/docs/issue-pipeline.md +0 -124
- package/examples/compat/is-even-add-test/PLAN.md +0 -30
- package/examples/compat/is-even-add-test/broken.diff +0 -11
- package/examples/compat/is-even-add-test/good.diff +0 -11
- package/examples/compat/is-even-add-test/package.json +0 -5
- package/examples/compat/is-even-add-test/src/is-even.js +0 -3
- package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
- package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
- package/examples/compat/reverse-grep-edit/broken.diff +0 -12
- package/examples/compat/reverse-grep-edit/good.diff +0 -12
- package/examples/compat/reverse-grep-edit/package.json +0 -5
- package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
- 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
|
|
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:
|
|
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:
|
|
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/
|
|
2296
|
-
commit: '
|
|
2297
|
-
retrievedAt: '2026-07-
|
|
2298
|
-
sha256: '
|
|
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
|
|
165
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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?.
|
|
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(`-
|
|
260
|
+
lines.push(`- Consultant triage: ${advice.triageCategory} (recoverable: ${advice.recoverable ? 'yes' : 'no'})`);
|
|
261
261
|
if (advice.resolutionDirective.trim()) {
|
|
262
|
-
lines.push(`-
|
|
262
|
+
lines.push(`- Consultant suggested directive: ${advice.resolutionDirective.trim()}`);
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
if (lines.length === 0) {
|
package/dist/neal/review-debt.js
CHANGED
|
@@ -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 {
|
|
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
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// commit-range diff tool
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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 =
|
|
63
|
-
?
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
},
|
package/dist/neal/review.js
CHANGED
|
@@ -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}`,
|
|
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
|
-
|
|
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.
|
|
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);
|
package/dist/neal/state-views.js
CHANGED
|
@@ -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
|
|
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,
|