@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.
- package/README.md +18 -13
- package/dist/neal/adjudicator/execute.js +0 -1
- package/dist/neal/adjudicator/final-completion.js +0 -1
- package/dist/neal/adjudicator/planning.js +0 -1
- package/dist/neal/agents/rounds.js +1 -2
- package/dist/neal/blocked-guidance.js +1 -7
- package/dist/neal/cli.js +3 -19
- package/dist/neal/commands/compat.js +36 -13
- package/dist/neal/commands/new-run.js +1 -6
- package/dist/neal/commands/plan-and-execute.js +1 -5
- package/dist/neal/commands/resume-run.js +5 -7
- package/dist/neal/config.js +3 -11
- package/dist/neal/orchestrator/completion.js +7 -23
- package/dist/neal/orchestrator/notifications.js +1 -1
- package/dist/neal/orchestrator/phases/coder.js +0 -1
- package/dist/neal/orchestrator/phases/planning.js +7 -16
- package/dist/neal/orchestrator/phases/recovery.js +42 -285
- package/dist/neal/orchestrator/phases/shared.js +0 -26
- package/dist/neal/orchestrator/run-loop.js +1 -5
- package/dist/neal/orchestrator.js +0 -1
- package/dist/neal/plan-queue.js +5 -15
- package/dist/neal/prompts/execute.js +2 -4
- package/dist/neal/prompts/planning.js +1 -3
- package/dist/neal/prompts/shared.js +0 -7
- package/dist/neal/prompts/specialized.js +0 -2
- package/dist/neal/prompts/specs.js +25 -5
- package/dist/neal/state-invariants.js +0 -1
- package/dist/neal/state.js +0 -4
- package/docs/adjudicator-inventory.md +2 -3
- package/docs/architecture.md +1 -2
- package/docs/compat.md +17 -7
- package/docs/maintenance.md +7 -6
- package/docs/release.md +9 -0
- package/docs/review-convergence.md +7 -8
- package/docs/state-machine.md +53 -63
- package/docs/troubleshooting.md +19 -6
- package/neal.yml +0 -9
- 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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);
|
package/dist/neal/state.js
CHANGED
|
@@ -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
|
|
262
|
-
|
|
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
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
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'`
|
|
157
|
-
|
|
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`
|
|
182
|
-
|
|
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
|
|
202
|
-
escalated to
|
|
203
|
-
|
|
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
|
package/docs/maintenance.md
CHANGED
|
@@ -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
|
|
50
|
-
|
|
51
|
-
4. **Adopt.**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
232
|
-
|
|
233
|
-
|
|
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
|
package/docs/state-machine.md
CHANGED
|
@@ -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
|
|
80
|
-
|
|
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
|
-
##
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`
|
|
126
|
-
|
|
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`)
|
|
141
|
-
`
|
|
142
|
-
`
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
`
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
(`
|
|
168
|
-
|
|
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
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
59
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
60
|
-
"@openai/codex-sdk": "0.
|
|
61
|
-
"ai": "7.0.
|
|
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.
|
|
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",
|