@navels/neal 0.2.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 (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 +18 -19
  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
@@ -1,113 +1,109 @@
1
1
  # Architecture
2
2
 
3
- A one-page map of how neal is built. The deep contracts live in the linked docs;
4
- this page is the synthesis how the run loop, the persisted state, the provider
5
- registry, and the prompt layer fit together.
6
-
7
- ```
8
- plan.md
9
-
10
- ┌─────▼─────────────────────────────────────────────┐
11
- │ Orchestrator (src/neal/orchestrator.ts + phases/) │
12
- │ │
13
- │ plan refinement ─▶ per-scope coder/reviewer loop │
14
- │ │ │ │
15
- │ │ ├─ split-plan (subdivide)
16
- │ │ └─ blocked recovery (adjudicated)
17
- │ ▼ ▼ │
18
- │ final-completion review ─▶ squash │
19
- └───────┬─────────────────────────┬───────────────────┘
20
- │ │
21
- roles ◀──────┤ ├──────▶ persisted state
22
- Provider │ │ .neal/ ledgers
23
- registry ▼ ▼ (run + queue)
24
- (planner / coder / reviewer) prompt specs + structured rounds
25
- ```
3
+ neal drives a plan document through a deterministic state machine with
4
+ planning and execution phases using coding agents in planner, coder, and reviewer roles.
5
+ The planning phase has the planner and reviewer revise the plan until
6
+ it has a valid execution shape, logical scopes, and a repository-grounded
7
+ implementation approach at a level a person can still review. File-level
8
+ discovery and local implementation choices stay in the execution phase.
9
+
10
+ During the execution phase, the coder implements a scope of work
11
+ with a fresh context, commits, and the reviewer checks the work. Any findings
12
+ are fed back to the coder and addressed. This coder/reviewer loop continues
13
+ until the reviewer is satisfied and execution proceeds to the next scope.
14
+ Once all scopes are complete, the coder and reviewer make a final pass to
15
+ review all the commits together. Finally, neal squashes all the work into a single commit.
16
+
17
+ Each role (planner, coder, reviewer) has its own prompt-driven behavior
18
+ and is backed by whatever provider (Anthropic, Codex, OpenRouter) and
19
+ model you choose. Run state is saved under `.neal/` so that a run can be
20
+ easily resumed without losing work in progress.
26
21
 
27
22
  ## 1. The run loop (orchestrator)
28
23
 
29
24
  `src/neal/orchestrator.ts` drives a deterministic state machine over a plan
30
25
  document. The phases live in `src/neal/orchestrator/phases/`:
31
26
 
32
- - **planning** refines the authored plan into an executable shape
33
- (`plan-refinement.ts`, `plan-validation.ts`; see
34
- [plan-format.md](plan-format.md)) through its own planner/reviewer loop.
35
- - **coder** runs one scope with the coder role from a fresh context, then
27
+ - **planning:** uses its own planner/reviewer loop to refine the plan you wrote
28
+ into a human-reviewable shape neal can execute,
29
+ decides whether it is `one_shot`, `multi_scope`, or `multi_scope_unknown`,
30
+ and records that choice in the plan (`plan-refinement.ts` and
31
+ `plan-validation.ts`). See [plan-format.md](plan-format.md).
32
+ - **coder:** runs one scope with the coder role from a fresh context, then
36
33
  commits.
37
- - **review** hands the committed diff to the read-only reviewer role; findings
38
- route back to the coder until accepted.
39
- - **recovery** interactive/unattended blocked recovery is the single chokepoint
40
- every block class funnels through, fronted by a bounded, read-only **blocked
41
- adjudicator** (`adjudicator/blocked-adjudicator.ts`) that triages coder-blocked
42
- signals, reviewer `review_stuck` deadlocks, and the split-plan invalid-payload
43
- block. Under unattended runs it autonomously resolves a narrow recoverable class
44
- (and escalates genuine walls); under attended runs it surfaces its verdict as
45
- advice and yields.
46
-
47
- After every scope is accepted, a **final-completion review**
34
+ - **review:** hands the committed diff to the read-only reviewer. Its findings
35
+ go back to the coder until the reviewer accepts the change.
36
+ - **recovery:** when a run gets stuck, the consultant is engaged. This is a role
37
+ backed by the reviewer provider/model (`adjudicator/consultant.ts`):
38
+ it can inspect the run but never change it, and it gets only a limited number
39
+ of tries. There are three kinds of stuck:
40
+ the coder reports it can't proceed, the coder and reviewer keep going back and
41
+ forth without resolution, or a step that tried to break a
42
+ scope into a smaller plan produced a plan that wasn't valid. When the fix is
43
+ small and safe, the consultant returns a directive that neal applies, and the
44
+ run keeps going. Otherwise the run stops and waits for you, or fails cleanly
45
+ if it's running unattended.
46
+
47
+ After every scope is accepted, a final-completion review
48
48
  (`final-completion-review.ts`) checks the whole plan, then neal squashes the run
49
- into a clean commit range.
49
+ into a clean commit.
50
50
 
51
51
  ## 2. State and persistence
52
52
 
53
- Every run is **crash-safe and resumable** because all state is persisted under
54
- the project-local `.neal/` directory ([storage.md](storage.md)) via atomic writes
55
- (`atomic-write.ts`). Two ledgers:
53
+ Every run is crash-safe and resumable, because all state is saved under the
54
+ project-local `.neal/` directory ([storage.md](storage.md)) with atomic writes
55
+ (`atomic-write.ts`). Two files hold that state:
56
56
 
57
- - **Run state** (`RUN_STATE.json`) the phase/status state machine for a single
58
- run, with documented invariants ([state-machine.md](state-machine.md)).
59
- - **Plan queue** (`plan-queue.ts`) the ordered set of scopes and their
60
- completion.
57
+ - **Run state** (`RUN_STATE.json`): the phase and status of a single run, with
58
+ its invariants documented in [state-machine.md](state-machine.md).
59
+ - **Plan queue** (`plan-queue.ts`): the ordered list of scopes and which ones
60
+ are done.
61
61
 
62
- `neal resume` (`resume-decision.ts`, `resume-planner.ts`) reconstructs an
63
- interrupted run from these ledgers which is why a killed process never loses
64
- committed work. A per-run lock (`run-lock.ts`) prevents concurrent runs from
65
- colliding.
62
+ `neal resume` (`resume-decision.ts`, `resume-planner.ts`) rebuilds an interrupted
63
+ run from these files. A per-run lock (`run-lock.ts`) keeps two runs from colliding.
66
64
 
67
65
  ## 3. Providers and roles
68
66
 
69
- The three roles planner, coder, reviewer are bound to providers
70
- independently, so you can run different vendors/models per role. The provider
71
- **registry** (`src/neal/providers/registry.ts`) is where capabilities are
72
- declared and **enforced**:
67
+ The three roles (planner, coder, reviewer) are each bound to a provider on their
68
+ own, so you can run a different vendor or model for each one. The provider
69
+ registry (`src/neal/providers/registry.ts`) is where each provider's capabilities
70
+ are declared and enforced:
73
71
 
74
72
  - **Native adapters:** `openai-codex` and `anthropic-claude` wrap the vendors'
75
- agentic SDKs (which own their tool loop and sandbox).
76
- - **`generic-agentic`:** a neal-owned agentic loop over any OpenAI-compatible /
77
- OpenRouter model, with a jailed tool surface
78
- (`providers/generic-agentic-tools.ts`).
79
-
80
- The key invariant: **the reviewer role is read-only by construction.** Every
81
- provider definition is checked at registration
82
- (`assertStructuredAdvisorReadOnly`) to declare `write:false, shell:false` for its
83
- structured-advisor (reviewer) capability so a reviewer structurally cannot
84
- mutate the repo, regardless of prompt. See [providers.md](providers.md) and
85
- [SECURITY.md](../SECURITY.md) for the full trust model.
73
+ agentic SDKs, which own their own tool loop and sandbox.
74
+ - **`openai-compatible`:** a neal-owned agent loop over any OpenAI-compatible or
75
+ OpenRouter model. File tools are jailed to the repository, while the coder's
76
+ shell tool is unsandboxed (`providers/openai-compatible-tools.ts`).
77
+
78
+ The most important rule: the reviewer can't write, by construction. At
79
+ registration, every provider is checked (`assertStructuredAdvisorReadOnly`) to
80
+ confirm its reviewer capability declares `write: false` and `shell: false`. So a
81
+ reviewer can't change the repo, no matter what the prompt says. See
82
+ [providers.md](providers.md) and [SECURITY.md](../SECURITY.md) for the full trust
83
+ model.
86
84
 
87
85
  ## 4. Prompts and adjudication
88
86
 
89
- Each role's behavior is defined by versioned **prompt specs**
90
- (`src/neal/agents/`, [PROMPT_SPECS.md](PROMPT_SPECS.md)) and run through shared
91
- **structured rounds** (`agents/rounds.ts`) that force schema-validated output
92
- (`agents/schemas.ts`). The coder/reviewer adjudication loop is inventoried in
93
- [ADJUDICATOR_INVENTORY.md](ADJUDICATOR_INVENTORY.md). Structured output is
94
- schema-enforced (not loose JSON), which is also how `neal compat` attributes a
95
- model's failures to the model rather than to transport noise.
87
+ Each role's behavior comes from versioned prompt specs (`src/neal/prompts/`,
88
+ [prompt-specs.md](prompt-specs.md)). They run through shared structured rounds
89
+ (`agents/rounds.ts`) that force the model's output to match a schema
90
+ (`agents/schemas.ts`). The full coder/reviewer loop is listed in
91
+ [adjudicator-inventory.md](adjudicator-inventory.md).
96
92
 
97
93
  ## 5. Model qualification (`neal compat`)
98
94
 
99
- `neal compat` (`src/neal/commands/compat.ts`) is a self-contained harness that
100
- drives a candidate model through the planner/coder/reviewer roles on trivial
101
- bundled fixtures, classifies failures structurally (protocol vs structured-output
102
- vs behavior), and emits a PASS/FAIL matrix. It's how the dated
103
- [compatible-models.md](compatible-models.md) whitelist is produced, and it
104
- doubles as the live CI smoke for dependency bumps (see
95
+ `neal compat` (`src/neal/commands/compat.ts`) is a self-contained harness. It
96
+ runs a candidate model through the planner, coder, and reviewer roles on small
97
+ bundled fixtures, sorts any failures into clear kinds (protocol,
98
+ structured-output, or behavior), and prints a PASS/FAIL table. It's how the
99
+ [compatible-models.md](compatible-models.md) whitelist gets made, and it also
100
+ serves as the live CI smoke test when a dependency is bumped (see
105
101
  [maintenance.md](maintenance.md) and [compat.md](compat.md)).
106
102
 
107
103
  ## Where to go next
108
104
 
109
- - [plan-format.md](plan-format.md) the executable plan contract
110
- - [state-machine.md](state-machine.md) run/queue state invariants
111
- - [providers.md](providers.md) provider adapter contract + extension checklist
112
- - [PROMPT_SPECS.md](PROMPT_SPECS.md) prompt-spec ownership boundaries
113
- - [storage.md](storage.md) the `.neal/` storage contract
105
+ - [plan-format.md](plan-format.md): the executable plan contract
106
+ - [state-machine.md](state-machine.md): run/queue state invariants
107
+ - [providers.md](providers.md): provider adapter contract + extension checklist
108
+ - [prompt-specs.md](prompt-specs.md): prompt-spec ownership boundaries
109
+ - [storage.md](storage.md): the `.neal/` storage contract
@@ -1,12 +1,12 @@
1
- # Automation Contract
1
+ # Automation contract
2
2
 
3
- This document is the machine-facing contract for driving neal from scripts,
4
- CI, and benchmark harnesses.
3
+ The machine-facing contract for driving neal from scripts, CI, and benchmark
4
+ harnesses.
5
5
 
6
6
  Noninteractive harnesses should run `neal execute <plan.md>` for an already
7
- accepted executable plan, or `neal run <plan.md>` when Neal should refine and
7
+ accepted executable plan, or `neal run <plan.md>` when neal should refine and
8
8
  execute one or more plans as a serial queue. Use `neal resume --run <run-id>` to
9
- continue an interrupted writer run; pass `--message` only when status says the
9
+ continue an interrupted writer run. Pass `--message` only when status says the
10
10
  run is waiting for operator guidance. After each writer command, classify the
11
11
  result from the process exit code plus `neal status --json --run <run-id>` when
12
12
  a run id is known. Use `neal status --json --all` only for run discovery and
@@ -32,15 +32,15 @@ The stable JSON classification fields are:
32
32
  - `commits`, `squash`, and `patch` for patch selection. `patch` includes the
33
33
  default-submission decision, reason, base/head/range, source, commit count,
34
34
  changed-file count, changed files, and unavailable reason.
35
- - `build` for reproducibility: Neal package version, Neal source Git SHA when
35
+ - `build` for reproducibility: neal package version, neal source Git SHA when
36
36
  available, Node version, whether the values came from run `meta.json` or live
37
37
  fallback, and the persisted planner/coder/reviewer agent config.
38
38
 
39
39
  Default public patch submission is conservative. Submit automatically only when
40
- `patch.defaultSubmissionEligible` is true. Neal sets that only for clean
40
+ `patch.defaultSubmissionEligible` is true. neal sets that only for clean
41
41
  completed execute runs with a non-empty readable patch range. Successful squash
42
- metadata is preferred (`squash.originalBaseCommit..squash.replacementCommit`);
43
- otherwise completed unsquashed execute runs use
42
+ metadata is preferred (`squash.originalBaseCommit..squash.replacementCommit`).
43
+ Otherwise completed unsquashed execute runs use
44
44
  `commits.initialBaseCommit ?? commits.baseCommit` through `commits.finalCommit`.
45
45
  Failed, blocked, paused, running, provider-error, waiting, timed-out,
46
46
  malformed-squash, pending-squash, unreadable-range, and empty-patch runs can
@@ -54,9 +54,9 @@ run-local `RUN_NARRATIVE.md` path reported at
54
54
  diffs, prompts, provider responses, or provider payloads as benchmark traces.
55
55
 
56
56
  Writer command exit codes (`0`, `1`, `2`, `3`) are defined in the README's
57
- [Command Exit Codes](../README.md#command-exit-codes) section.
57
+ [Command exit codes](../README.md#command-exit-codes) section.
58
58
 
59
- Harnesses own wall-clock timeouts. Launch Neal in a process group, terminate
59
+ Harnesses own wall-clock timeouts. Launch neal in a process group, terminate
60
60
  that group on timeout, wait a short grace period, and force-kill the group if it
61
61
  does not exit. When a run id is known, call `neal status --json --run <run-id>`
62
62
  after termination and record the wrapper timeout as the primary classification.
package/docs/compat.md CHANGED
@@ -1,15 +1,15 @@
1
- # `neal compat` model compatibility smoke test
1
+ # `neal compat`: model compatibility smoke test
2
2
 
3
3
  `neal compat` answers one question per model: **can this OpenAI-compatible model
4
- drive neal's loop at all** as a coder, a reviewer, or a planner? It is a
4
+ drive neal's loop at all**, as a coder, a reviewer, or a planner? It is a
5
5
  **compatibility smoke test, not a performance benchmark**. There is no
6
6
  resolve-rate, no score, no ranking, and no real SWE-bench repo involved. The
7
- fixtures are deliberately trivial; any working agent should pass them. A failure
7
+ fixtures are deliberately trivial. Any working agent should pass them. A failure
8
8
  therefore means something is **fundamentally wrong** with the model's ability to
9
- operate neal's contract not that a task was hard.
9
+ operate neal's contract. It does not mean a task was hard.
10
10
 
11
11
  `neal compat` is fully self-contained in the neal repo. The only things an
12
- operator needs to qualify a model are the neal checkout plus an API key no
12
+ operator needs to qualify a model are the neal checkout plus an API key: no
13
13
  external benchmark assets, no dataset download, no remote runner.
14
14
 
15
15
  ## Compatibility, not skill
@@ -18,7 +18,7 @@ external benchmark assets, no dataset download, no remote runner.
18
18
  tool calls + structured output, no max-step loops, no unresolved operator
19
19
  blocks, a clean finalization) **and** solve a trivial bundled fixture.
20
20
  - **Not measured:** skill. No resolve-rate, no scores, no rankings, no real
21
- SWE-bench repos trivial fixtures only.
21
+ SWE-bench repos, trivial fixtures only.
22
22
  - **Outcome:** a binary **PASS / FAIL per (model, role)**, with a structural
23
23
  failure mode recorded for every FAIL.
24
24
 
@@ -30,42 +30,42 @@ external benchmark assets, no dataset download, no remote runner.
30
30
  role's provider can answer one small structured probe
31
31
  (`provider_check_payload`). It proves *connectivity and basic protocol*, not
32
32
  that the model can complete a full neal run. When the resolved **coder or
33
- reviewer** role uses a non-native (generic-agentic / openai-compatible)
34
- provider, `neal check` prints a one-line pointer: *"This is a generic-agentic
33
+ reviewer** role uses the non-native `openai-compatible`
34
+ provider, `neal check` prints a one-line pointer: *"This is an openai-compatible
35
35
  model - run `neal compat` to confirm it can drive the full loop."* Native
36
36
  adapters (`openai-codex`, `anthropic-claude`) do not get the pointer. The
37
37
  planner inherits the coder provider by default, so the coder check covers the
38
- common case; an explicitly configured non-native planner paired with native
38
+ common case. An explicitly configured non-native planner paired with native
39
39
  coder/reviewer is not separately flagged (run `neal compat --role planner`
40
40
  to qualify it).
41
41
  - **`neal compat`** runs the same structured pre-filter first (it reuses
42
42
  `check`'s `verifyConfiguredProviders`), then drives the model through complete
43
43
  neal runs against the bundled fixtures and produces a PASS/FAIL matrix.
44
44
 
45
- So: `neal check` is the cheap connectivity gate; `neal compat` is the full-loop
46
- qualification gate that `check` points generic-agentic users toward.
45
+ So `neal check` is the cheap connectivity gate. `neal compat` is the full-loop
46
+ qualification gate that `check` points openai-compatible users toward.
47
47
 
48
48
  ## Usage
49
49
 
50
50
  ```bash
51
- neal compat [--model <slug>] [--role coder|reviewer|planner|all] [--reference openai-codex|anthropic-claude|generic-agentic:<slug>] [--json]
51
+ neal compat [--model <slug>] [--role coder|reviewer|planner|all] [--reference openai-codex|anthropic-claude|openai-compatible:<slug>] [--json]
52
52
  ```
53
53
 
54
54
  - `--role` (default `all`) selects which role(s) to test: `coder`, `reviewer`,
55
55
  `planner`, or `all`.
56
- - `--model <slug>` runs the slug on the **`generic-agentic`** provider in the
57
- candidate role (provider forced to `generic-agentic`, any configured effort
56
+ - `--model <slug>` runs the slug on the **`openai-compatible`** provider in the
57
+ candidate role (provider forced to `openai-compatible`, any configured effort
58
58
  dropped, so the slug drives a clean OpenRouter call). When omitted, the
59
59
  candidate role uses its configured provider/model unchanged.
60
60
  - `--reference <id>` names the provider for the **non-candidate** roles so a
61
61
  FAIL is attributable to the candidate in the tested role, not to a weak (and
62
- possibly flaky) partner. Accepted forms: a native provider id `openai-codex`
63
- (the default when omitted while `--model` is set) or `anthropic-claude` or
64
- `generic-agentic:<openrouter-model>` to run the reference roles on an
62
+ possibly flaky) partner. Accepted forms: a native provider id (`openai-codex`,
63
+ the default when omitted while `--model` is set, or `anthropic-claude`), or
64
+ `openai-compatible:<openrouter-model>` to run the reference roles on an
65
65
  OpenRouter model. A bare model slug is rejected. Native reference roles run on
66
66
  that adapter's default model (`gpt-5.5` for `openai-codex`, `claude-opus-4-8`
67
67
  for `anthropic-claude`). Prefer a native reference for whitelist
68
- qualification; see [compatible-models.md](compatible-models.md) for why
68
+ qualification. See [compatible-models.md](compatible-models.md) for why
69
69
  OpenRouter references proved unreliable as qualification partners. When
70
70
  **neither** `--model` nor `--reference` is given, every role stays on its
71
71
  configured provider/model (pure pass-through).
@@ -74,8 +74,10 @@ neal compat [--model <slug>] [--role coder|reviewer|planner|all] [--reference op
74
74
 
75
75
  The command resolves its base config via the same writer-provider resolver the
76
76
  other commands use, then derives a candidate config by cloning it and routing the
77
- tested role onto `generic-agentic` (when `--model` is set) and the non-candidate
78
- roles onto the native reference adapter (when either flag is set). It runs
77
+ tested role onto `openai-compatible` (when `--model` is set) and the non-candidate
78
+ roles onto the resolved reference provider: a native adapter at its default
79
+ model, or `openai-compatible` at the slug from the
80
+ `openai-compatible:<slug>` form (when either flag is set). It runs
79
81
  against **throwaway git copies** of the bundled fixtures and **never mutates** a
80
82
  committed fixture under `examples/compat/`. The process exits non-zero when the
81
83
  overall result is FAIL.
@@ -95,18 +97,29 @@ fixtures are:
95
97
  | Fixture | Roles | Contract surface |
96
98
  | --- | --- | --- |
97
99
  | `add-edit-verify` | coder, reviewer | edit + verify + commit |
98
- | `reverse-grep-edit` | coder, reviewer | read/grep-then-edit |
99
- | `is-even-add-test` | coder, reviewer | add-a-test |
100
+ | `sum-grep-edit` | coder, reviewer | read/grep-then-edit |
100
101
  | `plan-greeting` | planner | emit a `one_shot` plan |
101
102
 
103
+ Each fixture maps to a distinct way neal *directs* a model. A fixture that only
104
+ varies the coding task (not the direction shape) would measure skill, which this
105
+ command deliberately does not. That is why there are exactly three.
106
+
102
107
  Each coder fixture bundles buggy source, a test that fails against the buggy
103
108
  source, and a `PLAN.md` declaring `executionShape: one_shot` that describes the
104
109
  smallest complete fix. The reviewer fixtures reuse the same projects, each paired
105
110
  with two pre-made unified diffs (`good.diff`, `broken.diff`). The planner fixture
106
111
  provides an issue-statement seed (`ISSUE.md`) and no reviewer diffs.
107
112
 
113
+ Fixture source and test files deliberately avoid any comment or claim that
114
+ becomes false once `good.diff` applies (`// BUG: …`, "intentionally buggy",
115
+ a stale `// TODO:`). A reviewer worktree is the project plus the applied diff, so
116
+ such text is a real defect in the material under review and draws legitimate
117
+ blocking findings on the known-good diff. The one exception is each fixture's
118
+ `PLAN.md`, which describes the pre-fix state by definition and carries a fixture
119
+ note saying so.
120
+
108
121
  `examples/issue-triage-js` remains an optional tier-2 "real-ish" check above the
109
- trivial gate; `neal compat` does not require it.
122
+ trivial gate. `neal compat` does not require it.
110
123
 
111
124
  ### Manifest schema
112
125
 
@@ -132,27 +145,33 @@ trivial gate; `neal compat` does not require it.
132
145
  A planner fixture sets `"roles": ["planner"]` and provides `"issuePrompt"`
133
146
  instead of `reviewer`. Any fixture carrying the `reviewer` role must also define
134
147
  `verifyCommand`: reviewer fixtures reuse the coder project's command as the
135
- deterministic good/broken oracle `goodDiff` must make it pass, `brokenDiff`
148
+ deterministic good/broken oracle: `goodDiff` must make it pass, `brokenDiff`
136
149
  must make it fail. The `referenceFix` is used only by the structural fixture
137
150
  self-test (`test/compat-fixtures.test.ts`), never by a model run.
138
151
 
139
152
  ## Definition of PASS (per model, per role)
140
153
 
141
154
  Across **every** fixture for the role, the model must (1) **complete the run
142
- cleanly** the run reaches `status: 'done'` (not `'failed'` / `'blocked'`), does
143
- not emit an `unattended.block_unresolved` event, and does not exceed its
144
- step/round budget — **and** (2) produce the **role-correct result**:
155
+ cleanly** and (2) produce the **role-correct result**. A clean run reaches
156
+ `status: 'done'` (not `'failed'` / `'blocked'`), does not emit an
157
+ `unattended.block_unresolved` event, and does not exceed its step/round budget.
158
+ The role-correct result is:
145
159
 
146
160
  - **coder:** `finalState.status === 'done'` **and** the fixture's
147
161
  `verifyCommand` exits `0` in the throwaway worktree (the failing test now
148
162
  passes).
149
163
  - **reviewer:** the review-findings loop converges (`outcome === 'accepted'`) for
150
- **both** diffs, and the converged findings discriminate them: **zero** blocking
151
- findings for the good diff and **≥1** blocking finding (a `ReviewFindingItem`
152
- with `severity === 'blocking'`) for the broken diff. `outcome` alone is not
153
- sufficient it is `'accepted'` whenever the findings artifact converges,
154
- including a zero-finding artifact so the good/broken verdict is scored on
155
- `draft.findings` severities.
164
+ **both** diffs, and the converged findings **discriminate** them. Counting
165
+ blocking findings (a `ReviewFindingItem` with `severity === 'blocking'`) per
166
+ diff, PASS iff `blocking(broken) >= 1` **and** `blocking(good) < blocking(broken)`.
167
+ Severity calibration is **not** graded: a reviewer may raise blocking findings
168
+ on the good diff and still PASS, as long as the broken diff draws strictly more.
169
+ A reviewer that blocks on neither diff, or equally on both, FAILs. `outcome`
170
+ alone is not sufficient (it is `'accepted'` whenever the findings artifact
171
+ converges, including a zero-finding artifact), so the verdict is scored on
172
+ `draft.findings` severities. The verdict is a **pair** verdict: both the good
173
+ and broken cells carry the same `pass`, and each carries its own
174
+ `blockingCount`.
156
175
  - **planner** (secondary): the emitted plan document validates via
157
176
  `validatePlanDocument` as a schema-conformant `one_shot` plan.
158
177
 
@@ -164,44 +183,48 @@ penalized for a halt that a human operator would simply have resumed.
164
183
 
165
184
  ## Failure-mode taxonomy
166
185
 
167
- Each FAIL records a mode derived from **structural run state** — `finalState.status`,
168
- emitted run events, `ReviewFindingsOutcome`, and `validatePlanDocument` — never
169
- from substring-matching model prose. When more than one applies, the earliest in
170
- this list (most specific cause first) is recorded:
186
+ Each FAIL records a mode derived from **structural run state**
187
+ (`finalState.status`, emitted run events, `ReviewFindingsOutcome`, and
188
+ `validatePlanDocument`), never from substring-matching model prose. When more than
189
+ one applies, the earliest in this list (most specific cause first) is recorded:
171
190
 
172
- - `protocol` the `verifyConfiguredProviders` pre-filter threw for the candidate
173
- (it could not emit one valid `provider_check_payload`); fixtures were skipped.
174
- - `provider_failed` the run failed with a model-attributable provider error
191
+ - `protocol`: the `verifyConfiguredProviders` pre-filter threw for the candidate
192
+ (it could not emit one valid `provider_check_payload`). Fixtures were skipped.
193
+ - `provider_failed`: the run failed with a model-attributable provider error
175
194
  event (transport/auth/transient/other) rather than a clean completion, **except**
176
195
  a provider error whose `errorKind` is a structured-output kind, which is recorded
177
196
  as `structured_output` (below). This is also the bucket for a **writer** run that
178
- ends `status: 'failed'` without a more specific structural signal including
197
+ ends `status: 'failed'` without a more specific structural signal, including
179
198
  step/round-budget exhaustion, which the current runtime does not surface to compat
180
199
  as a distinct cap event, so writer step-cap exhaustion is reported here rather than
181
200
  as `max_step_loop`.
182
- - `block_unresolved` the run emitted `unattended.block_unresolved` (the model
183
- escalated to an operator block unattended mode could not resolve within budget),
184
- or the review loop's outcome was `'blocked'`.
185
- - `max_step_loop` the **reviewer** loop's outcome was `'cap_reached'` (the
201
+ - `block_unresolved`: the run emitted `unattended.block_unresolved` (the model
202
+ escalated to an operator block that unattended mode could not resolve within
203
+ budget), or the review loop's outcome was `'blocked'`.
204
+ - `max_step_loop`: the **reviewer** loop's outcome was `'cap_reached'` (the
186
205
  review-findings convergence cap was hit). Writer (coder/planner) step-cap
187
206
  exhaustion is not separately distinguishable under the current runtime and is
188
207
  classified as `provider_failed` (above).
189
- - `wrong_or_empty_output` — coder: the run reached `done` but `verifyCommand`
190
- exited non-zero (or the diff was empty); reviewer: the verdict was wrong
191
- (accepted the broken diff with no blocking finding, or flagged the good diff
192
- with a blocking finding); planner: the emitted plan was not a schema-conformant
193
- `one_shot` plan.
194
- - `structured_output` the model could not produce or honor **schema-enforced**
208
+ - `wrong_or_empty_output`. Coder: the run reached `done` but `verifyCommand`
209
+ exited non-zero (or the diff was empty); reviewer: the findings did not
210
+ discriminate the pair (no blocking finding on the broken diff, or the good diff
211
+ drew at least as many blocking findings as the broken one), recorded on both
212
+ cells of the pair; planner: the emitted plan was not a schema-conformant
213
+ `one_shot` plan. When one diff's review **failed** outright, the pair verdict is
214
+ unscoreable: that diff keeps its own failure mode and the other cell fails with
215
+ the same mode and a `pair unscoreable: …` detail, so the systematic cause is
216
+ what gets attributed.
217
+ - `structured_output`: the model could not produce or honor **schema-enforced**
195
218
  JSON. This covers a reviewer/planner round whose payload failed schema validation
196
219
  (surfaced as the corresponding round error) **and** a coder/reviewer provider
197
220
  error whose `errorKind` is `structured_output_invalid` or
198
- `structured_output_missing` e.g. an OpenRouter (generic-agentic) gateway that
221
+ `structured_output_missing`, e.g. an OpenRouter (openai-compatible) gateway that
199
222
  rejects the `type: 'json_schema'` request (HTTP 400) or returns a missing/invalid
200
223
  object. This is distinct from `provider_failed` (transport/auth/other transport
201
224
  errors) and from `wrong_or_empty_output` (a syntactically valid verdict/output
202
225
  that was substantively wrong). Both the coder (`classifyWriterFailure`) and
203
226
  reviewer (`classifyReviewerThrownFailure`) paths attribute these kinds here.
204
- - `finalization_error` the run failed during finalization/artifact writing, or
227
+ - `finalization_error`: the run failed during finalization/artifact writing, or
205
228
  the run produced no final document where one was expected.
206
229
 
207
230
  When no structural signal is conclusive, `provider_failed` is recorded with the
@@ -220,20 +243,21 @@ whitelist (`docs/compatible-models.md`):
220
243
 
221
244
  ```jsonc
222
245
  {
223
- "schemaVersion": 1,
246
+ "schemaVersion": 2,
224
247
  "model": "deepseek/deepseek-chat", // candidate --model, or null when omitted
225
- "reference": null, // native provider id (openai-codex | anthropic-claude), or null when omitted
248
+ "reference": null, // the --reference value as given: a native provider id (openai-codex | anthropic-claude), openai-compatible:<slug>, or null when omitted
226
249
  "role": "all", // coder | reviewer | planner | all
227
- "candidateProviders": { // provider each role used as the candidate: "generic-agentic" per role when --model is set, else the configured provider
228
- "coder": "generic-agentic",
229
- "reviewer": "generic-agentic",
230
- "planner": "generic-agentic"
250
+ "candidateProviders": { // provider each role used as the candidate: "openai-compatible" per role when --model is set, else the configured provider
251
+ "coder": "openai-compatible",
252
+ "reviewer": "openai-compatible",
253
+ "planner": "openai-compatible"
231
254
  },
232
255
  "cells": [
233
256
  {
234
257
  "role": "coder", // coder | reviewer | planner
235
258
  "fixtureId": "add-edit-verify", // manifest id; "provider:<role>" for a protocol pre-filter FAIL
236
259
  "diffKind": null, // "good" | "broken" for reviewer cells, else null
260
+ "blockingCount": null, // blocking findings on this diff; null for non-reviewer or errored cells
237
261
  "pass": true,
238
262
  "failureMode": null, // one of the taxonomy modes when pass=false, else null
239
263
  "detail": null // human-readable explanation when pass=false, else null
@@ -251,6 +275,12 @@ whitelist (`docs/compatible-models.md`):
251
275
  }
252
276
  ```
253
277
 
278
+ No tooling gates on `schemaVersion`. It exists so a stored report's scoring
279
+ semantics are identifiable. Version `2` is the discrimination criterion described
280
+ above plus `blockingCount`. Version `1` reports scored the reviewer on zero
281
+ blocking findings for the good diff and retained no counts, so they cannot be
282
+ re-scored.
283
+
254
284
  A role roll-up is PASS iff it has at least one cell and every one of its cells
255
285
  passed. `overallPass` is true iff every targeted role roll-up passed. The process
256
286
  exits non-zero when `overallPass` is false.
@@ -260,10 +290,10 @@ exits non-zero when `overallPass` is false.
260
290
  Cheap by construction: trivial fixtures and cheap models. If qualifying a model
261
291
  gets expensive, the fixture set is too big.
262
292
 
263
- ## Phase B building the whitelist
293
+ ## Phase B: building the whitelist
264
294
 
265
295
  With a provider configured (OpenRouter **paid** slugs only, never `:free`) and a
266
296
  known-good `--reference`, run `neal compat --model <slug> --role all --json` for
267
297
  each candidate and fill the dated [`compatible-models.md`](compatible-models.md)
268
- matrix from the JSON output. Date and version the matrix model behavior drifts,
298
+ matrix from the JSON output. Date and version the matrix. Model behavior drifts,
269
299
  so re-run on version bumps.