@navels/neal 0.6.3 → 0.6.5

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.
@@ -556,6 +556,7 @@ export async function runCoderPlanRound(args) {
556
556
  sessionHandle,
557
557
  finalResponse,
558
558
  marker,
559
+ blockedReason: normalizedPayload.action === 'blocked' ? normalizedPayload.blockedReason.trim() || null : null,
559
560
  };
560
561
  }
561
562
  const coder = getCoderAdapter(args.coder);
@@ -603,6 +604,7 @@ export async function runCoderPlanRound(args) {
603
604
  sessionHandle,
604
605
  finalResponse,
605
606
  marker,
607
+ blockedReason: null,
606
608
  };
607
609
  }
608
610
  function renderCoderPlanPayload(payload, marker) {
@@ -9,6 +9,7 @@ import { runOnePass } from '../orchestrator.js';
9
9
  import { writeExecutionArtifacts } from '../orchestrator/artifacts.js';
10
10
  import { countOpenNonBlockingFindings, determinePlanRefinementConvergence, formatPlanRefinementSummary, isPlanRefinementState, } from '../plan-refinement.js';
11
11
  import { acquireActiveRunLock, releaseActiveRunLockSync } from '../run-lock.js';
12
+ import { planResumeActions } from '../resume-planner.js';
12
13
  import { formatPublicRunStatus, getRunDisplayStatus } from '../run-status.js';
13
14
  import { getCurrentScopeLabel } from '../scopes.js';
14
15
  import { resolveRunStatePath } from '../run-registry.js';
@@ -200,6 +201,12 @@ function armShutdownWatchdog(finalState, logger) {
200
201
  });
201
202
  };
202
203
  }
204
+ // Keep a crash message readable on the console; `neal status` carries the
205
+ // full text from the `phase.error` event.
206
+ function truncateFailureMessage(message) {
207
+ const trimmed = message.trim();
208
+ return trimmed.length > 1000 ? `${trimmed.slice(0, 1000)}…` : trimmed;
209
+ }
203
210
  function formatFinalSummaryLine(finalState, publicStatus) {
204
211
  if (publicStatus === 'waiting_for_manual_gate') {
205
212
  const gate = finalState.manualGate;
@@ -255,6 +262,9 @@ function formatFinalNextAction(finalState, displayStatus, runId, guidance) {
255
262
  return `Inspect failure: neal status --run ${runId}; resume when ready: neal resume --run ${runId}`;
256
263
  }
257
264
  if (finalState.status === 'blocked') {
265
+ if (planResumeActions(finalState).some((action) => action.kind === 'restore_resumable_blocked_phase')) {
266
+ return `Address the reason above, then re-run ${formatPublicPhase(finalState.blockedFromPhase ?? finalState.phase)}: neal resume --run ${runId}`;
267
+ }
258
268
  return `Inspect blocked run: neal status --run ${runId}; provide guidance with neal resume --run ${runId} --message "..." only if the run is waiting for operator guidance`;
259
269
  }
260
270
  if (displayStatus.effectiveStatus === 'paused') {
@@ -265,7 +275,7 @@ function formatFinalNextAction(finalState, displayStatus, runId, guidance) {
265
275
  }
266
276
  return `Continue this run: neal resume --run ${runId}`;
267
277
  }
268
- export function renderFinalRunOutput(finalState, statePath, displayStatus, autoSquashResult = null) {
278
+ export function renderFinalRunOutput(finalState, statePath, displayStatus, autoSquashResult = null, failureMessage = null) {
269
279
  const runId = basename(finalState.runDir);
270
280
  const publicStatus = formatPublicRunStatus(displayStatus);
271
281
  const findings = countFinalFindings(finalState);
@@ -312,6 +322,18 @@ export function renderFinalRunOutput(finalState, statePath, displayStatus, autoS
312
322
  if (guidance) {
313
323
  lines.push('', ...renderBlockedGuidanceSections(guidance));
314
324
  }
325
+ else if (finalState.blockerReason) {
326
+ // A block that is not waiting for `--message` guidance has no guidance
327
+ // section, so print the stored reason under the same heading.
328
+ lines.push('', '## Why Neal Stopped', finalState.blockerReason);
329
+ }
330
+ else if (finalState.status === 'failed' && failureMessage) {
331
+ // A crashed round is caught above and rendered as a failed result instead
332
+ // of being rethrown, so the error text has to be printed here or the
333
+ // console never shows it. `neal status` reads the same message from the
334
+ // `phase.error` event.
335
+ lines.push('', '## Why Neal Stopped', truncateFailureMessage(failureMessage));
336
+ }
315
337
  lines.push('', '## Next Action', `- ${formatFinalNextAction(finalState, displayStatus, runId, guidance)}`);
316
338
  return lines.join('\n');
317
339
  }
@@ -411,6 +433,7 @@ export async function executeRun(state, statePath, logger, options = {}) {
411
433
  writeDiagnostic(renderInteractiveKeyHint(allowStopRequest));
412
434
  }
413
435
  let finalState;
436
+ let runFailureMessage = null;
414
437
  try {
415
438
  try {
416
439
  finalState = await runOnePass(state, statePath, logger, {
@@ -435,6 +458,7 @@ export async function executeRun(state, statePath, logger, options = {}) {
435
458
  if (!finalState) {
436
459
  throw error;
437
460
  }
461
+ runFailureMessage = error instanceof Error ? error.message : String(error);
438
462
  }
439
463
  }
440
464
  finally {
@@ -477,7 +501,7 @@ export async function executeRun(state, statePath, logger, options = {}) {
477
501
  status: finalState.status,
478
502
  runDir: finalState.runDir,
479
503
  });
480
- const finalOutput = renderFinalRunOutput(finalState, statePath, displayStatus, autoSquashResult);
504
+ const finalOutput = renderFinalRunOutput(finalState, statePath, displayStatus, autoSquashResult, runFailureMessage);
481
505
  process.stdout.write(finalOutput.trimEnd() + '\n');
482
506
  await logger.event('shutdown.final_output_written', {
483
507
  phase: finalState.phase,
@@ -211,17 +211,23 @@ export async function runCoderPlanPhase(state, statePath, logger) {
211
211
  }
212
212
  const completionProblem = getPlanningCompletionProblem(codex.marker);
213
213
  const dirtyWorktreeBlocker = await getPlanPhaseDirtyWorktreeBlocker(workingState, 'coder_plan');
214
+ const blocked = codex.marker === 'AUTONOMY_BLOCKED' || Boolean(completionProblem) || Boolean(dirtyWorktreeBlocker);
215
+ // Persist why the planner stopped so `neal status` and the run result can show
216
+ // it. A `coder_plan` block is never answerable via `neal resume --message`
217
+ // (that eligibility keys on `blockedFromPhase`), so a non-null reason here
218
+ // only informs the operator; it does not change resume behavior.
219
+ const reason = blocked
220
+ ? dirtyWorktreeBlocker ?? completionProblem ?? codex.blockedReason ?? 'The planner reported a blocker during plan refinement'
221
+ : null;
214
222
  const nextState = await saveState(statePath, {
215
223
  ...workingState,
216
224
  plannerSessionHandle: codex.sessionHandle,
217
225
  plannerSessionProtocol: codex.sessionHandle ? activePlannerSessionProtocol : null,
218
226
  lastScopeMarker: codex.marker,
219
- phase: codex.marker === 'AUTONOMY_BLOCKED' || completionProblem || dirtyWorktreeBlocker ? 'blocked' : 'reviewer_plan',
220
- status: codex.marker === 'AUTONOMY_BLOCKED' || completionProblem || dirtyWorktreeBlocker ? 'blocked' : 'running',
221
- blockedFromPhase: codex.marker === 'AUTONOMY_BLOCKED' || completionProblem || dirtyWorktreeBlocker ? 'coder_plan' : null,
222
- // The initial coder_plan authoring block never carries the recoverable
223
- // coder-response blocker reason; keep it cleared whether we advance or block.
224
- blockerReason: null,
227
+ phase: blocked ? 'blocked' : 'reviewer_plan',
228
+ status: blocked ? 'blocked' : 'running',
229
+ blockedFromPhase: blocked ? 'coder_plan' : null,
230
+ blockerReason: reason,
225
231
  });
226
232
  await writeExecutionArtifacts(nextState);
227
233
  await logger?.event('phase.complete', {
@@ -230,8 +236,7 @@ export async function runCoderPlanPhase(state, statePath, logger) {
230
236
  sessionHandle: codex.sessionHandle,
231
237
  nextPhase: nextState.phase,
232
238
  });
233
- if (nextState.status === 'blocked') {
234
- const reason = dirtyWorktreeBlocker ?? completionProblem ?? 'The coder reported a blocker during plan revision';
239
+ if (nextState.status === 'blocked' && reason !== null) {
235
240
  if (nextState.topLevelMode !== 'execute') {
236
241
  await notifyBlocked(nextState, reason, logger);
237
242
  return nextState;
@@ -325,14 +325,16 @@ function renderLifecycleEvents(state, scopeDescriptor, options) {
325
325
  }
326
326
  if (state.status === 'blocked') {
327
327
  const blockedPhase = state.blockedFromPhase ?? state.phase;
328
+ const blockLine = formatScopeRelatedPhase(blockedPhase)
329
+ ? options.scopeIntroEstablished
330
+ ? `Run is blocked during ${formatPublicPhase(blockedPhase)}.`
331
+ : `Run is blocked during ${formatPublicPhase(blockedPhase)} for ${scopeReference}.`
332
+ : `Run is blocked during ${formatPublicPhase(blockedPhase)}.`;
333
+ const reason = (state.blockerReason ?? state.interactiveBlockedRecovery?.blockedReason ?? '').replace(/\s+/g, ' ').trim();
328
334
  return [
329
335
  {
330
336
  signature: `lifecycle:${runId}:blocked:${blockedPhase}`,
331
- line: formatScopeRelatedPhase(blockedPhase)
332
- ? options.scopeIntroEstablished
333
- ? `Run is blocked during ${formatPublicPhase(blockedPhase)}.`
334
- : `Run is blocked during ${formatPublicPhase(blockedPhase)} for ${scopeReference}.`
335
- : `Run is blocked during ${formatPublicPhase(blockedPhase)}.`,
337
+ line: reason ? `${blockLine} ${reason}` : blockLine,
336
338
  },
337
339
  ];
338
340
  }
@@ -52,7 +52,9 @@ is gated on it as a whole.
52
52
  opens and merges the release-preparation PR (version bump + changelog), and
53
53
  runs the Publish workflow through the npm 2FA approval. See
54
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
+ may skip the Renovate soak with a manual PR: `scripts/bump-native-sdks.sh`
56
+ opens one at the current latest releases. Qualify it the same way. Leave
57
+ Renovate's own SDK PR open; it closes itself once the manual bump merges.
56
58
 
57
59
  ## TypeScript 6 and 7 side by side
58
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navels/neal",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
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": {
@@ -56,8 +56,8 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@ai-sdk/openai-compatible": "3.0.39",
59
- "@anthropic-ai/claude-agent-sdk": "0.3.252",
60
- "@openai/codex-sdk": "0.151.0",
59
+ "@anthropic-ai/claude-agent-sdk": "0.3.260",
60
+ "@openai/codex-sdk": "0.153.2",
61
61
  "ai": "7.0.83",
62
62
  "dotenv": "^17.4.2",
63
63
  "yaml": "^2.9.0",