@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
package/docs/release.md CHANGED
@@ -1,13 +1,12 @@
1
- # Release Process
1
+ # Release process
2
2
 
3
- This is the canonical tracked release guide for Neal. Releases are manual,
4
- versioned npm operations. Normal CI remains verification-only for pushes and
5
- pull requests; publishing is isolated to the GitHub Actions `Publish` workflow,
6
- which is triggered manually with `workflow_dispatch`.
3
+ Releases are manual, versioned npm operations. Normal CI only verifies pushes
4
+ and pull requests. The GitHub Actions `Publish` workflow handles publishing and
5
+ runs only through `workflow_dispatch`.
7
6
 
8
- ## Versioning Policy
7
+ ## Versioning policy
9
8
 
10
- Neal uses Semantic Versioning. `package.json.version` changes only in an
9
+ neal uses Semantic Versioning. `package.json.version` changes only in an
11
10
  intentional release-preparation pull request, not in ordinary implementation
12
11
  commits. Release versions are exact SemVer strings without a leading `v`, and
13
12
  an npm version must never be reused.
@@ -23,10 +22,10 @@ Examples:
23
22
  changing documented behavior.
24
23
  - Minor before `1.0.0`: `0.2.0` adds a feature or meaningful behavior change
25
24
  while the public contract is still settling.
26
- - Major: `1.0.0` establishes the stable public contract; later breaking
25
+ - Major: `1.0.0` establishes the stable public contract. Later breaking
27
26
  contract changes require `2.0.0`, `3.0.0`, and so on.
28
27
 
29
- ## SDK Dependency Policy
28
+ ## SDK dependency policy
30
29
 
31
30
  Direct coding-agent and AI SDK runtime dependencies must use exact SemVer specs
32
31
  in `package.json`. Do not use `latest`, caret ranges, tilde ranges, wildcards,
@@ -41,12 +40,12 @@ published CLI package:
41
40
 
42
41
  An SDK update should land as a normal dependency pull request that updates both
43
42
  `package.json` and `pnpm-lock.yaml`, runs normal CI, and records any provider
44
- compatibility findings. The resulting Neal release should match the user-facing
43
+ compatibility findings. The resulting neal release should match the user-facing
45
44
  impact: use a patch release for a compatibility fix that preserves documented
46
45
  behavior, a minor release for behavior changes before `1.0.0`, and a major
47
46
  release after `1.0.0` if a documented public contract breaks.
48
47
 
49
- ## Local Release Readiness
48
+ ## Local release readiness
50
49
 
51
50
  Prepare a release with a normal pull request that changes `package.json.version`
52
51
  to the intended exact SemVer version:
@@ -71,7 +70,7 @@ availability, and checks remote `v<version>` tag availability when an `origin`
71
70
  remote is configured. Package verification confirms the built CLI and packed
72
71
  tarball.
73
72
 
74
- ## Manual Publish Workflow
73
+ ## Manual publish workflow
75
74
 
76
75
  After the release-preparation pull request is merged and normal CI passes, run
77
76
  the `Publish` workflow manually from `main` with:
@@ -97,12 +96,12 @@ the real publish.
97
96
 
98
97
  The real-publish path is **staged**: the workflow runs `npm stage publish`,
99
98
  which places the version in a staged, not-publicly-available state. A
100
- maintainer then reviews and approves it with 2FA `npm stage list`,
99
+ maintainer then reviews and approves it with 2FA: `npm stage list`,
101
100
  `npm stage view <stage-id>` / `npm stage download <stage-id>`, and
102
101
  `npm stage approve <stage-id>` (or the npmjs.com UI). Nothing reaches `latest`
103
102
  without that human approval, so a compromised workflow cannot ship directly.
104
103
 
105
- ## First-Publish History
104
+ ## First-publish history
106
105
 
107
106
  `0.1.0` was published manually on 2026-07-12 with an interactive 2FA publish
108
107
  from a maintainer terminal: npm trusted publishing cannot be configured for a
@@ -110,26 +109,26 @@ package that has never been published, so the first publish had to
110
109
  authenticate directly. Every release after `0.1.0` goes through the `Publish`
111
110
  workflow and the staged flow above.
112
111
 
113
- ## Trusted Publishing Setup
112
+ ## Trusted publishing setup
114
113
 
115
114
  The publish workflow relies on trusted publishing/OIDC and intentionally has no
116
115
  npm-token fallback. It grants `id-token: write` for OIDC, keeps repository
117
116
  contents read-only, uses the `npm-publish` GitHub environment, and runs
118
117
  `npm stage publish` only on the real-publish path (staged publishing requires
119
- npm >= 11.15.0; the workflow upgrades npm accordingly).
118
+ npm >= 11.15.0, so the workflow upgrades npm accordingly).
120
119
 
121
120
  The npm-side configuration for `@navels/neal`:
122
121
 
123
122
  - Publishing access: **Require two-factor authentication and disallow tokens**.
124
- This blocks every traditional token permanently; trusted publishers are
123
+ This blocks every traditional token permanently. Trusted publishers are
125
124
  unaffected because they use OIDC, and staged approvals always require a
126
125
  maintainer's 2FA.
127
126
  - Trusted publisher: owner/repo `navels/neal`, workflow filename
128
127
  `publish.yml`, environment `npm-publish`, allowed action **`npm stage
129
- publish` only** (stage-only; plain `npm publish` is deliberately not
128
+ publish` only** (stage-only, plain `npm publish` is deliberately not
130
129
  granted).
131
130
 
132
- ## Release Boundaries
131
+ ## Release boundaries
133
132
 
134
133
  The current workflow checks that the remote `v<version>` tag is available, but
135
134
  it does not create tags or GitHub releases. Do not promise or perform release
@@ -0,0 +1,246 @@
1
+ # Plan-review convergence
2
+
3
+ Plan review either converges or the run fails. The reviewer is asked "is this
4
+ plan execution-ready?" fresh every round against a document that grows with each
5
+ fix, so every individually-defensible new finding forces another full revision
6
+ round. The only non-acceptance exits used to be terminal failures: reaching the
7
+ round cap, and an unattended coder block. That produced two failure modes in
8
+ real runs: long negotiations that keep re-litigating verification strength after
9
+ the plan is already correct, and runs that terminally fail when a coder
10
+ legitimately needs author input it must not fabricate.
11
+
12
+ The convergence policy addresses both failure modes as **deterministic
13
+ orchestration policy**, not prompt tuning. It never shortens a negotiation that
14
+ surfaced a genuine plan-correctness defect, and only stops burning rounds on
15
+ verification-hardening demands once the plan itself is correct. And a
16
+ coder-authored plan-stage block no longer terminally fails: it lands as a
17
+ recoverable blocked-with-reason state that an operator can answer via
18
+ `neal resume --message` (see
19
+ [Coder-authored plan-stage block recovery](#coder-authored-plan-stage-block-recovery)).
20
+
21
+ The core round policy is a single pure function,
22
+ `synthesizePlanReviewRoundFromFindings` in `src/neal/adjudicator/planning.ts`,
23
+ shared by the runtime (`runPlanReviewPhase`) and by the fixture replay harness
24
+ under `test/fixtures/plan-review-ledgers/` (see that directory's `SCHEMA.md`), so
25
+ the policy pinned by the committed ledgers is exactly the policy that runs.
26
+
27
+ ## Finding classes
28
+
29
+ Every plan-review finding carries a declared `findingClass`:
30
+
31
+ - **`plan_correctness`**: the plan would build the wrong thing, build an
32
+ unverifiable thing, or contains an impossible or self-contradictory
33
+ instruction. These always force a revision round (or block at the cap). The
34
+ auto-generated plan-structure findings default to `plan_correctness`.
35
+ - **`verification_hardening`**: the finding only demands strengthening *how the
36
+ plan will be verified* (more oracles, more pinning, more coverage of
37
+ already-specified behavior) without asserting the plan builds the wrong or an
38
+ unverifiable thing. Past the round threshold these can convert to recorded
39
+ debt instead of forcing another round.
40
+
41
+ The class has two deliberately different boundaries:
42
+
43
+ - **Reviewer-payload boundary (strict).** An *absent* class normalizes to
44
+ `plan_correctness`. A *present-but-invalid* class value is rejected exactly as
45
+ an invalid `severity` is: the sequential payload validator throws
46
+ `must be exactly one of: plan_correctness, verification_hardening`, so an
47
+ invalid class is a validation error, never a silent downgrade.
48
+ - **Persisted-state boundary (tolerant).** When hydrating a saved run,
49
+ `hydrateFinding` maps a present-but-unknown class to `plan_correctness` and
50
+ leaves an absent class `undefined` (execute-review findings never carry a
51
+ class and must stay unaffected). Older run states load without error.
52
+
53
+ The system-wide fail-safe does **not** depend on hydration filling absence: the
54
+ decision sites convert only an *explicit* `verification_hardening`, so any
55
+ absent/`undefined`/unknown class is always round-forcing (blocking).
56
+
57
+ ## The debt round threshold
58
+
59
+ The knob `neal.plan_review_debt_round_threshold` (default **3**, read via
60
+ `getPlanReviewDebtRoundThreshold(cwd)`) is the reviewer round at or past which a
61
+ novel verification-hardening finding may convert to debt instead of forcing a
62
+ round. Below the threshold, every blocking finding forces a revision round
63
+ regardless of class. Early rounds are for genuine convergence, not banking.
64
+
65
+ ## Disposition policy
66
+
67
+ For each round the policy partitions the merged open blocking findings
68
+ (`classifyPlanReviewConvergence`) into `debtConvertible` and `roundForcing`:
69
+
70
+ Pre-cap, a finding is `debtConvertible` only when **all** of:
71
+
72
+ - its class is exactly `verification_hardening`
73
+ - it is a **first occurrence** of its canonical (the minimum round across all
74
+ merged findings sharing that canonical equals the current round)
75
+ - the current round is `>= threshold`.
76
+
77
+ Everything else is `roundForcing`: any `plan_correctness`/absent/unknown class,
78
+ **any repeat occurrence** of an existing canonical (a re-raised point still earns
79
+ a dedicated round pre-cap), or any below-threshold round.
80
+
81
+ From that partition:
82
+
83
+ - **Arrival-time conversion.** A `debtConvertible` finding banks as `deferred`
84
+ plan-review debt the moment it arrives, even when a co-occurring
85
+ `plan_correctness` or repeat finding forces the round, so novelty-bounded
86
+ hardening asks never extend the round count.
87
+ - **Accept-with-recorded-reservations landing.** When every open blocking
88
+ finding is `debtConvertible` and none is `roundForcing`, the round lands
89
+ accepted (`landAcceptedWithDebt`). It reuses the existing acceptance
90
+ transition (`accept_plan` → `done` for top-level plan review,
91
+ `accept_derived_plan` for a derived-plan review), so no adjudication-spec
92
+ outcome is added.
93
+
94
+ Branch precedence in `resolvePlanReviewDisposition` is strict:
95
+
96
+ 1. convergence block (reopen / stall) →
97
+ 2. round-forcing blocking findings →
98
+ 3. accept-with-debt →
99
+ 4. open non-blocking findings →
100
+ 5. clean acceptance.
101
+
102
+ Because `roundForcing` outranks `landAcceptedWithDebt`, a single open
103
+ plan-correctness or repeat blocker keeps the round forced even when convertible
104
+ hardening findings are present alongside it.
105
+
106
+ ## Cap behavior
107
+
108
+ At the round cap (`round >= roundLimit`) there are no rounds left to force, so the
109
+ convertible predicate becomes **class-only**: *every* open
110
+ `verification_hardening` finding converts regardless of first-occurrence or
111
+ threshold. The cap contract:
112
+
113
+ - If the convergence block did **not** fire and *every* open blocking finding is
114
+ `verification_hardening`, the run lands accepted-with-debt (all convert).
115
+ - If any open blocking finding is `plan_correctness` (or absent/unknown class),
116
+ the run terminal-blocks with the existing max-rounds reason.
117
+
118
+ The convergence block keeps precedence at the cap too: a re-opened
119
+ (`getReopenedCanonical`, ≥3 blocking rounds of one canonical) or stalled
120
+ (`hasRepeatedUnresolvedBlockingCanonicals`) finding, *even hardening-class*,
121
+ terminal-blocks rather than converting. So a re-litigated-but-not-reopened
122
+ hardening point converts at the cap, but a genuinely stuck one still blocks.
123
+
124
+ ## Plan-review debt: two fields, two lifecycles
125
+
126
+ Debt is modeled as a **canonical-keyed projection of the current findings**
127
+ (`toPlanReviewDebt`), never an accumulator: for each canonical whose
128
+ latest-round finding is a `deferred` `verification_hardening` finding, exactly
129
+ one debt item is emitted (latest round wins, `originRound` = that finding's
130
+ round). A banked canonical that later reopens, is fixed, or is rejected simply
131
+ drops out of the projection. The lifecycle is automatic, with no stale or
132
+ duplicate entries and no ad-hoc removal rule.
133
+
134
+ There are two distinct top-level state fields:
135
+
136
+ - **`planReviewDebt`**: the **current-negotiation** projection. Recomputed as
137
+ `toPlanReviewDebt(mergedFindings)` everywhere the current findings change
138
+ (`synthesizePlanReviewRound` / `runPlanReviewPhase` and
139
+ `runPlanningResponsePhase`). This is what a top-level plan run carries to the
140
+ queue on completion.
141
+ - **`inheritedPlanReviewDebt`**: **durable, write-once**. Seeded only at init
142
+ from the queue handoff and **never recomputed by any plan-review phase**.
143
+
144
+ Two fields are required because an *execution* child re-enters plan review: the
145
+ execute runnable-phase registry includes `reviewer_plan`,
146
+ `coder_plan_response`, and `coder_plan_optional_response`, so
147
+ `runPlanReviewPhase` runs inside the execution child during split-plan recovery.
148
+ If a single recomputed field were both seeded from the inherited debt and
149
+ recomputed as `toPlanReviewDebt(mergedFindings)`, that derived-plan review would
150
+ project only the derived plan's own findings and **erase** the inherited
151
+ top-level debt before the next scope reviewer saw it. Keeping
152
+ `inheritedPlanReviewDebt` write-once and untouched by any plan-review phase
153
+ guarantees a derived-plan review in the execution child can only ever mutate
154
+ `planReviewDebt`, never the inherited debt the execution reviewer actually needs.
155
+
156
+ ## Cross-run handoff (and its exclusion)
157
+
158
+ Plan→execution handoff is **cross-run, not in-process**: `neal run` runs planning
159
+ and execution as separate fresh child runs. The durable carrier is the
160
+ `PlanAndExecuteQueueItem`:
161
+
162
+ 1. On planning-child completion, `completePlanningStage` copies
163
+ `finalState.planReviewDebt` onto the queue item alongside `acceptedPlanPath`
164
+ (covering both in-process completion and the cross-process resume through
165
+ `continuePlanAndExecuteQueueFromChildRun`).
166
+ 2. When the fresh execution child starts, `runFreshPlanAndExecuteChild` passes
167
+ the item's debt as the `inheritedPlanReviewDebt` init option **only** for the
168
+ execution stage. `createInitialState` seeds the durable
169
+ `inheritedPlanReviewDebt` state field from it (the recomputed `planReviewDebt`
170
+ always starts empty).
171
+ 3. `buildReviewerContextPacket` surfaces the inherited debt to the execution
172
+ reviewer as full items (`canonicalId`, `findingClass`, `originRound`,
173
+ `claim`, `requiredAction`) under a dedicated `## Inherited Plan-Review Debt`
174
+ section, not a bare count.
175
+
176
+ **Exclusion (by design):** cross-run propagation is supported only inside a
177
+ single `neal run` queue (planning child → execution child). A standalone
178
+ `neal plan` run persists debt only in its own run record. A later
179
+ `neal execute`/`neal run` against the accepted plan is a fresh run that
180
+ re-reviews the plan and does **not** inherit that debt.
181
+
182
+ ## Where debt is visible
183
+
184
+ - **`REVIEW.md`** (`renderReviewMarkdown`): each finding line carries
185
+ `- Finding class:` (with `n/a` for classless execute-review findings), and a
186
+ dedicated `## Plan Review Debt` section lists `Inherited:` and `Current:`
187
+ groups.
188
+ - **`neal status`** (`buildStatusSnapshot` / `renderHumanStatusSnapshot`): a
189
+ `planReviewDebt` snapshot field sums both arrays with a per-item `inherited`
190
+ flag, rendered as a `Plan review debt:` line.
191
+ - **Reviewer context packet**: the `## Inherited Plan-Review Debt` section
192
+ described above.
193
+
194
+ ## Coder-authored plan-stage block recovery
195
+
196
+ The other non-acceptance exit is a plan-stage coder block. A coder-authored
197
+ plan-stage **response** block (a `coder_plan_response` or
198
+ `coder_plan_optional_response` block, the only plan-stage path that previously
199
+ terminal-failed) lands as the documented blocked contract instead of a terminal
200
+ failure. `finalizeBlockedPlanReviewResponse`
201
+ (`src/neal/orchestrator/phases/planning.ts`) takes an explicit `blockCause`
202
+ (`coder_authored` | `dirty_worktree` | `reviewer_convergence`), and for a
203
+ `coder_authored` block on the top-level plan stage, in **both attended and
204
+ unattended runs**, it:
205
+
206
+ - persists `status: 'blocked'` with a durable `blockerReason` (the coder's
207
+ reported blocker), so the writer exits **2** (not `failed`/exit 3) and the
208
+ reason surfaces in both the JSON and human `neal status` renderings.
209
+ - makes the block answerable via `neal resume --message`, reusing the existing
210
+ `pendingPlanReviewGuidance` channel and `runPlanningResponseAdjudication`
211
+ delivery. `recordPlanReviewGuidance`
212
+ (`src/neal/commands/recovery-guidance.ts`) maps the origin to the phase that
213
+ consumes the guidance: a `reviewer_plan` block still delivers to
214
+ `coder_plan_response` (unchanged), while a coder-authored response block returns
215
+ to its own origin phase (a `coder_plan_optional_response` block returns to
216
+ `coder_plan_optional_response`), so the re-run selects the right open findings
217
+ and delivers the guidance instead of accepting on an empty blocking set.
218
+ Recorded guidance forces a response adjudication even when a prior blocked
219
+ response closed every finding, so the operator's answer is never silently
220
+ discarded.
221
+
222
+ The invariant is that `blockerReason` is `null` whenever `status !== 'blocked'`.
223
+ The resume-planner blocked→running transitions clear it so it never outlives its
224
+ block.
225
+
226
+ Only `coder_authored` response blocks take this recoverable landing, and the
227
+ durable `blockerReason` is the discriminator. A `dirty_worktree` safety block
228
+ (the planner dirtied non-plan files with no operator to clean them) records no
229
+ `blockerReason`: it lands at the same response phase but stays a normal blocked
230
+ state: it is not reported or answerable as waiting for `--message` guidance, and
231
+ it keeps its prior bare-resume behavior when a resumable planner session exists. A
232
+ `dirty_worktree` safety block and reviewer cap/stall exhaustion (`reviewer_convergence`)
233
+ under `unattended` keep terminal-failing (exit 3).
234
+
235
+ **Exclusion (the initial `coder_plan` authoring block):** the author-input route
236
+ does **not** cover the initial `coder_plan` block. That block already lands
237
+ `status: 'blocked'` (exit 2, not a terminal failure) and stays resumable via a
238
+ bare `neal resume`. Delivering an operator answer to a re-run authoring round
239
+ would require a second guidance-delivery channel this policy deliberately avoids.
240
+ Its `--message` route is a deliberate follow-up, not part of this policy.
241
+
242
+ ## Related references
243
+
244
+ - `docs/plan-format.md`: executable plan shapes and normalization.
245
+ - `docs/state-machine.md`: persisted run and queue state invariants.
246
+ - `docs/adjudicator-inventory.md`: the shared coder/reviewer loop inventory.
@@ -1,29 +1,29 @@
1
- # Neal State Machine
1
+ # State machine
2
2
 
3
3
  The product-level storage layout, artifact classifications, run pointers,
4
4
  retention guidance, and no-global-index decision are documented in
5
- [storage.md](storage.md). This document focuses on the persisted ledgers and the
6
- state invariants Neal enforces when reading and writing them.
5
+ [storage.md](storage.md). What follows covers the persisted ledgers and the
6
+ state invariants neal enforces when reading and writing them.
7
7
 
8
- Neal keeps two persisted ledgers:
8
+ neal keeps two persisted ledgers:
9
9
 
10
10
  - `OrchestrationState` is the child-run ledger. It lives in each run directory as `RUN_STATE.json` and records one plan or execute writer run.
11
11
  - `PlanAndExecuteQueueState` is the parent queue ledger. It lives under `.neal/queues/<queue-id>/QUEUE_STATE.json` and tracks serial `neal run` children, their active stage, child run ids, and queue-level stop status.
12
12
 
13
- These ledgers are related but separate. A queue item points at child run state paths; child runs do not own the queue's status, item ordering, or current queue pointer.
13
+ These ledgers are related but separate. A queue item points at child run state paths. Child runs do not own the queue's status, item ordering, or current queue pointer.
14
14
 
15
- ## Hydration And Validation
15
+ ## Hydration and validation
16
16
 
17
- `src/neal/state.ts` owns v1 child-run hydration. It reads JSON, checks the basic shape, requires the current v1 fields Neal writes, and rejects unknown enum-like strings while hydrating phase references. Missing or malformed required child-run fields fail load instead of receiving invented defaults.
17
+ `src/neal/state.ts` owns v1 child-run hydration. It reads JSON, checks the basic shape, requires the current v1 fields neal writes, and rejects unknown enum-like strings while hydrating phase references. Missing or malformed required child-run fields fail load instead of receiving invented defaults.
18
18
 
19
19
  `src/neal/state-invariants.ts` owns semantic validation after hydration and before save. `assertValidOrchestrationState` is pure: it has no filesystem, git, provider, logger, or clock dependency. The same invariant layer runs when `loadState` returns hydrated state and when `saveState` builds the timestamped next state.
20
20
 
21
21
  Queue state has its own parser and invariants in `src/neal/plan-queue.ts`, centered on `parsePlanAndExecuteQueueState` and related queue item parsing helpers.
22
22
 
23
- ## Schema Versions
23
+ ## Schema versions
24
24
 
25
- Persisted JSON that Neal reads as durable schema is versioned at v1.
26
- `RUN_STATE.json` hydrates through `normalizeStateV1`; `.neal/current.json`,
25
+ Persisted JSON that neal reads as durable schema is versioned at v1.
26
+ `RUN_STATE.json` hydrates through `normalizeStateV1`. `.neal/current.json`,
27
27
  queue state, `.neal/current-queue.json`, queue child links, run narratives,
28
28
  squash audit results, and the active writer lock all have v1 write shapes or
29
29
  parsers.
@@ -32,7 +32,7 @@ Run `meta.json` is support data, not child-run state. Optional context artifacts
32
32
  such as `plan-progress.json` are written as v1 and read defensively so malformed
33
33
  or unsupported support data does not replace the canonical run ledger.
34
34
 
35
- ## State Views And Public Lifecycle
35
+ ## State views and public lifecycle
36
36
 
37
37
  Persisted v1 child-run state remains record-shaped, but most callers should not
38
38
  treat `OrchestrationState` as one large nullable programming model.
@@ -55,7 +55,7 @@ human output can say `waiting_for_guidance`, `paused`, `blocked`, `failed`, or
55
55
  fields such as `effectiveStatus`, `waitingForOperatorGuidance`, and
56
56
  `pendingOperatorGuidance`.
57
57
 
58
- ## Phase And Status
58
+ ## Phase and status
59
59
 
60
60
  Known child-run phases and statuses are centralized in `state-invariants.ts` through `ORCHESTRATION_PHASES`, `ORCHESTRATION_STATUSES`, and their runtime guard helpers. Runnable dispatch is authored in `src/neal/orchestrator/run-loop.ts` as purpose-specific registries for plan, execute, interactive recovery, and execute finalization phases. Those registries are composed into `RUNNABLE_PHASE_REGISTRY` for dispatch and exposed through top-level-mode helpers so tests can assert which phases are runnable for plan and execute runs.
61
61
 
@@ -68,16 +68,18 @@ The enforced phase/status relationship is intentionally small:
68
68
 
69
69
  Read-only `neal review` is not a writer-run mode. It writes isolated findings artifacts under `.neal/reviews/<review-id>/` and does not create `OrchestrationState`.
70
70
 
71
- ## Recovery State
71
+ ## Recovery state
72
72
 
73
73
  Interactive blocked recovery is owned by `interactiveBlockedRecovery` while the active phase is `interactive_blocked_recovery`. The invariant layer validates execute-mode ownership, supported source phases, bounded turn counters, contiguous turn numbers, and disposition result phases.
74
74
 
75
- Every block class coder-blocked signals, reviewer `review_stuck` deadlocks, and
76
- the split-plan invalid-payload block funnels through the single
77
- `enterInteractiveBlockedRecovery` chokepoint, where the generalized blocked
78
- adjudicator triages it (see Site A below). The adjudicator is read-only: it never
79
- grants authorization, expands scope, or waives verification gates. Its verdict
80
- auto-acts only under unattended runs; attended runs surface it as advice and yield.
75
+ Every block class (coder-blocked signals, reviewer `review_stuck` deadlocks, and
76
+ the split-plan invalid-payload block) funnels through the single
77
+ `enterInteractiveBlockedRecovery` chokepoint, where the consultant triages it (see
78
+ Site A below). The consultant is read-only: it never grants authorization, expands
79
+ scope, or waives verification gates. A recoverable verdict acts automatically under
80
+ both run modes. The modes differ only on a non-recoverable verdict, which finalizes
81
+ the run terminally under unattended runs and yields to the operator (carrying the
82
+ verdict as advice) under attended runs.
81
83
 
82
84
  Public resume eligibility is classified by `src/neal/resume-decision.ts` before
83
85
  any recovery mutation. That read-only decision layer combines loaded child-run
@@ -91,7 +93,7 @@ selected actions only after the selected run has been classified as executable.
91
93
  `state-invariants.ts` mirrors the allowed phase sets so changes to recovery
92
94
  behavior are visible in focused tests.
93
95
 
94
- ## Unattended Mode
96
+ ## Unattended mode
95
97
 
96
98
  `--unattended` / `agent.unattended` resolves to a persisted
97
99
  `OrchestrationState.unattended` boolean (default `false`) for both plan and
@@ -99,17 +101,17 @@ execute top-level modes, so a separate `neal resume` process and the `neal run`
99
101
  plan→execute hand-off see it without re-passing a flag. The flag overrides the
100
102
  config key. The resolved value is also threaded into the planner, reviewer,
101
103
  coder, final-completion, and plan-reviewer prompts, where it adds one autonomy
102
- line only when true; with `unattended` false the rendered prompts are
104
+ line only when true. With `unattended` false the rendered prompts are
103
105
  byte-identical to attended output.
104
106
 
105
- Unattended changes only the three structural operator-block sites; it never
107
+ Unattended changes only the three structural operator-block sites. It never
106
108
  weakens verification, authorization, or squash/grading, and never removes
107
109
  `block_for_operator` from any decision surface. Every unattended branch gates on
108
110
  structural state (`state.unattended`, `actionResolution.effectiveAction`,
109
111
  `phase`, `blockedFromPhase`, the bounded auto-resume counter), never on
110
112
  substring-matching assistant or guidance text.
111
113
 
112
- - **Site A execute-mode interactive recovery.** All fresh blocks funnel
114
+ - **Site A: execute-mode interactive recovery.** All fresh blocks funnel
113
115
  through `enterInteractiveBlockedRecovery` (`src/neal/orchestrator/phases/recovery.ts`).
114
116
  Under unattended, while `unattendedAutoResumeCount < UNATTENDED_MAX_AUTO_RESUMES`
115
117
  (a module constant, reconciled so it never pushes past
@@ -119,46 +121,46 @@ substring-matching assistant or guidance text.
119
121
  message uses, increments the persisted counter, and lets the run proceed into
120
122
  the recovery phase. Past the cap (or the `maxTurns` boundary) it runs the
121
123
  shared terminal-fail action instead of waiting.
122
- - **Generalized blocked adjudicator (bounded, both modes).** Inside
124
+ - **The consultant (bounded, both modes).** Inside
123
125
  `enterInteractiveBlockedRecovery`, *before* the generic auto-resume / yield
124
- decision, eligible blocks are triaged by a read-only adjudicator
125
- (`runBlockedAdjudicator`, in `src/neal/adjudicator/blocked-adjudicator.ts`,
126
+ decision, eligible blocks are triaged by the read-only consultant
127
+ (`runConsultant`, in `src/neal/adjudicator/consultant.ts`,
126
128
  running through the same no-write reviewer plumbing the review/final-completion
127
129
  reviewers use, making zero commits and zero file edits). Eligible source phases
128
- (`ADJUDICATOR_ELIGIBLE_SOURCE_PHASES`) are the coder-block phases (`coder_scope`
130
+ (`CONSULTANT_ELIGIBLE_SOURCE_PHASES`) are the coder-block phases (`coder_scope`
129
131
  / `coder_response` / `coder_optional_response`, which also carry the split-plan
130
132
  invalid-payload block) and the reviewer `review_stuck` phases (`reviewer_scope` /
131
- `reviewer_plan`). The adjudicator first applies an anti-thrash guard
133
+ `reviewer_plan`). The consultant first applies an anti-thrash guard
132
134
  (`recentBlocks`, keyed on scope identity + source phase + normalized blocker key +
133
135
  evidence fingerprint): a same-scope repeat with no new evidence short-circuits to
134
136
  `recoverable:false` without an LLM round. Otherwise it returns a verdict
135
- `{ recoverable, triageCategory, resolutionDirective, rationale }`. Under
136
- unattended, a `recoverable` `misunderstanding` verdict with a concrete in-scope
137
- directive enters recovery with that directive injected as the pending turn
138
- (consumed exactly like a human `neal resume --message`); a `recoverable:false`
139
- genuine wall (`authorization` / `external_precondition` / `impossible_task`) runs
140
- the shared terminal-fail action. Under attended, the same gated adjudicator runs
141
- read-only, persists its verdict as `interactiveBlockedRecovery.adjudicatorAdvice`,
142
- and the run yields for the operator (no auto-apply). It is bounded by the counter
143
- `reviewStuckArbiterCount` against the `review_stuck_arbiter_max_attempts` knob
144
- (default `1`, `0` disables) -- a SEPARATE budget that never touches
137
+ `{ recoverable, triageCategory, resolutionDirective, rationale }`. A `recoverable`
138
+ `misunderstanding` verdict with a concrete in-scope directive enters recovery with
139
+ that directive injected as the pending turn (consumed exactly like a human
140
+ `neal resume --message`) under both run modes. The modes differ only on a
141
+ `recoverable:false` genuine blocker (`authorization` / `external_precondition` /
142
+ `impossible_task`): unattended runs the shared terminal-fail action, while attended
143
+ persists the verdict as `interactiveBlockedRecovery.consultantAdvice` and yields for
144
+ the operator. It is bounded by the counter
145
+ `consultantAttemptCount` against the `consultant_max_attempts` knob
146
+ (default `1`, `0` disables). It's a separate budget that never touches
145
147
  `unattendedAutoResumeCount` or `interactiveBlockedRecovery.maxTurns`. Every other
146
148
  case (ineligible source phase, disabled/exhausted cap, turn cap, or any
147
- adjudicator error) falls through to the generic auto-resume / yield path
148
- unchanged, writing neither `recentBlocks` nor `adjudicatorAdvice`.
149
+ consultant error) falls through to the generic auto-resume / yield path
150
+ unchanged, writing neither `recentBlocks` nor `consultantAdvice`.
149
151
  The decisions are auditable from the structured event log via the
150
- `review_stuck_arbiter.{start,verdict,resolved,declined,exhausted}` events, which
152
+ `consultant.{start,verdict,resolved,declined}` events, which
151
153
  carry `scopeNumber`, `sourcePhase`, `blockedReason`, and (on `verdict`/`resolved`)
152
154
  `recoverable`, `triageCategory`, `targetCanonicalIds`, and the post-increment
153
- `reviewStuckArbiterCount`.
154
- - **Sites B and C final-completion review and the top-level plan-review gate.**
155
+ `consultantAttemptCount`.
156
+ - **Sites B and C: final-completion review and the top-level plan-review gate.**
155
157
  These gates block directly (bypassing the recovery chokepoint), and their own
156
- budgets the final-completion continue-execution cap and the
157
- review-round/convergence cap already bounded the autonomous effort. Under
158
+ budgets (the final-completion continue-execution cap and the
159
+ review-round/convergence cap) already bounded the autonomous effort. Under
158
160
  unattended they run the shared terminal-fail action immediately rather than
159
- saving `status:'blocked'`; there is no auto-resume and no synthesized
161
+ saving `status:'blocked'`. There is no auto-resume and no synthesized
160
162
  `pendingPlanReviewGuidance`. Site C edits only the `topLevelMode !== 'execute'`
161
- branch of `finalizeBlockedPlanReviewResponse`; execute-mode derived-plan-review
163
+ branch of `finalizeBlockedPlanReviewResponse`. Execute-mode derived-plan-review
162
164
  blocks (`topLevelMode === 'execute'`) re-enter site A and are handled there.
163
165
 
164
166
  The shared terminal-fail action is `persistUnattendedBlockUnresolvedFailure`
@@ -167,14 +169,14 @@ The shared terminal-fail action is `persistUnattendedBlockUnresolvedFailure`
167
169
  `phase`/`blockedFromPhase` for diagnostics), re-render execution artifacts, write
168
170
  a `failed` checkpoint retrospective, and emit the classified
169
171
  `unattended.block_unresolved` log event (`reason:'unattended_block_unresolved'`
170
- plus the `UnattendedBlockSite` origin) deliberately without `notifyBlocked`,
172
+ plus the `UnattendedBlockSite` origin), deliberately without `notifyBlocked`,
171
173
  which is the attended wait notification. The run exits with writer code `3`, and
172
174
  any produced diff/plan is left unsubmitted as an artifact. There is no top-level
173
- reason field on `OrchestrationState`; the classification rides the log event and
175
+ reason field on `OrchestrationState`. The classification lives in the log event and
174
176
  retrospective. Attended runs are unchanged and still wait for
175
177
  `neal resume --message` at all three sites.
176
178
 
177
- ## Resume Planning
179
+ ## Resume planning
178
180
 
179
181
  New-run initialization and existing-run resume reconciliation are separate.
180
182
  `loadOrInitialize` still provides the command-facing entrypoint, but resume
@@ -198,10 +200,10 @@ normalization, event logging, pointer writes, and execution artifacts.
198
200
  when the shared decision says interactive recovery is waiting for guidance.
199
201
  Pending guidance resumes with plain `neal resume --run <run-id>`. Queue
200
202
  continuation still belongs to `continuePlanAndExecuteQueueFromChildRun` after
201
- the resumed child run finishes; selecting a child run for resume does not
203
+ the resumed child run finishes. Selecting a child run for resume does not
202
204
  advance or repair the parent queue by itself.
203
205
 
204
- ## Derived Plans
206
+ ## Derived plans
205
207
 
206
208
  Derived-plan fields are child-run state because they describe replacement execution for the active child run, not the parent queue. The invariant layer enforces these ownership rules:
207
209
 
@@ -212,23 +214,23 @@ Derived-plan fields are child-run state because they describe replacement execut
212
214
 
213
215
  Derived-plan execution should continue to flow through the existing orchestrator transition helpers before any future shape refactor changes the persisted fields.
214
216
 
215
- ## Final Completion
217
+ ## Final completion
216
218
 
217
- Final-completion review is execute-mode only. The `final_completion_review` phase requires a `finalCompletionSummary`. When a reviewer verdict exists, `finalCompletionResolvedAction` must match the effective action, including the continue-execution cap case where Neal resolves to operator blocking instead of starting another scope.
219
+ Final-completion review is execute-mode only. The `final_completion_review` phase requires a `finalCompletionSummary`. When a reviewer verdict exists, `finalCompletionResolvedAction` must match the effective action, including the continue-execution cap case where neal resolves to operator blocking instead of starting another scope.
218
220
 
219
221
  When final completion review asks to continue execution, the orchestrator may clear summary and verdict fields and reopen `coder_scope`. That reopened running state is valid.
220
222
 
221
- Accepted execute scopes also pass through execute finalization before Neal either opens the next scope or starts final-completion review. The persisted internal runnable cursor for that step is `execute_finalization`, which is separate from the public `neal squash` command. Public `neal squash` is a post-run command with selection, preview, and interactive TTY confirmation before it rewrites history.
223
+ Accepted execute scopes also pass through execute finalization before neal either opens the next scope or starts final-completion review. The persisted internal runnable cursor for that step is `execute_finalization`, which is separate from the public `neal squash` command. Public `neal squash` is a post-run command with selection, preview, and interactive TTY confirmation before it rewrites history.
222
224
 
223
- ## Atomic Writes And Locks
225
+ ## Atomic writes and locks
224
226
 
225
227
  `src/neal/atomic-write.ts` provides per-file atomic replacement helpers. State, current run pointers, queue state, queue pointers, queue links, queue summaries, run metadata, progress artifacts, review/final-completion/recovery/split-plan/retrospective artifacts, run narratives, and default squash audit writes use temp-file-plus-rename writes where in scope.
226
228
 
227
229
  This is not a multi-file transaction. If a process stops between writes, each individual JSON or text file should be either the previous complete file or the next complete file, but related files can briefly disagree. Resume and status commands must continue to tolerate that by resolving run paths and validating loaded state.
228
230
 
229
- The active-run lock is separate. `src/neal/run-lock.ts` uses exclusive creation for `.neal/active-run.lock`; that acquisition path should not be converted to atomic rename because the exclusive-create behavior is the mutual exclusion mechanism.
231
+ The active-run lock is separate. `src/neal/run-lock.ts` uses exclusive creation for `.neal/active-run.lock`. That acquisition path should not be converted to atomic rename because the exclusive-create behavior is the mutual exclusion mechanism.
230
232
 
231
- ## Change Checklist
233
+ ## Change checklist
232
234
 
233
235
  When adding a child-run phase:
234
236
 
@@ -253,7 +255,7 @@ When adding a queue-state field:
253
255
  - Parse and validate it in `parsePlanAndExecuteQueueState` or `parsePlanAndExecuteQueueItem`.
254
256
  - Include it in queue summary or current queue pointer output only when it is operator-facing or needed for resume.
255
257
 
256
- ## Future Refactor Path
258
+ ## Future refactor path
257
259
 
258
260
  Do not jump directly from the current record-shaped `OrchestrationState` to a
259
261
  persisted discriminated union. The lower-risk path now in place is to keep v1