@navels/neal 0.4.2 → 0.5.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 (38) hide show
  1. package/README.md +18 -13
  2. package/dist/neal/adjudicator/execute.js +0 -1
  3. package/dist/neal/adjudicator/final-completion.js +0 -1
  4. package/dist/neal/adjudicator/planning.js +0 -1
  5. package/dist/neal/agents/rounds.js +1 -2
  6. package/dist/neal/blocked-guidance.js +1 -7
  7. package/dist/neal/cli.js +3 -19
  8. package/dist/neal/commands/compat.js +36 -13
  9. package/dist/neal/commands/new-run.js +1 -6
  10. package/dist/neal/commands/plan-and-execute.js +1 -5
  11. package/dist/neal/commands/resume-run.js +5 -7
  12. package/dist/neal/config.js +3 -11
  13. package/dist/neal/orchestrator/completion.js +7 -23
  14. package/dist/neal/orchestrator/notifications.js +1 -1
  15. package/dist/neal/orchestrator/phases/coder.js +0 -1
  16. package/dist/neal/orchestrator/phases/planning.js +7 -16
  17. package/dist/neal/orchestrator/phases/recovery.js +42 -285
  18. package/dist/neal/orchestrator/phases/shared.js +0 -26
  19. package/dist/neal/orchestrator/run-loop.js +1 -5
  20. package/dist/neal/orchestrator.js +0 -1
  21. package/dist/neal/plan-queue.js +5 -15
  22. package/dist/neal/prompts/execute.js +2 -4
  23. package/dist/neal/prompts/planning.js +1 -3
  24. package/dist/neal/prompts/shared.js +0 -7
  25. package/dist/neal/prompts/specialized.js +0 -2
  26. package/dist/neal/prompts/specs.js +25 -5
  27. package/dist/neal/state-invariants.js +0 -1
  28. package/dist/neal/state.js +0 -4
  29. package/docs/adjudicator-inventory.md +2 -3
  30. package/docs/architecture.md +1 -2
  31. package/docs/compat.md +17 -7
  32. package/docs/maintenance.md +7 -6
  33. package/docs/release.md +9 -0
  34. package/docs/review-convergence.md +7 -8
  35. package/docs/state-machine.md +53 -63
  36. package/docs/troubleshooting.md +19 -6
  37. package/neal.yml +0 -9
  38. package/package.json +8 -8
@@ -2,7 +2,6 @@ import { guardStructuredJsonOutputFormatLines } from '../agents/structured-json.
2
2
  import { renderInlinedRangeDiffSection } from '../context/inline-review-context.js';
3
3
  import { assertPromptBuilder } from './assert-builder.js';
4
4
  import { getUserGuidanceLines } from './guidance.js';
5
- import { getUnattendedAutonomyLines } from './shared.js';
6
5
  import { getAdversarialReviewDoctrineLines, getCodeReviewFalsificationLines, getFindingQualityLines, getPreexistingFailureContractLines, getRegressionPreservationLines, getVerificationSkepticismLines, } from './review-doctrine.js';
7
6
  const PROMPT_MODULE_PATH = 'src/neal/prompts/specialized.ts';
8
7
  // Guarded output-format instruction block shared by the two structured-JSON
@@ -154,7 +153,6 @@ export function buildFinalCompletionReviewerPrompt(args) {
154
153
  'Use `accept_complete` only when the full plan objectives are satisfied and the aggregate implementation is acceptable under ordinary code review standards.',
155
154
  'Use `continue_execution` only when the remaining work is concrete, bounded, and suitable for one explicit follow-on scope.',
156
155
  'Use `block_for_operator` when the remaining gap is ambiguous, externally constrained, or needs human direction.',
157
- ...getUnattendedAutonomyLines(args.unattended),
158
156
  'When you return `continue_execution`, you must provide a non-null `missingWork` object with `summary`, `requiredOutcome`, and `verification`.',
159
157
  'When you return any other action, `missingWork` must be null.',
160
158
  '',
@@ -88,7 +88,7 @@ const CONSULTANT_CONTEXT = context('ConsultantPromptContext', [
88
88
  export const PROMPT_SPECS = [
89
89
  {
90
90
  id: 'plan_author',
91
- version: 3,
91
+ version: 4,
92
92
  changelog: [
93
93
  {
94
94
  version: 1,
@@ -102,6 +102,10 @@ export const PROMPT_SPECS = [
102
102
  version: 3,
103
103
  renderSha: '51943007b2a334ee0c92d8314616fb882e9cea7b410b05c5aeda668c7c9e04dc',
104
104
  },
105
+ {
106
+ version: 4,
107
+ renderSha: 'e9f5b2dd5f66df87986b2e2f884c560d02a235db053b544221dcb43512e057a4',
108
+ },
105
109
  ],
106
110
  role: 'coder',
107
111
  purpose: 'Author or revise concise, human-reviewable Neal-executable plans at moderate-to-high-level implementation detail.',
@@ -176,7 +180,7 @@ export const PROMPT_SPECS = [
176
180
  },
177
181
  {
178
182
  id: 'plan_reviewer',
179
- version: 3,
183
+ version: 4,
180
184
  changelog: [
181
185
  {
182
186
  version: 1,
@@ -190,6 +194,10 @@ export const PROMPT_SPECS = [
190
194
  version: 3,
191
195
  renderSha: 'fa0309ed0e394c690dfc3135da3c98a74dd5413ccb754fefa100171bf35b72b6',
192
196
  },
197
+ {
198
+ version: 4,
199
+ renderSha: '65ad05a8bbe1f4a6d631db8778bf6bed82249151f3412a7773f9568ae2d40f11',
200
+ },
193
201
  ],
194
202
  role: 'reviewer',
195
203
  purpose: 'Review human-reviewable Neal-executable plans for material approach, scope, verification, and resume-safety defects.',
@@ -275,12 +283,16 @@ export const PROMPT_SPECS = [
275
283
  },
276
284
  {
277
285
  id: 'scope_coder',
278
- version: 1,
286
+ version: 2,
279
287
  changelog: [
280
288
  {
281
289
  version: 1,
282
290
  renderSha: 'c579d65db51be90fa81dbe3b7dd260083bc4960cc4cba32ccf2cd802d19703bc',
283
291
  },
292
+ {
293
+ version: 2,
294
+ renderSha: '0ce921ee7d0acc4042bacf31e1509968f7e76df8db8417eb4b724ce7842794ac',
295
+ },
284
296
  ],
285
297
  role: 'coder',
286
298
  purpose: 'Execute exactly one bounded implementation scope and respond to in-scope review feedback without starting new scopes.',
@@ -384,7 +396,7 @@ export const PROMPT_SPECS = [
384
396
  },
385
397
  {
386
398
  id: 'scope_reviewer',
387
- version: 3,
399
+ version: 4,
388
400
  changelog: [
389
401
  {
390
402
  version: 1,
@@ -398,6 +410,10 @@ export const PROMPT_SPECS = [
398
410
  version: 3,
399
411
  renderSha: '4b75fab01367f0e4e263bc7635f8753e8e73c08b3ca46cf6bb6e695a67da31a4',
400
412
  },
413
+ {
414
+ version: 4,
415
+ renderSha: 'da87b19f2401ffdca21e3cefec1037c6470b3e74810b6152d07c55fa4924047f',
416
+ },
401
417
  ],
402
418
  role: 'reviewer',
403
419
  purpose: 'Review execute-scope results for correctness, verification coverage, and meaningful progress toward the active parent objective.',
@@ -549,7 +565,7 @@ export const PROMPT_SPECS = [
549
565
  },
550
566
  {
551
567
  id: 'completion_reviewer',
552
- version: 2,
568
+ version: 3,
553
569
  changelog: [
554
570
  {
555
571
  version: 1,
@@ -559,6 +575,10 @@ export const PROMPT_SPECS = [
559
575
  version: 2,
560
576
  renderSha: 'cc06cea4a8de6366f07a6943718fc1ae219e4c9092a5cf52542fa9de0caa02da',
561
577
  },
578
+ {
579
+ version: 3,
580
+ renderSha: '127097db4b0d06cba8943681d12d93fbb07e577e078c99c1d81036399bedb66a',
581
+ },
562
582
  ],
563
583
  role: 'reviewer',
564
584
  purpose: 'Judge whole-plan completion and decide whether Neal should accept completion, continue execution, or block for operator input.',
@@ -476,7 +476,6 @@ export function assertValidOrchestrationState(state, context) {
476
476
  }
477
477
  assertSafeIntegerAtLeast(state.currentScopeNumber, 'currentScopeNumber', 0, context);
478
478
  assertSafeIntegerAtLeast(state.coderRetryCount, 'coderRetryCount', 0, context);
479
- assertSafeIntegerAtLeast(state.unattendedAutoResumeCount, 'unattendedAutoResumeCount', 0, context);
480
479
  assertSafeIntegerAtLeast(state.consultantAttemptCount, 'consultantAttemptCount', 0, context);
481
480
  assertSafeIntegerAtLeast(state.finalCompletionContinueExecutionCount, 'finalCompletionContinueExecutionCount', 0, context);
482
481
  assertSafeIntegerAtLeast(state.splitPlanCountForCurrentScope, 'splitPlanCountForCurrentScope', 0, context);
@@ -83,8 +83,6 @@ export async function createInitialState(init, baseCommit) {
83
83
  topLevelMode: init.topLevelMode,
84
84
  allowedDirtyPaths: [...init.allowedDirtyPaths],
85
85
  agentConfig: init.agentConfig,
86
- unattended: init.unattended ?? false,
87
- unattendedAutoResumeCount: 0,
88
86
  consultantAttemptCount: 0,
89
87
  autoSquashOnCompletion: init.autoSquashOnCompletion ?? true,
90
88
  progressJsonPath: init.progressJsonPath,
@@ -848,8 +846,6 @@ function normalizeStateV1(parsed) {
848
846
  topLevelMode,
849
847
  allowedDirtyPaths: readStringArray(state, 'allowedDirtyPaths'),
850
848
  agentConfig: hydrateAgentConfig(readRequired(state, 'agentConfig'), 'agentConfig'),
851
- unattended: readOptionalBoolean(state, 'unattended') ?? false,
852
- unattendedAutoResumeCount: readOptionalSafeInteger(state, 'unattendedAutoResumeCount') ?? 0,
853
849
  consultantAttemptCount: readOptionalSafeInteger(state, 'consultantAttemptCount') ?? 0,
854
850
  // Legacy-tolerant: states persisted before this field existed hydrate to
855
851
  // true, preserving the historical always-squash behavior on resume.
@@ -258,9 +258,8 @@ generic recovery with no consultant invocation:
258
258
  - Per-scope budget: `state.consultantAttemptCount`, bounded by
259
259
  `neal.consultant_max_attempts` (default `1`, `0` disables) and
260
260
  reset to `0` at scope boundaries. One invocation consumes one unit whether it
261
- auto-applies a recoverable verdict (both run modes) or, on a non-recoverable
262
- verdict, finalizes terminally (unattended) or surfaces the verdict as operator
263
- advice and yields (attended).
261
+ auto-applies a recoverable verdict or, on a non-recoverable verdict, surfaces
262
+ the verdict as operator advice and yields.
264
263
  - Anti-thrash window: a block that repeats a `state.recentBlocks` record with
265
264
  the same scope identity (`scopeNumber` + `derivedScopeIndex`), the same
266
265
  `sourcePhase`, the same normalized blocker key, and no new evidence
@@ -41,8 +41,7 @@ document. The phases live in `src/neal/orchestrator/phases/`:
41
41
  forth without resolution, or a step that tried to break a
42
42
  scope into a smaller plan produced a plan that wasn't valid. When the fix is
43
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.
44
+ run keeps going. Otherwise the run stops and waits for you.
46
45
 
47
46
  After every scope is accepted, a final-completion review
48
47
  (`final-completion-review.ts`) checks the whole plan, then neal squashes the run
package/docs/compat.md CHANGED
@@ -153,8 +153,8 @@ self-test (`test/compat-fixtures.test.ts`), never by a model run.
153
153
 
154
154
  Across **every** fixture for the role, the model must (1) **complete the run
155
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.
156
+ `status: 'done'` not `'failed'`, `'blocked'`, or an operator-stop wait —
157
+ and does not exceed its step/round budget.
158
158
  The role-correct result is:
159
159
 
160
160
  - **coder:** `finalState.status === 'done'` **and** the fixture's
@@ -178,8 +178,15 @@ The role-correct result is:
178
178
  Any single fundamental failure on any fixture for the role → **FAIL** for that
179
179
  role, with the mode recorded.
180
180
 
181
- `neal compat` forces **unattended mode** on every run so a model is never
182
- penalized for a halt that a human operator would simply have resumed.
181
+ `neal compat` runs each fixture with **no operator attached**. A run that stops
182
+ to wait for an operator is classified `block_unresolved` and FAILs: the
183
+ fixtures are trivial, so needing a human is itself the compatibility failure.
184
+ This is the same rule external harnesses apply — a blocked run always exits
185
+ with writer code `2`, and a driver with no operator (neal-swebench, CI)
186
+ records that exit as a failure verdict. Compat also silences neal's own
187
+ operator notifier for its child runs (it sets the defined-but-empty
188
+ `NEAL_NOTIFY_BIN` override at startup), so a blocked fixture run never pings
189
+ the operator's configured notify helper mid-matrix.
183
190
 
184
191
  ## Failure-mode taxonomy
185
192
 
@@ -198,9 +205,12 @@ one applies, the earliest in this list (most specific cause first) is recorded:
198
205
  step/round-budget exhaustion, which the current runtime does not surface to compat
199
206
  as a distinct cap event, so writer step-cap exhaustion is reported here rather than
200
207
  as `max_step_loop`.
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'`.
208
+ - `block_unresolved`: the run's final persisted state is an operator stop (the
209
+ model escalated to a block that only a human could answer). The signal
210
+ mirrors the writer exit-code-2 mapping: the run is structurally waiting for
211
+ the operator per `getRunDisplayStatus` (the interactive-recovery wait or a
212
+ pending-guidance view) or persisted `status: 'blocked'`. Also recorded when
213
+ the review loop's outcome was `'blocked'`.
204
214
  - `max_step_loop`: the **reviewer** loop's outcome was `'cap_reached'` (the
205
215
  review-findings convergence cap was hit). Writer (coder/planner) step-cap
206
216
  exhaustion is not separately distinguishable under the current runtime and is
@@ -46,12 +46,13 @@ is gated on it as a whole.
46
46
  It runs the full suite plus a live `neal compat --role all` pass-through on
47
47
  every bumped adapter in the PR (in a throwaway worktree, with roles and
48
48
  models pinned explicitly so nothing leaks from `~/.neal/config.yml`), posts
49
- the compat matrices to the PR, and approves on PASS (`--merge` also
50
- squash-merges).
51
- 4. **Adopt.** Merge, bump neal's version, add a CHANGELOG entry noting the bump +
52
- any behavior change, and cut a release via the existing workflow. Urgent
53
- bumps (a fix neal needs immediately) may skip the Renovate soak with a
54
- manual PR. Qualify them the same way.
49
+ the compat matrices to the PR, and approves on PASS, leaving the PR open
50
+ for the release script.
51
+ 4. **Adopt.** Run `scripts/release-sdk-bump.sh <pr-number>`: it merges the PR,
52
+ opens and merges the release-preparation PR (version bump + changelog), and
53
+ runs the Publish workflow through the npm 2FA approval. See
54
+ [docs/release.md](release.md). Urgent bumps (a fix neal needs immediately)
55
+ may skip the Renovate soak with a manual PR. Qualify them the same way.
55
56
 
56
57
  ## TypeScript 6 and 7 side by side
57
58
 
package/docs/release.md CHANGED
@@ -45,6 +45,15 @@ patch release for a compatibility fix that preserves documented behavior, a
45
45
  minor release for behavior changes before `1.0.0`, and a major release after
46
46
  `1.0.0` if a documented public contract breaks.
47
47
 
48
+ For a qualified dependency-bump PR, `scripts/release-sdk-bump.sh <pr-number>`
49
+ runs this entire process as one command: it merges the dependency PR, opens and
50
+ merges the release-preparation pull request with a generated changelog section,
51
+ runs the Publish workflow dry run and, after a confirmation, the real run, and
52
+ prompts for the npm 2FA stage approval. It refuses PRs that touch anything
53
+ beyond `package.json` and `pnpm-lock.yaml`, and refuses native agentic-SDK
54
+ bumps that lack a `scripts/qualify-sdk.sh` PASS review. Every other release
55
+ follows the manual steps below.
56
+
48
57
  ## Prepare a release
49
58
 
50
59
  Bump `package.json.version` and add a nonempty `## [<version>]` section to
@@ -1,10 +1,10 @@
1
1
  # Plan-review convergence
2
2
 
3
- Plan review either converges or the run fails. The reviewer is asked "is this
3
+ Plan review either converges or the run blocks. The reviewer is asked "is this
4
4
  plan execution-ready?" fresh every round against a document that grows with each
5
5
  fix, so every individually-defensible new finding forces another full revision
6
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
7
+ round cap, and a plan-stage coder block with no answerable landing. That produced two failure modes in
8
8
  real runs: long negotiations that keep re-litigating verification strength after
9
9
  the plan is already correct, and runs that terminally fail when a coder
10
10
  legitimately needs author input it must not fabricate.
@@ -13,7 +13,7 @@ The convergence policy addresses both failure modes as **deterministic
13
13
  orchestration policy**, not prompt tuning. It never shortens a negotiation that
14
14
  surfaced a genuine plan-correctness defect, and only stops burning rounds on
15
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
16
+ coder-authored plan-stage block lands as a
17
17
  recoverable blocked-with-reason state that an operator can answer via
18
18
  `neal resume --message` (see
19
19
  [Coder-authored plan-stage block recovery](#coder-authored-plan-stage-block-recovery)).
@@ -200,8 +200,7 @@ terminal-failed) lands as the documented blocked contract instead of a terminal
200
200
  failure. `finalizeBlockedPlanReviewResponse`
201
201
  (`src/neal/orchestrator/phases/planning.ts`) takes an explicit `blockCause`
202
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:
203
+ `coder_authored` block on the top-level plan stage, it:
205
204
 
206
205
  - persists `status: 'blocked'` with a durable `blockerReason` (the coder's
207
206
  reported blocker), so the writer exits **2** (not `failed`/exit 3) and the
@@ -228,9 +227,9 @@ durable `blockerReason` is the discriminator. A `dirty_worktree` safety block
228
227
  (the planner dirtied non-plan files with no operator to clean them) records no
229
228
  `blockerReason`: it lands at the same response phase but stays a normal blocked
230
229
  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).
230
+ it keeps its prior bare-resume behavior when a resumable planner session exists.
231
+ Reviewer cap/stall exhaustion (`reviewer_convergence`) likewise lands a normal
232
+ blocked state with no `blockerReason` (exit 2).
234
233
 
235
234
  **Exclusion (the initial `coder_plan` authoring block):** the author-input route
236
235
  does **not** cover the initial `coder_plan` block. That block already lands
@@ -76,10 +76,8 @@ Every block class (coder-blocked signals, reviewer `review_stuck` deadlocks, and
76
76
  the split-plan invalid-payload block) funnels through the single
77
77
  `enterInteractiveBlockedRecovery` chokepoint, where the consultant triages it (see
78
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.
79
+ scope, or waives verification gates. A recoverable verdict acts automatically. A
80
+ non-recoverable verdict yields to the operator, carrying the verdict as advice.
83
81
 
84
82
  Public resume eligibility is classified by `src/neal/resume-decision.ts` before
85
83
  any recovery mutation. That read-only decision layer combines loaded child-run
@@ -93,37 +91,39 @@ selected actions only after the selected run has been classified as executable.
93
91
  `state-invariants.ts` mirrors the allowed phase sets so changes to recovery
94
92
  behavior are visible in focused tests.
95
93
 
96
- ## Unattended mode
97
-
98
- `--unattended` / `agent.unattended` resolves to a persisted
99
- `OrchestrationState.unattended` boolean (default `false`) for both plan and
100
- execute top-level modes, so a separate `neal resume` process and the `neal run`
101
- plan→execute hand-off see it without re-passing a flag. The flag overrides the
102
- config key. The resolved value is also threaded into the planner, reviewer,
103
- coder, final-completion, and plan-reviewer prompts, where it adds one autonomy
104
- line only when true. With `unattended` false the rendered prompts are
105
- byte-identical to attended output.
106
-
107
- Unattended changes only the three structural operator-block sites. It never
108
- weakens verification, authorization, or squash/grading, and never removes
109
- `block_for_operator` from any decision surface. Every unattended branch gates on
110
- structural state (`state.unattended`, `actionResolution.effectiveAction`,
111
- `phase`, `blockedFromPhase`, the bounded auto-resume counter), never on
112
- substring-matching assistant or guidance text.
113
-
114
- - **Site A: execute-mode interactive recovery.** All fresh blocks funnel
115
- through `enterInteractiveBlockedRecovery` (`src/neal/orchestrator/phases/recovery.ts`).
116
- Under unattended, while `unattendedAutoResumeCount < UNATTENDED_MAX_AUTO_RESUMES`
117
- (a module constant, reconciled so it never pushes past
118
- `interactiveBlockedRecovery.maxTurns`), it appends a synthesized conservative
119
- guidance turn (`UNATTENDED_AUTO_RESUME_GUIDANCE` from
120
- `src/neal/blocked-guidance.ts`) via the same turn-recording helper a human
121
- message uses, increments the persisted counter, and lets the run proceed into
122
- the recovery phase. Past the cap (or the `maxTurns` boundary) it runs the
123
- shared terminal-fail action instead of waiting.
124
- - **The consultant (bounded, both modes).** Inside
125
- `enterInteractiveBlockedRecovery`, *before* the generic auto-resume / yield
126
- decision, eligible blocks are triaged by the read-only consultant
94
+ ## Operator-block sites
95
+
96
+ Every operator stop lands in a controlled, operator-actionable state, and the
97
+ writer exits with code `2` (`src/neal/commands/writer-exit-codes.ts`). Exit `2`
98
+ means the run needs operator intervention, not that every stop accepts resume
99
+ guidance: sites A and C below take `neal resume --message`, while site B stays
100
+ non-mechanically blocked (`neal resume` keeps it blocked) and the operator
101
+ inspects `neal status` and the run artifacts instead. A consumer that needs a
102
+ hard verdict with no operator available (a benchmark harness, `neal compat`)
103
+ treats an exit-2 operator stop as a failure itself.
104
+
105
+ Block handling never weakens verification, authorization, or squash/grading,
106
+ and never removes `block_for_operator` from any decision surface. Every block
107
+ branch gates on structural state (`actionResolution.effectiveAction`, `phase`,
108
+ `blockedFromPhase`, the derived state views), never on substring-matching
109
+ assistant or guidance text.
110
+
111
+ There are three structural block sites:
112
+
113
+ - **Site A: execute-mode interactive recovery.** All fresh execute-mode blocks
114
+ funnel through `enterInteractiveBlockedRecovery`
115
+ (`src/neal/orchestrator/phases/recovery.ts`), where the block first gets
116
+ bounded read-only consultant triage (below). A recoverable verdict with a
117
+ concrete directive auto-applies and the run continues. Everything else — a
118
+ non-recoverable verdict, or a consultant gated off by eligibility, budget, or
119
+ error yields as the operator wait: `status: 'running'` +
120
+ `phase: 'interactive_blocked_recovery'`, carrying any verdict as
121
+ `interactiveBlockedRecovery.consultantAdvice`. The wait notification
122
+ (`notifyBlocked`) fires only when the run is structurally waiting for the
123
+ operator (`shouldNotifyInteractiveBlockedRecoveryEntry` gates on the derived
124
+ waiting-for-guidance view), and the run resumes via `neal resume --message`.
125
+ - **The consultant (bounded).** Inside `enterInteractiveBlockedRecovery`,
126
+ eligible blocks are triaged by the read-only consultant
127
127
  (`runConsultant`, in `src/neal/adjudicator/consultant.ts`,
128
128
  running through the same no-write reviewer plumbing the review/final-completion
129
129
  reviewers use, making zero commits and zero file edits). Eligible source phases
@@ -137,44 +137,34 @@ substring-matching assistant or guidance text.
137
137
  `{ recoverable, triageCategory, resolutionDirective, rationale }`. A `recoverable`
138
138
  `misunderstanding` verdict with a concrete in-scope directive enters recovery with
139
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
140
+ `neal resume --message`). A `recoverable:false` genuine blocker
141
+ (`authorization` / `external_precondition` / `impossible_task`) persists the
142
+ verdict as `interactiveBlockedRecovery.consultantAdvice` and yields for
144
143
  the operator. It is bounded by the counter
145
144
  `consultantAttemptCount` against the `consultant_max_attempts` knob
146
145
  (default `1`, `0` disables). It's a separate budget that never touches
147
- `unattendedAutoResumeCount` or `interactiveBlockedRecovery.maxTurns`. Every other
146
+ `interactiveBlockedRecovery.maxTurns`. Every other
148
147
  case (ineligible source phase, disabled/exhausted cap, turn cap, or any
149
- consultant error) falls through to the generic auto-resume / yield path
148
+ consultant error) falls through to the operator wait
150
149
  unchanged, writing neither `recentBlocks` nor `consultantAdvice`.
151
150
  The decisions are auditable from the structured event log via the
152
- `consultant.{start,verdict,resolved,declined}` events, which
151
+ `consultant.{start,verdict,resolved}` events, which
153
152
  carry `scopeNumber`, `sourcePhase`, `blockedReason`, and (on `verdict`/`resolved`)
154
153
  `recoverable`, `triageCategory`, `targetCanonicalIds`, and the post-increment
155
154
  `consultantAttemptCount`.
156
155
  - **Sites B and C: final-completion review and the top-level plan-review gate.**
157
- These gates block directly (bypassing the recovery chokepoint), and their own
158
- budgets (the final-completion continue-execution cap and the
159
- review-round/convergence cap) already bounded the autonomous effort. Under
160
- unattended they run the shared terminal-fail action immediately rather than
161
- saving `status:'blocked'`. There is no auto-resume and no synthesized
162
- `pendingPlanReviewGuidance`. Site C edits only the `topLevelMode !== 'execute'`
163
- branch of `finalizeBlockedPlanReviewResponse`. Execute-mode derived-plan-review
164
- blocks (`topLevelMode === 'execute'`) re-enter site A and are handled there.
165
-
166
- The shared terminal-fail action is `persistUnattendedBlockUnresolvedFailure`
167
- (`src/neal/orchestrator/phases/shared.ts`), which mirrors the
168
- `persistCoderFailureState` failed-run shape: save `status:'failed'` (preserving
169
- `phase`/`blockedFromPhase` for diagnostics), re-render execution artifacts, write
170
- a `failed` checkpoint retrospective, and emit the classified
171
- `unattended.block_unresolved` log event (`reason:'unattended_block_unresolved'`
172
- plus the `UnattendedBlockSite` origin), deliberately without `notifyBlocked`,
173
- which is the attended wait notification. The run exits with writer code `3`, and
174
- any produced diff/plan is left unsubmitted as an artifact. There is no top-level
175
- reason field on `OrchestrationState`. The classification lives in the log event and
176
- retrospective. Attended runs are unchanged and still wait for
177
- `neal resume --message` at all three sites.
156
+ These gates block directly with `status: 'blocked'` and the wait
157
+ notification, bypassing the recovery chokepoint with no consultant routing;
158
+ their own budgets (the final-completion continue-execution cap and the
159
+ review-round/convergence cap) already bound the autonomous effort. Their
160
+ resume semantics differ: site B's `blockedFromPhase`
161
+ (`final_completion_review`) is not in `RESUMABLE_BLOCKED_PHASES`, so
162
+ `neal resume` reports it as keep-blocked, while site C's top-level
163
+ plan-review block is recognized by the plan-review guidance path and resumes
164
+ via `neal resume --message`. Site C is the `topLevelMode !== 'execute'`
165
+ branch of `finalizeBlockedPlanReviewResponse`. Execute-mode
166
+ derived-plan-review blocks (`topLevelMode === 'execute'`) re-enter site A and
167
+ are handled there.
178
168
 
179
169
  ## Resume planning
180
170
 
@@ -95,12 +95,25 @@ commit. Create the initial baseline commit first.
95
95
 
96
96
  ## Stuck or blocked runs
97
97
 
98
- **Symptom:** the run stops with `status: "blocked"` (exit code 2).
99
- **Cause:** blocked is a controlled state, not a failure: the coder hit
100
- something it may not resolve alone, and attended runs wait for guidance.
101
- **Fix:** `neal status` explains the blocker and prints the exact
102
- `neal resume --run <run-id> --message "..."` command. `--message` is only
103
- accepted in that waiting-for-guidance state.
98
+ **Symptom:** the run stops waiting for the operator (exit code 2), either as
99
+ `status: "blocked"` or as a waiting-for-guidance recovery state.
100
+ **Cause:** an operator stop is a controlled state, not a failure: the run hit
101
+ something it may not resolve alone. Before yielding, eligible execute-mode
102
+ blocks coder-reported blocks and structural reviewer `review_stuck`
103
+ deadlocks get one bounded read-only consultant triage; a recoverable verdict
104
+ with a concrete directive is applied automatically and the run continues
105
+ without stopping. When the consultant is disabled
106
+ (`consultant_max_attempts: 0`), its per-scope budget is exhausted, the block
107
+ comes from an ineligible phase, or the consultant itself errors, the run
108
+ yields with no consultant advice. Advice, when there is any, is carried on
109
+ the stop.
110
+ **Fix:** follow `neal status` — recovery is site-specific, so what it prints
111
+ is the contract. A stop waiting for guidance prints the exact
112
+ `neal resume --run <run-id> --message "..."` command (`--message` is only
113
+ accepted there). Other blocked states — the final-completion review block,
114
+ for example — are not mechanically resumable: `neal resume` reports them as
115
+ still blocked, and `neal status` explains the blocker so you can address it
116
+ directly.
104
117
 
105
118
  **Symptom:** `effectiveStatus: "waiting_for_manual_gate"`.
106
119
  **Cause:** the scope reached expected human work. Instructions are in the
package/neal.yml CHANGED
@@ -93,12 +93,3 @@
93
93
  # # provider: openai-codex
94
94
  # # model: null
95
95
  # # effort: null
96
- #
97
- # # Run headlessly when no operator is available to answer an operator block
98
- # # (CI, cron, or a benchmark harness). When true, the execute-mode
99
- # # interactive-recovery loop auto-resumes with conservative guidance for a
100
- # # bounded number of turns and then fails cleanly, while the final-completion
101
- # # and top-level plan-review gates fail cleanly and terminally instead of
102
- # # waiting. Verification, authorization, and squash/grading are unchanged.
103
- # # The `--unattended` CLI flag overrides this key. Defaults to false.
104
- # unattended: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navels/neal",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "A source-first multi-agent CLI for planning, executing, reviewing, and resuming scoped code changes.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "author": "Lee Nave",
10
10
  "type": "module",
11
- "packageManager": "pnpm@11.21.0",
11
+ "packageManager": "pnpm@11.22.0",
12
12
  "homepage": "https://github.com/navels/neal#readme",
13
13
  "repository": {
14
14
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "engines": {
43
43
  "node": ">=24.19.0",
44
- "pnpm": ">=11.21.0"
44
+ "pnpm": ">=11.22.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "rm -rf dist && node node_modules/typescript-7/bin/tsc -p tsconfig.json && chmod +x dist/neal/index.js",
@@ -55,10 +55,10 @@
55
55
  "typecheck": "node node_modules/typescript-7/bin/tsc --noEmit -p tsconfig.json && node node_modules/typescript-7/bin/tsc -p tsconfig.test.json"
56
56
  },
57
57
  "dependencies": {
58
- "@ai-sdk/openai-compatible": "3.0.30",
59
- "@anthropic-ai/claude-agent-sdk": "0.3.237",
60
- "@openai/codex-sdk": "0.148.0",
61
- "ai": "7.0.65",
58
+ "@ai-sdk/openai-compatible": "3.0.34",
59
+ "@anthropic-ai/claude-agent-sdk": "0.3.238",
60
+ "@openai/codex-sdk": "0.149.0",
61
+ "ai": "7.0.74",
62
62
  "dotenv": "^17.4.2",
63
63
  "yaml": "^2.9.0",
64
64
  "zod": "4.4.3"
@@ -66,7 +66,7 @@
66
66
  "devDependencies": {
67
67
  "@eslint/js": "^10.0.1",
68
68
  "@types/node": "^24.13.3",
69
- "eslint": "^10.8.1",
69
+ "eslint": "^10.9.0",
70
70
  "tsx": "^4.23.12",
71
71
  "typescript": "^6.0.3",
72
72
  "typescript-7": "npm:typescript@^7.0.2",