@navels/neal 0.4.3 → 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/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 +6 -6
package/README.md
CHANGED
|
@@ -231,13 +231,11 @@ neal run tmp/A.md tmp/B.md
|
|
|
231
231
|
neal run --no-squash tmp/A.md tmp/B.md
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
neal run --unattended tmp/A.md tmp/B.md
|
|
240
|
-
```
|
|
234
|
+
When a run needs an operator it stops in a
|
|
235
|
+
controlled state and the command exits with code `2`; `neal status` says
|
|
236
|
+
whether it takes resume guidance or needs inspection. A harness with no
|
|
237
|
+
operator available (CI, cron, a benchmark driver) treats that exit as a
|
|
238
|
+
failure. See [docs/automation.md](docs/automation.md).
|
|
241
239
|
|
|
242
240
|
To run an example through neal after configuring providers:
|
|
243
241
|
|
|
@@ -297,9 +295,9 @@ neal check
|
|
|
297
295
|
neal compat [--model <slug>] [--role coder|reviewer|planner|all] [--reference openai-codex|anthropic-claude|openai-compatible:<slug>] [--json]
|
|
298
296
|
|
|
299
297
|
# Plan execution
|
|
300
|
-
neal run [--no-squash]
|
|
301
|
-
neal plan <plan.md>
|
|
302
|
-
neal execute <plan.md> [--no-squash]
|
|
298
|
+
neal run [--no-squash] <plan.md> [more-plans...]
|
|
299
|
+
neal plan <plan.md>
|
|
300
|
+
neal execute <plan.md> [--no-squash]
|
|
303
301
|
neal resume [--run <run-id>] [--message "..."]
|
|
304
302
|
|
|
305
303
|
# Plan-free review
|
|
@@ -341,9 +339,8 @@ work by default. Pass `--no-squash` to keep the per-scope commits.
|
|
|
341
339
|
needs guidance or manual work, `neal resume` and `neal status` explain what is
|
|
342
340
|
needed and print the command to continue.
|
|
343
341
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
blocks, manual gates, and resume behavior, and [docs/plan-format.md](docs/plan-format.md)
|
|
342
|
+
See [docs/state-machine.md](docs/state-machine.md) for operator blocks, manual
|
|
343
|
+
gates, and resume behavior, and [docs/plan-format.md](docs/plan-format.md)
|
|
347
344
|
for execution shapes and selected-plan handling.
|
|
348
345
|
|
|
349
346
|
### Plan-free review
|
|
@@ -383,6 +380,14 @@ use this shell contract:
|
|
|
383
380
|
- `3`: failed writer run or failed `neal run` queue after neal has run
|
|
384
381
|
state/result evidence.
|
|
385
382
|
|
|
383
|
+
`2` means the run stopped for operator intervention; `3` means a genuine
|
|
384
|
+
failure. A run that stops for an operator always exits `2`, never `3`. Not
|
|
385
|
+
every exit-2 stop resumes with a message: most accept `neal resume`
|
|
386
|
+
(optionally with `--message`), but some — a blocked final-completion review —
|
|
387
|
+
stay blocked and need `neal status` and artifact inspection instead. Harnesses
|
|
388
|
+
that need a hard verdict with no operator attached treat exit `2` as a failure
|
|
389
|
+
themselves.
|
|
390
|
+
|
|
386
391
|
Use `neal status --json` for the stable detailed automation interface. `neal
|
|
387
392
|
status` exits `0` when it successfully reports status, even if the reported run
|
|
388
393
|
is blocked, paused, waiting for guidance, waiting for a manual gate, or failed.
|
|
@@ -329,7 +329,6 @@ export async function runExecuteReviewerAdjudication(args) {
|
|
|
329
329
|
reviewerContext: await buildAndPersistReviewerContextPacket({ state: args.state }),
|
|
330
330
|
inlinedRangeDiff,
|
|
331
331
|
earlierScopeChanges,
|
|
332
|
-
unattended: args.state.unattended,
|
|
333
332
|
logger: args.logger,
|
|
334
333
|
});
|
|
335
334
|
return {
|
|
@@ -65,7 +65,6 @@ export async function runFinalCompletionReviewerAdjudication(args) {
|
|
|
65
65
|
scratchDir,
|
|
66
66
|
reviewerContext: await buildAndPersistReviewerContextPacket({ state: args.state }),
|
|
67
67
|
inlinedRangeDiff,
|
|
68
|
-
unattended: args.state.unattended,
|
|
69
68
|
logger: args.logger,
|
|
70
69
|
});
|
|
71
70
|
return {
|
|
@@ -325,7 +325,6 @@ export async function runPlanningReviewerAdjudication(args) {
|
|
|
325
325
|
reviewerContext: await buildAndPersistReviewerContextPacket({ state: args.state }),
|
|
326
326
|
reviewedPlanContent,
|
|
327
327
|
parentPlanContent,
|
|
328
|
-
unattended: args.state.unattended,
|
|
329
328
|
// Reinforce an author-declared `one_shot` only for the top-level authored plan; derived
|
|
330
329
|
// plans declare and own their own shape and are never clamped.
|
|
331
330
|
authoredOneShot: !context.derivedPlanReview && args.state.authoredExecutionShape === 'one_shot',
|
|
@@ -376,7 +376,7 @@ export async function runCoderScopeRound(args) {
|
|
|
376
376
|
const { sessionHandle, structured } = await runCoderStructuredPrompt({
|
|
377
377
|
coder: args.coder,
|
|
378
378
|
cwd: args.cwd,
|
|
379
|
-
prompt: buildScopePrompt(args.planDoc, progressText
|
|
379
|
+
prompt: buildScopePrompt(args.planDoc, progressText),
|
|
380
380
|
schema,
|
|
381
381
|
label: 'Coder scope round',
|
|
382
382
|
structuredJsonProtocol: buildStructuredJsonProtocolSpec({
|
|
@@ -512,7 +512,6 @@ export async function runCoderPlanRound(args) {
|
|
|
512
512
|
coder: args.coder,
|
|
513
513
|
cwd: args.cwd,
|
|
514
514
|
prompt: buildPlanningPrompt(args.planDoc, planDocument, {
|
|
515
|
-
unattended: args.unattended,
|
|
516
515
|
authoredOneShot: args.authoredOneShot,
|
|
517
516
|
}),
|
|
518
517
|
schema,
|
|
@@ -9,14 +9,8 @@ import { getRunStatePath } from './state.js';
|
|
|
9
9
|
const FALLBACK_REASON = 'Neal is waiting for operator guidance before it can continue.';
|
|
10
10
|
const PLAN_REVIEW_REASON = 'Plan review requires operator guidance before Neal can continue.';
|
|
11
11
|
// Conservative operator-facing message offered for the `unknown` blocked
|
|
12
|
-
// category.
|
|
13
|
-
// guidance in the same conservative spirit without forking the wording here.
|
|
12
|
+
// category.
|
|
14
13
|
export const CONTINUE_WITH_GUIDANCE_MESSAGE = 'Continue using this operator guidance. Keep existing verification requirements intact and do not assume any extra authorization.';
|
|
15
|
-
// Synthesized guidance recorded by the unattended execute-mode interactive
|
|
16
|
-
// recovery auto-resume (site A). Self-contained (no human operator text to echo
|
|
17
|
-
// back) and deliberately conservative: continue autonomously, keep every
|
|
18
|
-
// verification requirement, assume no extra authorization.
|
|
19
|
-
export const UNATTENDED_AUTO_RESUME_GUIDANCE = 'No operator is available. Continue autonomously using your best judgment. Keep all existing verification requirements intact and do not assume any extra authorization.';
|
|
20
14
|
const MISSING_EXTERNAL_PREREQUISITE_KEYWORDS = [
|
|
21
15
|
'credential',
|
|
22
16
|
'credentials',
|
package/dist/neal/cli.js
CHANGED
|
@@ -5,9 +5,9 @@ export function buildUsageLines(version) {
|
|
|
5
5
|
`neal ${version}`,
|
|
6
6
|
'',
|
|
7
7
|
'Usage: neal setup',
|
|
8
|
-
' or: neal plan <plan.md>
|
|
9
|
-
' or: neal execute <plan.md> [--no-squash]
|
|
10
|
-
' or: neal run [--no-squash]
|
|
8
|
+
' or: neal plan <plan.md>',
|
|
9
|
+
' or: neal execute <plan.md> [--no-squash]',
|
|
10
|
+
' or: neal run [--no-squash] <plan.md> [more-plans...]',
|
|
11
11
|
' or: neal resume [--run <run-id>] [--message "..."]',
|
|
12
12
|
' or: neal review [message] (--last <n> | --since <base>)',
|
|
13
13
|
' or: neal squash [plan.md]',
|
|
@@ -26,10 +26,8 @@ export function buildUsageLines(version) {
|
|
|
26
26
|
' neal plan tmp/PLAN.md',
|
|
27
27
|
' neal execute tmp/PLAN.md',
|
|
28
28
|
' neal execute tmp/PLAN.md --no-squash',
|
|
29
|
-
' neal execute tmp/PLAN.md --unattended',
|
|
30
29
|
' neal run tmp/PLAN.md',
|
|
31
30
|
' neal run --no-squash tmp/PLAN.md',
|
|
32
|
-
' neal run --unattended tmp/PLAN.md',
|
|
33
31
|
' neal resume',
|
|
34
32
|
' neal resume --run <run-id>',
|
|
35
33
|
' neal resume --run <run-id> --message "Use the narrower helper approach."',
|
|
@@ -100,7 +98,6 @@ export function parseNewRunArgs(args, defaults) {
|
|
|
100
98
|
}
|
|
101
99
|
const planDoc = requireSubcommandPlanPath(command, args[1]);
|
|
102
100
|
let squashOnCompletion = command === 'execute';
|
|
103
|
-
let unattended = false;
|
|
104
101
|
let index = 2;
|
|
105
102
|
while (index < args.length) {
|
|
106
103
|
const extra = args[index];
|
|
@@ -109,11 +106,6 @@ export function parseNewRunArgs(args, defaults) {
|
|
|
109
106
|
index += 1;
|
|
110
107
|
continue;
|
|
111
108
|
}
|
|
112
|
-
if (extra === '--unattended') {
|
|
113
|
-
unattended = true;
|
|
114
|
-
index += 1;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
109
|
if (extra?.startsWith('--')) {
|
|
118
110
|
throw new Error(`neal ${command} accepts exactly one plan file path argument; unsupported flag: ${extra}`);
|
|
119
111
|
}
|
|
@@ -124,7 +116,6 @@ export function parseNewRunArgs(args, defaults) {
|
|
|
124
116
|
planDoc,
|
|
125
117
|
agentConfig,
|
|
126
118
|
squashOnCompletion,
|
|
127
|
-
unattended,
|
|
128
119
|
};
|
|
129
120
|
}
|
|
130
121
|
export function parsePlanAndExecuteArgs(args, defaults) {
|
|
@@ -139,7 +130,6 @@ export function parsePlanAndExecuteArgs(args, defaults) {
|
|
|
139
130
|
};
|
|
140
131
|
let index = 1;
|
|
141
132
|
let squashOnCompletion = true;
|
|
142
|
-
let unattended = false;
|
|
143
133
|
while (index < args.length) {
|
|
144
134
|
const value = args[index];
|
|
145
135
|
if (value === '--no-squash') {
|
|
@@ -147,11 +137,6 @@ export function parsePlanAndExecuteArgs(args, defaults) {
|
|
|
147
137
|
index += 1;
|
|
148
138
|
continue;
|
|
149
139
|
}
|
|
150
|
-
if (value === '--unattended') {
|
|
151
|
-
unattended = true;
|
|
152
|
-
index += 1;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
140
|
if (value.startsWith('--')) {
|
|
156
141
|
throw new Error(`neal run accepts only plan file path operands; unsupported flag: ${value}`);
|
|
157
142
|
}
|
|
@@ -165,7 +150,6 @@ export function parsePlanAndExecuteArgs(args, defaults) {
|
|
|
165
150
|
planDocs,
|
|
166
151
|
agentConfig,
|
|
167
152
|
squashOnCompletion,
|
|
168
|
-
unattended,
|
|
169
153
|
};
|
|
170
154
|
}
|
|
171
155
|
export function parseReviewArgs(args) {
|
|
@@ -16,6 +16,7 @@ import { createAgentReviewFindingsProviderAdapter } from '../review-findings/pro
|
|
|
16
16
|
import { runNealReviewCli } from '../review-findings/run.js';
|
|
17
17
|
import { enableAgentSettingsIsolation } from '../providers/agent-settings-isolation.js';
|
|
18
18
|
import { isNealProviderError } from '../providers/types.js';
|
|
19
|
+
import { getRunDisplayStatus } from '../run-status.js';
|
|
19
20
|
import { getRunDir } from '../storage-paths.js';
|
|
20
21
|
import { verifyConfiguredProviders } from './check.js';
|
|
21
22
|
import { executeRun } from './runtime.js';
|
|
@@ -343,7 +344,7 @@ export async function runWriterFixture(options) {
|
|
|
343
344
|
const runDir = getRunDir(throwawayCwd, createRunId());
|
|
344
345
|
const planDoc = resolve(throwawayCwd, options.documentRelativePath);
|
|
345
346
|
let finalStatus = null;
|
|
346
|
-
let
|
|
347
|
+
let finalState = null;
|
|
347
348
|
let threwDuringRun = false;
|
|
348
349
|
let errorMessage = null;
|
|
349
350
|
let verifyExitCode = null;
|
|
@@ -353,10 +354,8 @@ export async function runWriterFixture(options) {
|
|
|
353
354
|
options.onPrepared?.({ throwawayCwd, runDir, planDoc });
|
|
354
355
|
const loaded = await loadOrInitialize(planDoc, throwawayCwd, options.candidateConfig, undefined, options.mode, {
|
|
355
356
|
runDir,
|
|
356
|
-
unattended: true,
|
|
357
357
|
allowedDirtyPaths: [planDoc],
|
|
358
358
|
});
|
|
359
|
-
unattended = loaded.state.unattended;
|
|
360
359
|
// executeRun renders its final run summary to process.stdout (runtime.ts).
|
|
361
360
|
// Capture and discard that here so it never leaks into the compat
|
|
362
361
|
// command's own stdout (the PASS/FAIL table / `--json` matrix). Footer and
|
|
@@ -364,9 +363,9 @@ export async function runWriterFixture(options) {
|
|
|
364
363
|
const realStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
365
364
|
process.stdout.write = (() => true);
|
|
366
365
|
try {
|
|
367
|
-
const result = await executeRun(loaded.state, loaded.statePath, loaded.logger
|
|
366
|
+
const result = await executeRun(loaded.state, loaded.statePath, loaded.logger);
|
|
368
367
|
finalStatus = result.finalState.status;
|
|
369
|
-
|
|
368
|
+
finalState = result.finalState;
|
|
370
369
|
}
|
|
371
370
|
catch (error) {
|
|
372
371
|
threwDuringRun = true;
|
|
@@ -389,7 +388,7 @@ export async function runWriterFixture(options) {
|
|
|
389
388
|
}
|
|
390
389
|
return {
|
|
391
390
|
finalStatus,
|
|
392
|
-
|
|
391
|
+
finalState,
|
|
393
392
|
runDir,
|
|
394
393
|
throwawayCwd,
|
|
395
394
|
events,
|
|
@@ -406,6 +405,19 @@ export async function runWriterFixture(options) {
|
|
|
406
405
|
// ---------------------------------------------------------------------------
|
|
407
406
|
// Failure-mode classification for writer runs
|
|
408
407
|
// ---------------------------------------------------------------------------
|
|
408
|
+
// An operator stop, derived purely from the run's final persisted state. The
|
|
409
|
+
// signal mirrors the writer exit-code-2 mapping (writer-exit-codes.ts): the run
|
|
410
|
+
// is structurally waiting for the operator (the interactive-recovery wait, or a
|
|
411
|
+
// pending-guidance view) or persisted `status: 'blocked'` (the final-completion
|
|
412
|
+
// and top-level plan-review gates).
|
|
413
|
+
export function isOperatorStopFinalState(state) {
|
|
414
|
+
const displayStatus = getRunDisplayStatus(state);
|
|
415
|
+
if (displayStatus.effectiveStatus === 'waiting_for_operator' ||
|
|
416
|
+
displayStatus.pendingOperatorGuidance) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
return state.status === 'blocked';
|
|
420
|
+
}
|
|
409
421
|
export function classifyWriterFailure(args) {
|
|
410
422
|
const { events } = args;
|
|
411
423
|
// Precedence follows the plan taxonomy (most specific first).
|
|
@@ -422,16 +434,13 @@ export function classifyWriterFailure(args) {
|
|
|
422
434
|
}
|
|
423
435
|
return 'provider_failed';
|
|
424
436
|
}
|
|
425
|
-
if (
|
|
426
|
-
return 'block_unresolved';
|
|
427
|
-
}
|
|
428
|
-
if (args.finalStatus === 'blocked') {
|
|
437
|
+
if (args.finalState !== null && isOperatorStopFinalState(args.finalState)) {
|
|
429
438
|
return 'block_unresolved';
|
|
430
439
|
}
|
|
431
440
|
if (hasEvent(events, 'phase.error')) {
|
|
432
441
|
return 'structured_output';
|
|
433
442
|
}
|
|
434
|
-
if (args.threwDuringRun || args.
|
|
443
|
+
if (args.threwDuringRun || args.finalState === null) {
|
|
435
444
|
return 'finalization_error';
|
|
436
445
|
}
|
|
437
446
|
// status 'failed' with no conclusive structural signal.
|
|
@@ -468,7 +477,7 @@ export async function evaluateCoderFixture(args) {
|
|
|
468
477
|
};
|
|
469
478
|
}
|
|
470
479
|
const failureMode = classifyWriterFailure({
|
|
471
|
-
|
|
480
|
+
finalState: run.finalState,
|
|
472
481
|
events: run.events,
|
|
473
482
|
threwDuringRun: run.threwDuringRun,
|
|
474
483
|
});
|
|
@@ -494,7 +503,7 @@ export async function evaluatePlannerFixture(args) {
|
|
|
494
503
|
});
|
|
495
504
|
if (run.finalStatus !== 'done') {
|
|
496
505
|
const failureMode = classifyWriterFailure({
|
|
497
|
-
|
|
506
|
+
finalState: run.finalState,
|
|
498
507
|
events: run.events,
|
|
499
508
|
threwDuringRun: run.threwDuringRun,
|
|
500
509
|
});
|
|
@@ -761,6 +770,19 @@ export function formatCompatTable(report) {
|
|
|
761
770
|
lines.push(`Overall: ${report.overallPass ? 'PASS' : 'FAIL'}`);
|
|
762
771
|
return lines.join('\n');
|
|
763
772
|
}
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
// Top-level orchestration
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
// Compat child runs must be structurally quiet: a child run that lands in an
|
|
777
|
+
// operator-stop state would otherwise invoke the operator's configured notify
|
|
778
|
+
// helper mid-matrix. `getNotifyBin` (config.ts) treats a defined-but-empty
|
|
779
|
+
// NEAL_NOTIFY_BIN as "notifications disabled", so setting the empty override at
|
|
780
|
+
// compat startup silences neal's own notifier for the whole process — the same
|
|
781
|
+
// process-env pattern as `enableAgentSettingsIsolation`, which only isolates
|
|
782
|
+
// the SDK adapters' settings, not neal's notifier.
|
|
783
|
+
export function suppressCompatRunNotifications() {
|
|
784
|
+
process.env.NEAL_NOTIFY_BIN = '';
|
|
785
|
+
}
|
|
764
786
|
export async function runCompat(args) {
|
|
765
787
|
const { cwd, parsed } = args;
|
|
766
788
|
// compat is a hermetic capability probe that runs the whole role matrix
|
|
@@ -768,6 +790,7 @@ export async function runCompat(args) {
|
|
|
768
790
|
// interactive config so the probe stays quiet (no per-turn notifier hooks)
|
|
769
791
|
// and repeatable. Normal neal runs never call this, so they honor the config.
|
|
770
792
|
enableAgentSettingsIsolation();
|
|
793
|
+
suppressCompatRunNotifications();
|
|
771
794
|
const deps = args.deps ?? {};
|
|
772
795
|
const compatDir = deps.compatDir ?? getCompatExamplesDir();
|
|
773
796
|
const manifest = deps.manifest ?? loadCompatManifest(compatDir);
|
|
@@ -2,7 +2,7 @@ import { stat } from 'node:fs/promises';
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { parseNewRunArgs } from '../cli.js';
|
|
5
|
-
import { assertWriterProvidersConfigured
|
|
5
|
+
import { assertWriterProvidersConfigured } from '../config.js';
|
|
6
6
|
import { assertGitRepositoryWithCommit } from '../git.js';
|
|
7
7
|
import { loadOrInitialize } from '../orchestrator.js';
|
|
8
8
|
import { assertAgentConfigSupportsWriterRun } from '../providers/registry.js';
|
|
@@ -34,9 +34,6 @@ export async function runNewRunCommand(args) {
|
|
|
34
34
|
};
|
|
35
35
|
assertAgentConfigSupportsWriterRun(parsed.agentConfig, { context: `new ${parsed.topLevelMode} writer run` });
|
|
36
36
|
const planDoc = resolve(cwd, parsed.planDoc);
|
|
37
|
-
// Flag overrides config: `--unattended` forces true, otherwise fall back to
|
|
38
|
-
// the resolved `agent.unattended` config value (default false).
|
|
39
|
-
const unattended = parsed.unattended || getConfiguredUnattended(cwd);
|
|
40
37
|
await assertGitRepositoryWithCommit(cwd, `neal ${parsed.topLevelMode}`);
|
|
41
38
|
await requireExistingPlanFile(planDoc, parsed.planDoc);
|
|
42
39
|
const result = await withPreparedWriterRun({
|
|
@@ -47,7 +44,6 @@ export async function runNewRunCommand(args) {
|
|
|
47
44
|
const loaded = await loadOrInitialize(planDoc, cwd, parsed.agentConfig, undefined, parsed.topLevelMode, {
|
|
48
45
|
allowedDirtyPaths: parsed.topLevelMode === 'execute' ? [planDoc] : [],
|
|
49
46
|
runDir: prepared.runDir,
|
|
50
|
-
unattended,
|
|
51
47
|
autoSquashOnCompletion: parsed.squashOnCompletion,
|
|
52
48
|
});
|
|
53
49
|
markInitialized();
|
|
@@ -57,7 +53,6 @@ export async function runNewRunCommand(args) {
|
|
|
57
53
|
// state is the single source of truth so `neal resume` sees the same
|
|
58
54
|
// preference this process does.
|
|
59
55
|
autoSquashOnCompletion: loaded.state.autoSquashOnCompletion,
|
|
60
|
-
unattended,
|
|
61
56
|
});
|
|
62
57
|
});
|
|
63
58
|
setWriterCommandExitCode(getExecuteRunResultExitCode(result));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import { parsePlanAndExecuteArgs } from '../cli.js';
|
|
3
|
-
import { assertWriterProvidersConfigured
|
|
3
|
+
import { assertWriterProvidersConfigured } from '../config.js';
|
|
4
4
|
import { assertGitRepositoryWithCommit } from '../git.js';
|
|
5
5
|
import { runPlanAndExecuteQueue } from '../plan-queue.js';
|
|
6
6
|
import { assertAgentConfigSupportsWriterRun } from '../providers/registry.js';
|
|
@@ -29,15 +29,11 @@ export async function runPlanAndExecuteCommand(args, deps) {
|
|
|
29
29
|
};
|
|
30
30
|
assertAgentConfigSupportsWriterRun(parsed.agentConfig, { context: 'plan-and-execute queue' });
|
|
31
31
|
await assertGitRepositoryWithCommit(cwd, 'neal run');
|
|
32
|
-
// Flag overrides config: `--unattended` forces true, otherwise fall back to
|
|
33
|
-
// the resolved `agent.unattended` config value (default false).
|
|
34
|
-
const unattended = parsed.unattended || getConfiguredUnattended(cwd);
|
|
35
32
|
const queueState = await runPlanAndExecuteQueue({
|
|
36
33
|
cwd,
|
|
37
34
|
planDocs: parsed.planDocs,
|
|
38
35
|
agentConfig: parsed.agentConfig,
|
|
39
36
|
squashOnCompletion: parsed.squashOnCompletion,
|
|
40
|
-
unattended,
|
|
41
37
|
deps,
|
|
42
38
|
});
|
|
43
39
|
setWriterCommandExitCode(getPlanAndExecuteQueueExitCode(queueState));
|
|
@@ -215,8 +215,8 @@ async function resumeManualGateRun(selection, lock) {
|
|
|
215
215
|
function executeResumedRun(state, statePath, logger) {
|
|
216
216
|
return executeRun(state, statePath, logger, {
|
|
217
217
|
// Honor the squash preference persisted at run creation (`--no-squash`)
|
|
218
|
-
// instead of re-deriving it
|
|
219
|
-
//
|
|
218
|
+
// instead of re-deriving it. Legacy states missing the field hydrate to
|
|
219
|
+
// true, keeping the historical behavior.
|
|
220
220
|
autoSquashOnCompletion: state.autoSquashOnCompletion,
|
|
221
221
|
});
|
|
222
222
|
}
|
|
@@ -297,11 +297,9 @@ async function applyResumeRunExitCode(outcome, deps) {
|
|
|
297
297
|
const queueState = await continueQueueFromChildRun({
|
|
298
298
|
childResult: outcome.result,
|
|
299
299
|
agentConfig: outcome.result.finalState.agentConfig,
|
|
300
|
-
// Source
|
|
301
|
-
// agentConfig is, so
|
|
302
|
-
|
|
303
|
-
// Likewise source the squash preference from the resumed child's
|
|
304
|
-
// persisted state so remaining queue items keep honoring --no-squash.
|
|
300
|
+
// Source the squash preference from the resumed child's persisted
|
|
301
|
+
// state, exactly as agentConfig is, so remaining queue items keep
|
|
302
|
+
// honoring --no-squash.
|
|
305
303
|
squashOnCompletion: outcome.result.finalState.autoSquashOnCompletion,
|
|
306
304
|
});
|
|
307
305
|
setWriterCommandExitCode(queueState
|
package/dist/neal/config.js
CHANGED
|
@@ -36,7 +36,6 @@ const DEFAULT_CONFIG = {
|
|
|
36
36
|
provider: 'anthropic-claude',
|
|
37
37
|
model: null,
|
|
38
38
|
},
|
|
39
|
-
unattended: false,
|
|
40
39
|
},
|
|
41
40
|
};
|
|
42
41
|
const cachedConfig = new Map();
|
|
@@ -161,9 +160,6 @@ function mergeConfig(base, override) {
|
|
|
161
160
|
...base.agent?.reviewer,
|
|
162
161
|
...override.agent?.reviewer,
|
|
163
162
|
},
|
|
164
|
-
// Scalar agent toggle: merged explicitly because the deep-merge above only
|
|
165
|
-
// covers the three provider sub-objects. Override wins when present.
|
|
166
|
-
unattended: override.agent?.unattended ?? base.agent?.unattended,
|
|
167
163
|
},
|
|
168
164
|
providers: {
|
|
169
165
|
openai_compatible: {
|
|
@@ -194,7 +190,9 @@ function loadConfigFile(cwd = process.cwd()) {
|
|
|
194
190
|
return cached;
|
|
195
191
|
}
|
|
196
192
|
const sources = getConfigSourceInfo(cacheKey);
|
|
197
|
-
const
|
|
193
|
+
const userConfig = readYamlFileIfPresent(sources.user.path);
|
|
194
|
+
const repoConfig = readYamlFileIfPresent(sources.repo.path);
|
|
195
|
+
const resolved = mergeConfig(mergeConfig({}, userConfig), repoConfig);
|
|
198
196
|
cachedConfig.set(cacheKey, resolved);
|
|
199
197
|
return resolved;
|
|
200
198
|
}
|
|
@@ -409,12 +407,6 @@ export function getDefaultCoderModel(cwd = process.cwd()) {
|
|
|
409
407
|
? null
|
|
410
408
|
: (parseStringValue(config.agent?.coder?.model) ?? DEFAULT_CONFIG.agent.coder.model);
|
|
411
409
|
}
|
|
412
|
-
export function getConfiguredUnattended(cwd = process.cwd()) {
|
|
413
|
-
const config = loadConfigFile(cwd);
|
|
414
|
-
return typeof config.agent?.unattended === 'boolean'
|
|
415
|
-
? config.agent.unattended
|
|
416
|
-
: DEFAULT_CONFIG.agent.unattended;
|
|
417
|
-
}
|
|
418
410
|
export function getDefaultPlannerProvider(cwd = process.cwd()) {
|
|
419
411
|
const config = loadConfigFile(cwd);
|
|
420
412
|
return (parseConfigProviderValue(config.agent?.planner?.provider, 'agent.planner.provider') ??
|
|
@@ -22,7 +22,6 @@ import { getFinalCompletionReviewerScratchDir, getScopeReviewerScratchDir } from
|
|
|
22
22
|
import { requireFinalCompletionView, resolveFinalCompletionReviewAction, } from '../state-views.js';
|
|
23
23
|
import { filterAllowedDirtyPathStatus, filterWrapperOwnedWorktreeStatus, formatDirtyWorktreeDiagnostic, } from '../worktree-status.js';
|
|
24
24
|
import { REVIEWER_CONTENT_REFUSED_BLOCK_REASON, shouldNotifyFailure } from './failures.js';
|
|
25
|
-
import { persistUnattendedBlockUnresolvedFailure } from './phases/shared.js';
|
|
26
25
|
async function writeFailedFinalCompletionReviewArtifact(args) {
|
|
27
26
|
const unstructuredOutput = await readFinalCompletionUnstructuredOutput({
|
|
28
27
|
runDir: args.state.runDir,
|
|
@@ -558,15 +557,6 @@ export async function runFinalCompletionReviewPhase(state, statePath, logger, ru
|
|
|
558
557
|
finalCompletionContinueExecutionCapReached: actionResolution.continueExecutionCapReached,
|
|
559
558
|
};
|
|
560
559
|
assertAdjudicationTransitionSignal(context.spec, actionResolution.effectiveAction, 'orchestrator:final_completion_review');
|
|
561
|
-
// Site B: the final-completion review gate. Under `unattended` a
|
|
562
|
-
// `block_for_operator` resolution (model-chosen or the forced
|
|
563
|
-
// continue-execution cap folded into `effectiveAction`) has no operator to
|
|
564
|
-
// answer, so run the shared terminal-fail action instead of saving
|
|
565
|
-
// `status:'blocked'`. The continue-execution cap already bounded the
|
|
566
|
-
// autonomous push, so there is no auto-resume. The aggregate diff is preserved
|
|
567
|
-
// unsubmitted by the shared action. Gate structurally on the persisted flag,
|
|
568
|
-
// never on guidance text.
|
|
569
|
-
const unattendedFinalCompletionBlock = actionResolution.effectiveAction === 'block_for_operator' && state.unattended;
|
|
570
560
|
const nextState = actionResolution.effectiveAction === 'accept_complete'
|
|
571
561
|
? await saveState(statePath, {
|
|
572
562
|
...baseState,
|
|
@@ -591,18 +581,12 @@ export async function runFinalCompletionReviewPhase(state, statePath, logger, ru
|
|
|
591
581
|
splitPlanCountForCurrentScope: 0,
|
|
592
582
|
blockedFromPhase: null,
|
|
593
583
|
})
|
|
594
|
-
:
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
: await saveState(statePath, {
|
|
601
|
-
...baseState,
|
|
602
|
-
phase: 'blocked',
|
|
603
|
-
status: 'blocked',
|
|
604
|
-
blockedFromPhase: 'final_completion_review',
|
|
605
|
-
});
|
|
584
|
+
: await saveState(statePath, {
|
|
585
|
+
...baseState,
|
|
586
|
+
phase: 'blocked',
|
|
587
|
+
status: 'blocked',
|
|
588
|
+
blockedFromPhase: 'final_completion_review',
|
|
589
|
+
});
|
|
606
590
|
await runtime.writeExecutionArtifacts(nextState);
|
|
607
591
|
await logger?.event('phase.complete', {
|
|
608
592
|
phase: 'final_completion_review',
|
|
@@ -618,7 +602,7 @@ export async function runFinalCompletionReviewPhase(state, statePath, logger, ru
|
|
|
618
602
|
const finalSubject = terminalScope?.commitSubject ?? 'Finalize scope work';
|
|
619
603
|
await notifyComplete(nextState, finalSubject, logger);
|
|
620
604
|
}
|
|
621
|
-
else if (actionResolution.effectiveAction === 'block_for_operator'
|
|
605
|
+
else if (actionResolution.effectiveAction === 'block_for_operator') {
|
|
622
606
|
const reason = getFinalCompletionReviewBlockReason({
|
|
623
607
|
reviewerAction: reviewerResult.verdict.action === 'accept_complete' ? 'block_for_operator' : reviewerResult.verdict.action,
|
|
624
608
|
effectiveAction: actionResolution.effectiveAction,
|
|
@@ -4,7 +4,7 @@ import { getCurrentScopeLabel, getExecutionPlanPath, getExecutionPlanScopeCount,
|
|
|
4
4
|
import { saveState } from '../state.js';
|
|
5
5
|
import { getDerivedPlanView } from '../state-views.js';
|
|
6
6
|
// Concise representation of the read-only consultant advice for the
|
|
7
|
-
// operator notification surface. Present only on
|
|
7
|
+
// operator notification surface. Present only on a run whose active
|
|
8
8
|
// interactive-blocked-recovery record carries consultant advice (knob > 0,
|
|
9
9
|
// budget available, eligible source phase). Returns '' otherwise so notification
|
|
10
10
|
// behavior for recovery states without advice — and terminal blocked
|
|
@@ -152,7 +152,6 @@ export async function runCoderScopePhase(state, statePath, logger) {
|
|
|
152
152
|
progressMarkdownPath: state.progressMarkdownPath,
|
|
153
153
|
sessionHandle: state.coderSessionHandle,
|
|
154
154
|
coderSessionProtocol: state.coderSessionProtocol,
|
|
155
|
-
unattended: state.unattended,
|
|
156
155
|
onSessionStarted: async (sessionHandle) => {
|
|
157
156
|
state.coderSessionHandle = sessionHandle;
|
|
158
157
|
state.coderSessionProtocol = activeCoderSessionProtocol;
|
|
@@ -18,7 +18,7 @@ import { isCoderTimeoutError, REVIEWER_CONTENT_REFUSED_BLOCK_REASON, shouldNotif
|
|
|
18
18
|
import { flushDerivedPlanNotifications, notifyBlocked, notifyComplete, } from '../notifications.js';
|
|
19
19
|
import { adoptAcceptedDerivedPlan, shouldNotifyDerivedPlanAcceptance, transitionPlanReviewWithoutOpenFindings, } from '../transitions.js';
|
|
20
20
|
import { enterInteractiveBlockedRecovery, shouldNotifyInteractiveBlockedRecoveryEntry } from './recovery.js';
|
|
21
|
-
import { bestEffortCleanupTimedOutCoder, persistCoderFailureState,
|
|
21
|
+
import { bestEffortCleanupTimedOutCoder, persistCoderFailureState, printReviewResult, scheduleCoderFreshSessionRetry, shouldRetryCoderWithFreshSession, } from './shared.js';
|
|
22
22
|
function getNormalizedPlanArtifactPath(state, planPath) {
|
|
23
23
|
const parsed = parse(planPath);
|
|
24
24
|
const extension = parsed.ext || '.md';
|
|
@@ -128,10 +128,7 @@ export async function finalizeBlockedPlanReviewResponse(state, statePath, derive
|
|
|
128
128
|
if (blockCause === 'coder_authored') {
|
|
129
129
|
// Recoverable landing for a coder-authored *response* block: persist the
|
|
130
130
|
// durable blocker reason and leave `status:'blocked'` (writer exit 2) for
|
|
131
|
-
// an operator to answer via `neal resume --message`.
|
|
132
|
-
// unattended land identically now — both blocked-with-reason — because
|
|
133
|
-
// neither could previously answer this block (attended left it blocked but
|
|
134
|
-
// reason-less; unattended terminal-failed).
|
|
131
|
+
// an operator to answer via `neal resume --message`.
|
|
135
132
|
const blockedState = await saveState(statePath, {
|
|
136
133
|
...state,
|
|
137
134
|
blockerReason: blocker,
|
|
@@ -141,16 +138,11 @@ export async function finalizeBlockedPlanReviewResponse(state, statePath, derive
|
|
|
141
138
|
}
|
|
142
139
|
return flushDerivedPlanNotifications(blockedState, statePath, logger, blocker);
|
|
143
140
|
}
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// and the convergence cap already bounded the autonomous push. Any plan
|
|
150
|
-
// artifact is preserved unsubmitted, exactly as today's failed runs leave it.
|
|
151
|
-
if (state.unattended) {
|
|
152
|
-
return persistUnattendedBlockUnresolvedFailure(state, statePath, 'reviewer_plan', logger);
|
|
153
|
-
}
|
|
141
|
+
// A `dirty_worktree` safety block or a `reviewer_convergence` cap/stall
|
|
142
|
+
// block: leave `status:'blocked'` (writer exit 2) without a durable
|
|
143
|
+
// blockerReason. The dirty-worktree case must not be silently converted to
|
|
144
|
+
// the blocked-with-reason landing (the planner dirtied non-plan files), and
|
|
145
|
+
// the convergence cap already bounded the autonomous push.
|
|
154
146
|
if (!derivedPlanReview) {
|
|
155
147
|
await notifyBlocked(state, blocker, logger);
|
|
156
148
|
}
|
|
@@ -185,7 +177,6 @@ export async function runCoderPlanPhase(state, statePath, logger) {
|
|
|
185
177
|
planDoc: state.planDoc,
|
|
186
178
|
sessionHandle: state.plannerSessionHandle,
|
|
187
179
|
coderSessionProtocol: state.plannerSessionProtocol,
|
|
188
|
-
unattended: state.unattended,
|
|
189
180
|
// Reinforce an author-declared `one_shot` only for the top-level authored plan; the
|
|
190
181
|
// coder_plan phase only runs for top-level `neal plan` refinement (derived-plan
|
|
191
182
|
// revisions run through coder_plan_response).
|