@navels/neal 0.4.3 → 0.5.1

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 (36) 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/review-convergence.md +7 -8
  33. package/docs/state-machine.md +53 -63
  34. package/docs/troubleshooting.md +19 -6
  35. package/neal.yml +0 -9
  36. package/package.json +7 -7
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
- Run headless (CI, cron, or a benchmark harness) with no operator available to
235
- answer an operator block:
236
-
237
- ```bash
238
- neal execute PLAN.md --unattended
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] [--unattended] <plan.md> [more-plans...]
301
- neal plan <plan.md> [--unattended]
302
- neal execute <plan.md> [--no-squash] [--unattended]
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
- `--unattended` runs `neal plan`, `neal execute`, or `neal run` without waiting
345
- for an operator. See [docs/state-machine.md](docs/state-machine.md) for unattended
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, { unattended: args.unattended }),
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. Exported so the unattended site-A auto-resume can synthesize
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> [--unattended]',
9
- ' or: neal execute <plan.md> [--no-squash] [--unattended]',
10
- ' or: neal run [--no-squash] [--unattended] <plan.md> [more-plans...]',
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 unattended = null;
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, { unattended: true });
366
+ const result = await executeRun(loaded.state, loaded.statePath, loaded.logger);
368
367
  finalStatus = result.finalState.status;
369
- unattended = result.finalState.unattended;
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
- unattended,
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 (hasEvent(events, 'unattended.block_unresolved')) {
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.finalStatus === null) {
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
- finalStatus: run.finalStatus,
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
- finalStatus: run.finalStatus,
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, getConfiguredUnattended } from '../config.js';
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, getConfiguredUnattended } from '../config.js';
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, exactly as `unattended` is. Legacy states
219
- // missing the field hydrate to true, keeping the historical behavior.
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 unattended from the resumed child's persisted state, exactly as
301
- // agentConfig is, so the queue continuation keeps headless runs headless.
302
- unattended: outcome.result.finalState.unattended,
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
@@ -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 resolved = mergeConfig(mergeConfig({}, readYamlFileIfPresent(sources.user.path)), readYamlFileIfPresent(sources.repo.path));
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
- : unattendedFinalCompletionBlock
595
- ? await persistUnattendedBlockUnresolvedFailure({
596
- ...baseState,
597
- phase: 'blocked',
598
- blockedFromPhase: 'final_completion_review',
599
- }, statePath, 'final_completion_review', logger)
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' && !state.unattended) {
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 an attended run whose active
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, persistUnattendedBlockUnresolvedFailure, printReviewResult, scheduleCoderFreshSessionRetry, shouldRetryCoderWithFreshSession, } from './shared.js';
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`. Attended and
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
- // Site C: a `dirty_worktree` safety block or a `reviewer_convergence`
145
- // cap/stall block. Attended runs leave `status:'blocked'`; unattended runs
146
- // have no operator to answer, so run the shared terminal-fail action. The
147
- // dirty-worktree case must not be silently converted to the recoverable
148
- // landing (the planner dirtied non-plan files with no operator to clean it),
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).