@navels/neal 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) 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 +264 -24
  7. package/dist/neal/agents/prompts.js +7 -15
  8. package/dist/neal/agents/rounds.js +51 -73
  9. package/dist/neal/agents/schemas.js +615 -502
  10. package/dist/neal/agents/structured-json.js +36 -0
  11. package/dist/neal/blocked-guidance.js +16 -9
  12. package/dist/neal/cli.js +2 -2
  13. package/dist/neal/commands/check.js +2 -2
  14. package/dist/neal/commands/compat.js +100 -36
  15. package/dist/neal/commands/interactive-activity.js +2 -2
  16. package/dist/neal/commands/recovery-guidance.js +20 -8
  17. package/dist/neal/commands/runtime.js +4 -2
  18. package/dist/neal/config.js +40 -7
  19. package/dist/neal/context/inline-review-context.js +13 -28
  20. package/dist/neal/context/reviewer-context.js +19 -14
  21. package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
  22. package/dist/neal/eval/reviewer-eval.js +155 -0
  23. package/dist/neal/git.js +9 -3
  24. package/dist/neal/interactive-controls.js +5 -0
  25. package/dist/neal/orchestrator/completion.js +215 -113
  26. package/dist/neal/orchestrator/failures.js +7 -0
  27. package/dist/neal/orchestrator/notifications.js +9 -9
  28. package/dist/neal/orchestrator/phases/planning.js +124 -112
  29. package/dist/neal/orchestrator/phases/recovery.js +151 -115
  30. package/dist/neal/orchestrator/phases/review.js +28 -1
  31. package/dist/neal/orchestrator/split-plan.js +12 -11
  32. package/dist/neal/orchestrator/transitions.js +36 -71
  33. package/dist/neal/orchestrator.js +3 -0
  34. package/dist/neal/plan-doc.js +24 -1
  35. package/dist/neal/plan-queue.js +29 -1
  36. package/dist/neal/prompts/assert-builder.js +8 -1
  37. package/dist/neal/prompts/execute.js +11 -19
  38. package/dist/neal/prompts/planning.js +64 -55
  39. package/dist/neal/prompts/review-doctrine.js +13 -50
  40. package/dist/neal/prompts/specialized.js +28 -22
  41. package/dist/neal/prompts/specs.js +223 -17
  42. package/dist/neal/providers/agent-settings-isolation.js +35 -0
  43. package/dist/neal/providers/anthropic-claude.js +309 -248
  44. package/dist/neal/providers/detection.js +3 -7
  45. package/dist/neal/providers/git-config-isolation.js +83 -0
  46. package/dist/neal/providers/openai-codex.js +102 -204
  47. package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
  48. package/dist/neal/providers/openai-compatible.js +1079 -403
  49. package/dist/neal/providers/openrouter-routing.js +68 -0
  50. package/dist/neal/providers/pricing.js +124 -0
  51. package/dist/neal/providers/rate-card.js +2275 -0
  52. package/dist/neal/providers/registry.js +6 -17
  53. package/dist/neal/providers/telemetry.js +4 -0
  54. package/dist/neal/providers/types.js +12 -0
  55. package/dist/neal/resume-decision.js +7 -6
  56. package/dist/neal/resume-planner.js +9 -0
  57. package/dist/neal/retrospective.js +36 -7
  58. package/dist/neal/review-debt.js +30 -0
  59. package/dist/neal/review-findings/provider.js +17 -29
  60. package/dist/neal/review.js +17 -1
  61. package/dist/neal/run-metrics.js +74 -9
  62. package/dist/neal/state-invariants.js +11 -3
  63. package/dist/neal/state-views.js +27 -5
  64. package/dist/neal/state.js +59 -15
  65. package/dist/neal/status.js +28 -1
  66. package/dist/neal/support.js +3 -3
  67. package/docs/README.md +22 -18
  68. package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
  69. package/docs/architecture.md +79 -83
  70. package/docs/automation.md +11 -11
  71. package/docs/compat.md +91 -61
  72. package/docs/compatible-models.md +145 -110
  73. package/docs/demo.md +15 -5
  74. package/docs/maintenance.md +74 -32
  75. package/docs/plan-format.md +61 -33
  76. package/docs/prompt-evals.md +96 -0
  77. package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
  78. package/docs/providers.md +330 -283
  79. package/docs/release.md +43 -40
  80. package/docs/review-convergence.md +246 -0
  81. package/docs/state-machine.md +61 -59
  82. package/docs/storage.md +40 -31
  83. package/docs/troubleshooting.md +32 -29
  84. package/examples/compat/add-edit-verify/PLAN.md +2 -0
  85. package/examples/compat/add-edit-verify/broken.diff +2 -2
  86. package/examples/compat/add-edit-verify/good.diff +2 -2
  87. package/examples/compat/add-edit-verify/src/add.js +1 -1
  88. package/examples/compat/manifest.json +25 -29
  89. package/examples/compat/sum-grep-edit/PLAN.md +33 -0
  90. package/examples/compat/sum-grep-edit/broken.diff +12 -0
  91. package/examples/compat/sum-grep-edit/good.diff +12 -0
  92. package/examples/compat/sum-grep-edit/package.json +5 -0
  93. package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
  94. package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
  95. package/examples/issue-triage-js/README.md +0 -15
  96. package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
  97. package/examples/reviewer-eval/clean-doc/change.diff +9 -0
  98. package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
  99. package/examples/reviewer-eval/clean-extract/change.diff +13 -0
  100. package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
  101. package/examples/reviewer-eval/dropped-error/change.diff +15 -0
  102. package/examples/reviewer-eval/manifest.json +45 -0
  103. package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
  104. package/examples/reviewer-eval/off-by-one/change.diff +11 -0
  105. package/{config.yml → neal.yml} +1 -1
  106. package/package.json +20 -15
  107. package/dist/neal/providers/generic-agentic.js +0 -1238
  108. package/docs/comparison.md +0 -105
  109. package/examples/compat/is-even-add-test/PLAN.md +0 -30
  110. package/examples/compat/is-even-add-test/broken.diff +0 -11
  111. package/examples/compat/is-even-add-test/good.diff +0 -11
  112. package/examples/compat/is-even-add-test/package.json +0 -5
  113. package/examples/compat/is-even-add-test/src/is-even.js +0 -3
  114. package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
  115. package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
  116. package/examples/compat/reverse-grep-edit/broken.diff +0 -12
  117. package/examples/compat/reverse-grep-edit/good.diff +0 -12
  118. package/examples/compat/reverse-grep-edit/package.json +0 -5
  119. package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
  120. 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
+ }
@@ -0,0 +1,124 @@
1
+ // Shared pricing type and rate math for token-only provider adapters
2
+ // (openai-compatible and openai-codex). Those adapters report
3
+ // token counts but not dollars. Cost is resolved per bucket in this order:
4
+ // operator-configured per-million rates under
5
+ // `providers.openai_compatible.pricing` (the override tier), else the vendored
6
+ // published rate card (`rate-card.ts`, keyed by exact model slug), else
7
+ // tokens-only (`null` — Neal never invents dollars). The Claude adapter does
8
+ // not use this: it passes through the provider-reported `total_cost_usd`
9
+ // instead, and that provider-reported cost always wins upstream (it never
10
+ // reaches `resolveRateCost`).
11
+ import { RATE_CARD } from './rate-card.js';
12
+ // Same coercion as `numberValue` in run-metrics.ts: non-number or non-finite
13
+ // values normalize to 0 so the arithmetic never yields NaN.
14
+ function num(value) {
15
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
16
+ }
17
+ /**
18
+ * Rate-compute the USD cost of one turn's token usage.
19
+ *
20
+ * Field-to-rate mapping (kept explicit so the math is reproducible from the
21
+ * source alone):
22
+ *
23
+ * - `totalInput = input_tokens + inputTokens` — the reported prompt/input
24
+ * count. For OpenAI-compatible Chat Completions this count is *inclusive of*
25
+ * cached tokens (`prompt_tokens` already contains
26
+ * `prompt_tokens_details.cached_tokens`; the AI SDK surfaces these as
27
+ * `inputTokens` and `cachedInputTokens`).
28
+ * - `cachedInput = cached_input_tokens + cachedInputTokens +
29
+ * cache_read_input_tokens + cacheReadInputTokens` — tokens billed at the
30
+ * cached rate.
31
+ * - `billedUncachedInput = max(0, totalInput - cachedInput)` — cached tokens
32
+ * are subtracted from the inclusive total so a cached token is billed once,
33
+ * at the cached rate, never also at the full input rate. The `max(0, ...)`
34
+ * clamp is the defined handling for inconsistent counts (cached reported
35
+ * greater than total): treat the excess as fully cached rather than emitting a
36
+ * negative term.
37
+ * - `output = output_tokens + outputTokens` — already includes reasoning
38
+ * tokens for these providers, so reasoning output is not added separately.
39
+ * - Cache-creation tokens (`cache_creation_input_tokens`) are an Anthropic-only
40
+ * concept billed via provider-reported cost, not by these rates, so they are
41
+ * intentionally excluded here.
42
+ *
43
+ * Returns 0 (never NaN) when no tokens are present.
44
+ */
45
+ export function computeRateCostUsd(usage, pricing) {
46
+ const value = (usage && typeof usage === 'object' && !Array.isArray(usage)
47
+ ? usage
48
+ : {});
49
+ const totalInput = num(value.input_tokens) + num(value.inputTokens);
50
+ const cachedInput = num(value.cached_input_tokens) +
51
+ num(value.cachedInputTokens) +
52
+ num(value.cache_read_input_tokens) +
53
+ num(value.cacheReadInputTokens);
54
+ const billedUncachedInput = Math.max(0, totalInput - cachedInput);
55
+ const output = num(value.output_tokens) + num(value.outputTokens);
56
+ return ((billedUncachedInput / 1e6) * pricing.inputPerMillion +
57
+ (cachedInput / 1e6) * pricing.cachedInputPerMillion +
58
+ (output / 1e6) * pricing.outputPerMillion);
59
+ }
60
+ const PER_TOKEN_TO_PER_MILLION = 1e6;
61
+ function isFiniteNonNegativeNumber(value) {
62
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
63
+ }
64
+ /**
65
+ * Resolve a model slug to per-million pricing from the vendored rate card.
66
+ *
67
+ * Exact-match only: the slug is looked up (after trimming surrounding
68
+ * whitespace) as a literal key. No provider prefix is stripped and there is no
69
+ * basename fallback, so a slash-qualified slug like `local/gpt-5.5`,
70
+ * `azure/<deployment>`, or a finetune whose basename collides with a listed
71
+ * model does NOT inherit that model's price — pricing an unlisted slug would
72
+ * invent dollars, which the never-invent contract forbids. Provider-qualified
73
+ * slugs are priced only when the card carries that exact key (LiteLLM lists many
74
+ * `vendor/model` keys directly).
75
+ *
76
+ * Returns null for a non-string / empty / whitespace-only model, for an unknown
77
+ * slug, and for a card entry whose input or output cost is not a finite
78
+ * non-negative number (a partial entry is a miss, never a partial rate). The
79
+ * per-token rates are converted to per-million. The cached rate uses
80
+ * `cacheReadInputTokenCost` when it is a finite non-negative number, else falls
81
+ * back to the input rate (no published cache discount means cached reads are
82
+ * billed at the input rate — never free, never an invented discount).
83
+ */
84
+ export function lookupCardPricing(model, card = RATE_CARD) {
85
+ if (typeof model !== 'string') {
86
+ return null;
87
+ }
88
+ const key = model.trim();
89
+ if (key === '') {
90
+ return null;
91
+ }
92
+ const entry = card[key];
93
+ if (entry === undefined) {
94
+ return null;
95
+ }
96
+ if (!isFiniteNonNegativeNumber(entry.inputCostPerToken) || !isFiniteNonNegativeNumber(entry.outputCostPerToken)) {
97
+ return null;
98
+ }
99
+ const cachedPerToken = isFiniteNonNegativeNumber(entry.cacheReadInputTokenCost)
100
+ ? entry.cacheReadInputTokenCost
101
+ : entry.inputCostPerToken;
102
+ return {
103
+ inputPerMillion: entry.inputCostPerToken * PER_TOKEN_TO_PER_MILLION,
104
+ cachedInputPerMillion: cachedPerToken * PER_TOKEN_TO_PER_MILLION,
105
+ outputPerMillion: entry.outputCostPerToken * PER_TOKEN_TO_PER_MILLION,
106
+ };
107
+ }
108
+ /**
109
+ * Resolve a turn's rate-computed cost for a single provider/role bucket.
110
+ *
111
+ * Resolution order per bucket: operator-configured `configPricing` (the
112
+ * override tier) beats the vendored card (`lookupCardPricing(model)`). Returns
113
+ * null when neither yields pricing (tokens-only). When pricing is found, returns
114
+ * the flat base-tier computation from `computeRateCostUsd` tagged
115
+ * `costSource: 'rate'`; no tiered/threshold logic is applied, so a card-listed
116
+ * model is priced at its published base rates regardless of prompt length.
117
+ */
118
+ export function resolveRateCost(args) {
119
+ const pricing = args.configPricing ?? lookupCardPricing(args.model, args.card ?? RATE_CARD);
120
+ if (!pricing) {
121
+ return null;
122
+ }
123
+ return { costUsd: computeRateCostUsd(args.usage, pricing), costSource: 'rate' };
124
+ }