@navels/neal 0.3.3 → 0.4.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.
@@ -3,6 +3,7 @@ import { runCoderResponseRound, runReviewerRound } from '../agents.js';
3
3
  import { readOnlyReviewerNeedsInlinedDiff } from '../context/inline-review-context.js';
4
4
  import { buildAndPersistReviewerContextPacket } from '../context/reviewer-context.js';
5
5
  import { getReviewStuckWindow } from '../config.js';
6
+ import { getDiffForRangePaths } from '../git.js';
6
7
  import { EXECUTE_FINALIZATION_PHASE } from '../execute-finalization.js';
7
8
  import { classifyAlreadySatisfiedTopLevelScopeAcceptance, classifyEmptyDerivedParentAdvance, getExecutionPlanPath, getParentScopeLabel, renderRecentAcceptedScopesSummary, } from '../scopes.js';
8
9
  import { getScopeReviewerScratchDir } from '../storage-paths.js';
@@ -297,6 +298,11 @@ export async function runExecuteReviewerAdjudication(args) {
297
298
  const inlinedRangeDiff = readOnlyReviewerNeedsInlinedDiff(args.state.agentConfig.reviewer)
298
299
  ? await args.getDiffForRange(args.state.cwd, args.state.baseCommit, headCommit)
299
300
  : null;
301
+ const earlierScopeChanges = await collectEarlierScopeChanges({
302
+ state: args.state,
303
+ changedFiles,
304
+ getDiffForRangePaths: args.getDiffForRangePaths ?? getDiffForRangePaths,
305
+ });
300
306
  const reviewerResult = await (args.runReviewerRound ?? runReviewerRound)({
301
307
  reviewer: args.state.agentConfig.reviewer,
302
308
  // Resume the reviewer's own session. The handle persists across review
@@ -322,6 +328,7 @@ export async function runExecuteReviewerAdjudication(args) {
322
328
  scratchDir,
323
329
  reviewerContext: await buildAndPersistReviewerContextPacket({ state: args.state }),
324
330
  inlinedRangeDiff,
331
+ earlierScopeChanges,
325
332
  unattended: args.state.unattended,
326
333
  logger: args.logger,
327
334
  });
@@ -334,6 +341,37 @@ export async function runExecuteReviewerAdjudication(args) {
334
341
  reviewerResult,
335
342
  };
336
343
  }
344
+ // Files in the current scope's diff that an earlier accepted scope also
345
+ // changed, each paired with that earlier scope's diff restricted to the file.
346
+ // The reviewer session is the only long-lived memory across scopes, and its
347
+ // record of an earlier scope is the coder's summary; this turns "scope 3's
348
+ // file moved during scope 6" into something it reads instead of something it
349
+ // has to remember. Only accepted scopes count, and a scope replaced by a
350
+ // derived plan is skipped because its work was reset. Scopes with no
351
+ // recorded commit range are skipped.
352
+ export async function collectEarlierScopeChanges(args) {
353
+ const currentFiles = new Set(args.changedFiles);
354
+ const changes = [];
355
+ for (const scope of args.state.completedScopes) {
356
+ if (scope.result !== 'accepted' || scope.replacedByDerivedPlanPath || !scope.baseCommit || !scope.finalCommit) {
357
+ continue;
358
+ }
359
+ for (const file of scope.changedFiles) {
360
+ if (!currentFiles.has(file)) {
361
+ continue;
362
+ }
363
+ const diff = await args.getDiffForRangePaths(args.state.cwd, scope.baseCommit, scope.finalCommit, [file]);
364
+ changes.push({
365
+ file,
366
+ scopeNumber: scope.number,
367
+ baseCommit: scope.baseCommit,
368
+ finalCommit: scope.finalCommit,
369
+ diff,
370
+ });
371
+ }
372
+ }
373
+ return changes;
374
+ }
337
375
  function classifyReviewerParentAdvance(args) {
338
376
  if (args.meaningfulProgressAction !== 'advance_parent' && args.meaningfulProgressAction !== 'block_for_operator') {
339
377
  return null;
@@ -1,6 +1,16 @@
1
1
  import { NealProviderError } from '../providers/types.js';
2
2
  const SUMMARY_MAX_LENGTH = 800;
3
- const ORIGINAL_RESPONSE_MAX_LENGTH = 12000;
3
+ // Bound on the original response echoed into a repair prompt. A real
4
+ // `neal review` payload with nine findings ended at ~14,400 chars, so the old
5
+ // 12,000 cap handed the repair model a JSON object cut off mid-finding and it
6
+ // invented a "truncated" warning and dropped a finding (#11). 60,000 leaves
7
+ // about 4x headroom over that payload while still bounding a runaway response.
8
+ const ORIGINAL_RESPONSE_MAX_LENGTH = 60000;
9
+ // Any fenced block, with or without a language label, that is the only
10
+ // non-whitespace content of a response. Accepted with raw-JSON tolerance
11
+ // because repair turns render "raw whole-response JSON object" as a ```json
12
+ // fence and that payload is otherwise complete.
13
+ const GENERIC_OPENING_FENCE_PATTERN = /^[ \t]*```[ \t]*[A-Za-z0-9_-]*[ \t]*$/;
4
14
  const NEAL_JSON_OPENING_FENCE_PATTERN = /^[ \t]*```[ \t]*neal-json[ \t]*$/;
5
15
  const CLOSING_FENCE_PATTERN = /^[ \t]*```[ \t]*$/;
6
16
  export function buildStructuredJsonPrompt(basePrompt, protocol) {
@@ -67,12 +77,16 @@ export function guardStructuredJsonOutputFormatLines(lines, label) {
67
77
  export function extractStructuredJsonPayload(assistantText) {
68
78
  const blocks = findNealJsonBlocks(assistantText);
69
79
  if (blocks.length > 1) {
80
+ // Still a failure, but hand the first block's JSON to the repair prompt so
81
+ // the repair model works from the real payload instead of a truncated
82
+ // transcript. The first block is the one the prompt contract asked for;
83
+ // later blocks are usually echoes or rewrites of it (#11).
70
84
  return {
71
85
  ok: false,
72
86
  errorKind: 'multiple_control_blocks',
73
87
  errorSummary: `Expected exactly one final neal-json control block, but found ${blocks.length}.`,
74
88
  prose: assistantText.trim(),
75
- rawJson: null,
89
+ rawJson: blocks[0].rawJson,
76
90
  };
77
91
  }
78
92
  if (blocks.length === 1) {
@@ -96,6 +110,15 @@ export function extractStructuredJsonPayload(assistantText) {
96
110
  });
97
111
  }
98
112
  const trimmed = assistantText.trim();
113
+ const loneFencedJson = extractLoneFencedJson(trimmed);
114
+ if (loneFencedJson !== null) {
115
+ return parseExtractedJson({
116
+ source: 'raw-json',
117
+ prose: '',
118
+ rawJson: loneFencedJson,
119
+ malformedSummaryPrefix: 'The fenced JSON response was invalid',
120
+ });
121
+ }
99
122
  if (!looksLikeRawJsonObject(trimmed)) {
100
123
  return {
101
124
  ok: false,
@@ -524,6 +547,24 @@ function parseExtractedJson(args) {
524
547
  function looksLikeRawJsonObject(trimmed) {
525
548
  return trimmed.startsWith('{') || trimmed.startsWith('[');
526
549
  }
550
+ // Returns the inner text when the whole (trimmed) response is exactly one
551
+ // fenced block: an opening fence line, content with no fence lines inside, and
552
+ // a closing fence as the last line. Null otherwise, including when anything
553
+ // sits outside the fence; that case keeps its existing error.
554
+ function extractLoneFencedJson(trimmed) {
555
+ const lines = trimmed.split(/\r?\n/);
556
+ if (lines.length < 3) {
557
+ return null;
558
+ }
559
+ if (!GENERIC_OPENING_FENCE_PATTERN.test(lines[0]) || !CLOSING_FENCE_PATTERN.test(lines[lines.length - 1])) {
560
+ return null;
561
+ }
562
+ const inner = lines.slice(1, -1);
563
+ if (inner.some((line) => /^[ \t]*```/.test(line))) {
564
+ return null;
565
+ }
566
+ return inner.join('\n');
567
+ }
527
568
  function isJsonObject(value) {
528
569
  return value !== null && typeof value === 'object' && !Array.isArray(value);
529
570
  }
@@ -5,6 +5,10 @@ export const REVIEWER_CONTEXT_JSON = 'REVIEWER_CONTEXT.json';
5
5
  export const REVIEWER_CONTEXT_MARKDOWN = 'REVIEWER_CONTEXT.md';
6
6
  const COMPLETED_SCOPE_LIMIT = 12;
7
7
  const FINDING_LIMIT = 24;
8
+ // Files listed per completed scope. The full list stays in RUN_STATE.json;
9
+ // the packet carries enough for the reviewer to see which files an earlier
10
+ // scope owned, with an explicit count when the list is cut.
11
+ const COMPLETED_SCOPE_CHANGED_FILE_LIMIT = 20;
8
12
  export async function buildAndPersistReviewerContextPacket(args) {
9
13
  const packet = buildReviewerContextPacket(args);
10
14
  await mkdir(args.state.runDir, { recursive: true });
@@ -23,6 +27,8 @@ export function buildReviewerContextPacket(args) {
23
27
  marker: scope.marker,
24
28
  finalCommit: scope.finalCommit,
25
29
  summary: scope.summary ?? null,
30
+ changedFiles: scope.changedFiles.slice(0, COMPLETED_SCOPE_CHANGED_FILE_LIMIT),
31
+ changedFileCount: scope.changedFiles.length,
26
32
  reviewRounds: scope.reviewRounds,
27
33
  findings: scope.findings,
28
34
  residualReviewDebt: scope.residualReviewDebt?.length ?? 0,
@@ -110,7 +116,7 @@ function buildReviewerContextCitations(state) {
110
116
  }
111
117
  export function renderReviewerContextMarkdown(packet) {
112
118
  const scopeLines = packet.completedScopes.length
113
- ? packet.completedScopes.map((scope) => `- Scope ${scope.number}: ${scope.result}; marker=${scope.marker}; finalCommit=${scope.finalCommit ?? 'none'}; reviewRounds=${scope.reviewRounds}; findings=${scope.findings}; residualDebt=${scope.residualReviewDebt}; summary=${scope.summary ?? 'none'}`)
119
+ ? packet.completedScopes.map((scope) => `- Scope ${scope.number}: ${scope.result}; marker=${scope.marker}; finalCommit=${scope.finalCommit ?? 'none'}; reviewRounds=${scope.reviewRounds}; findings=${scope.findings}; residualDebt=${scope.residualReviewDebt}; files=${formatCompletedScopeFiles(scope)}; summary=${scope.summary ?? 'none'}`)
114
120
  : ['- none'];
115
121
  const findingLines = packet.findings.length
116
122
  ? packet.findings.map((finding) => `- ${finding.canonicalId} (${finding.id}): ${finding.severity}/${finding.status}; source=${finding.source}; files=${finding.files.join(', ') || 'none'}; claim=${finding.claim}; requiredAction=${finding.requiredAction}`)
@@ -157,6 +163,13 @@ export function renderReviewerContextMarkdown(packet) {
157
163
  ...packet.citations.map((citation) => `- ${citation.label}: ${citation.path}`),
158
164
  ].join('\n');
159
165
  }
166
+ function formatCompletedScopeFiles(scope) {
167
+ if (scope.changedFiles.length === 0) {
168
+ return 'none';
169
+ }
170
+ const omitted = scope.changedFileCount - scope.changedFiles.length;
171
+ return omitted > 0 ? `${scope.changedFiles.join(', ')} (+${omitted} more)` : scope.changedFiles.join(', ');
172
+ }
160
173
  function runIdFromDir(runDir) {
161
174
  const normalized = runDir.replace(/\/+$/, '');
162
175
  return normalized.slice(normalized.lastIndexOf('/') + 1);
package/dist/neal/git.js CHANGED
@@ -197,6 +197,14 @@ export async function getDiffForRange(cwd, base, head) {
197
197
  }
198
198
  return runGit(['diff', '--find-renames', `${base}..${head}`], cwd);
199
199
  }
200
+ // Range diff restricted to the given paths. Fixed argv with a `--` separator,
201
+ // never a shell string, so a path can never be read as a revision or option.
202
+ export async function getDiffForRangePaths(cwd, base, head, paths) {
203
+ if (base === head || paths.length === 0) {
204
+ return '';
205
+ }
206
+ return runGit(['diff', '--find-renames', `${base}..${head}`, '--', ...paths], cwd);
207
+ }
200
208
  export async function getChangedFilesForRange(cwd, base, head) {
201
209
  if (base === head) {
202
210
  return [];
@@ -1,7 +1,7 @@
1
1
  import { ReviewerRoundError } from '../../agents.js';
2
2
  import { runExecuteReviewerAdjudication, synthesizeExecuteReviewerState, } from '../../adjudicator/execute.js';
3
3
  import { assertAdjudicationTransitionSignal } from '../../adjudicator/specs.js';
4
- import { getChangedFilesForRange, getCommitRange, getDiffForRange, getDiffStatForRange, getHeadCommit, } from '../../git.js';
4
+ import { getChangedFilesForRange, getCommitRange, getDiffForRange, getDiffForRangePaths, getDiffStatForRange, getHeadCommit, } from '../../git.js';
5
5
  import { writeDetail } from '../../diagnostic.js';
6
6
  import { saveState } from '../../state.js';
7
7
  import { writeExecutionArtifacts } from '../artifacts.js';
@@ -41,6 +41,7 @@ export async function runReviewPhase(state, statePath, logger) {
41
41
  getDiffStatForRange,
42
42
  getChangedFilesForRange,
43
43
  getDiffForRange,
44
+ getDiffForRangePaths,
44
45
  }));
45
46
  reviewerSynthesis = synthesizeExecuteReviewerState({
46
47
  state,
@@ -1,4 +1,4 @@
1
- import { renderInlinedRangeDiffSection } from '../context/inline-review-context.js';
1
+ import { renderInlinedRangeDiffSection, truncateInlineSectionBody } from '../context/inline-review-context.js';
2
2
  import { AUTONOMY_BLOCKED, AUTONOMY_DONE, AUTONOMY_SCOPE_DONE, AUTONOMY_SPLIT_PLAN, buildProgressSection, getCanonicalPlanContractLines, getDerivedPlanSectionContractLines, getExecuteScopeProgressPayloadContractLines, getProtocolMarkerArtifactProhibitionLines, getStandalonePlanPayloadSourceOfTruthLines, getTerminalMarkerArtifactBoundaryLines, getUnattendedAutonomyLines, } from './shared.js';
3
3
  import { assertPromptBuilder } from './assert-builder.js';
4
4
  import { getUserGuidanceLines } from './guidance.js';
@@ -107,6 +107,11 @@ export function buildLegacyScopePrompt(planDoc, progressText) {
107
107
  `- ${AUTONOMY_SPLIT_PLAN}`,
108
108
  ].join('\n');
109
109
  }
110
+ export const EARLIER_SCOPE_CHANGES_SECTION_HEADING = '## Earlier-scope changes to files in this diff';
111
+ // Rendered for every execute-scope review, with or without an overlap: a
112
+ // tool-access reviewer can find earlier-scope history itself, and the rule
113
+ // about what that history means must not depend on whether Neal inlined it.
114
+ const EARLIER_SCOPE_PRESERVATION_LINE = "A change to a file that an earlier accepted scope signed off must preserve what that scope's review accepted. Weakening or removing a test, assertion, or check that an earlier scope introduced is a blocking finding unless the plan explicitly calls for it.";
110
115
  export function buildReviewerPrompt(args) {
111
116
  const spec = assertPromptBuilder('scope_reviewer', 'buildReviewerPrompt', PROMPT_MODULE_PATH);
112
117
  const primaryVariant = spec.variants.find((variant) => variant.kind === 'primary');
@@ -166,6 +171,7 @@ export function buildReviewerPrompt(args) {
166
171
  ...getFindingQualityLines(),
167
172
  ...skepticismLines,
168
173
  ...regressionLines,
174
+ EARLIER_SCOPE_PRESERVATION_LINE,
169
175
  ...preexistingLines,
170
176
  args.previousHeadCommit
171
177
  ? `Previous reviewer head was ${args.previousHeadCommit}. Focus especially on changes since that commit, while still considering the full current state.`
@@ -214,6 +220,30 @@ export function buildReviewerPrompt(args) {
214
220
  }),
215
221
  ]
216
222
  : []),
223
+ ...(args.earlierScopeChanges && args.earlierScopeChanges.length > 0
224
+ ? ['', renderEarlierScopeChangesSection(args.earlierScopeChanges)]
225
+ : []),
226
+ ].join('\n');
227
+ }
228
+ // Earlier accepted scopes' per-file diffs for files the current diff touches
229
+ // again. The body shares the inlined-diff bound (truncateInlineSectionBody),
230
+ // which appends an explicit truncation marker instead of dropping content
231
+ // silently.
232
+ function renderEarlierScopeChangesSection(changes) {
233
+ const body = changes
234
+ .map((change) => [
235
+ `### ${change.file} (scope ${change.scopeNumber}, ${change.baseCommit}..${change.finalCommit})`,
236
+ '',
237
+ change.diff.trim() === '' ? '(empty diff)' : change.diff,
238
+ ].join('\n'))
239
+ .join('\n\n');
240
+ return [
241
+ EARLIER_SCOPE_CHANGES_SECTION_HEADING,
242
+ '',
243
+ 'The files below were changed by an earlier accepted scope in this run and appear again in the current diff. Each entry shows what that earlier scope did to the file, so what the current scope alters is something you read here rather than something you have to remember.',
244
+ 'Check the current diff against each entry before accepting.',
245
+ '',
246
+ truncateInlineSectionBody(body),
217
247
  ].join('\n');
218
248
  }
219
249
  function getReviewerContextLines(reviewerContext) {
@@ -65,6 +65,7 @@ const SCOPE_REVIEWER_CONTEXT = context('ScopeReviewerPromptContext', [
65
65
  field('recentHistorySummary', 'review_history', true, 'Accepted-scope history for the active parent objective.'),
66
66
  field('reviewMarkdownPath', 'run_artifact', true, 'Review artifact that carries prior findings and coder responses.'),
67
67
  field('scratchDir', 'run_artifact', true, 'Run-local reviewer scratch directory for temporary verification artifacts.'),
68
+ field('earlierScopeChanges', 'repository_state', false, 'Files in the current scope diff that an earlier accepted scope also changed, each with that scope number, commit range, and per-file diff. Computed from completedScopes in run state; omitted when there is no overlap.'),
68
69
  field('accessMode', 'orchestrator_state', false, "Two-way reviewer doctrine access mode derived from the reviewer provider's structured-advisor tool access: 'tool-access' (inspect and execute) or 'read-only' (read tools only; no command execution, test runs, or scratch work). Defaults to 'tool-access' when absent."),
69
70
  ]);
70
71
  const COMPLETION_CODER_CONTEXT = context('CompletionCoderPromptContext', [
@@ -383,7 +384,7 @@ export const PROMPT_SPECS = [
383
384
  },
384
385
  {
385
386
  id: 'scope_reviewer',
386
- version: 2,
387
+ version: 3,
387
388
  changelog: [
388
389
  {
389
390
  version: 1,
@@ -393,6 +394,10 @@ export const PROMPT_SPECS = [
393
394
  version: 2,
394
395
  renderSha: '431a75ad341a531535606af607187239a31d12cf921575eb17774b317ad639a0',
395
396
  },
397
+ {
398
+ version: 3,
399
+ renderSha: '4b75fab01367f0e4e263bc7635f8753e8e73c08b3ca46cf6bb6e695a67da31a4',
400
+ },
396
401
  ],
397
402
  role: 'reviewer',
398
403
  purpose: 'Review execute-scope results for correctness, verification coverage, and meaningful progress toward the active parent objective.',
@@ -416,11 +421,13 @@ export const PROMPT_SPECS = [
416
421
  field('reviewMarkdownPath', 'run_artifact', true, 'Review history artifact path.'),
417
422
  field('progressJustification', 'review_history', true, 'Coder progress-justification payload.'),
418
423
  field('scratchDir', 'run_artifact', true, 'Run-local scratch directory for reviewer verification artifacts.'),
424
+ field('earlierScopeChanges', 'repository_state', false, 'Earlier accepted scopes\' per-file diffs for files the current diff touches again; absent when there is no overlap.'),
419
425
  ]),
420
426
  },
421
427
  providerVariants: SHARED_PROVIDER_VARIANTS,
422
428
  evaluationNotes: [
423
429
  'Render tests should assert reviewer prompts include shared adversarial falsification, verification skepticism, concrete finding-quality doctrine, meaningful-progress instructions, and parent-objective history.',
430
+ 'Render tests should assert the earlier-scope preservation line renders in every cell and the earlier-scope changes section renders only when an overlap is supplied.',
424
431
  'Future fixture cases should cover cases where local correctness differs from parent-objective convergence.',
425
432
  ],
426
433
  firstMigrationPriority: 2,
@@ -612,6 +612,7 @@ function buildClaudeCoreQueryOptions(spec) {
612
612
  ...(spec.model ? { model: spec.model } : {}),
613
613
  ...(spec.effort ? { effort: spec.effort } : {}),
614
614
  tools: spec.tools,
615
+ ...(spec.strictMcpConfig ? { strictMcpConfig: true } : {}),
615
616
  ...(spec.hooks ? { hooks: spec.hooks } : {}),
616
617
  // Under compat qualification only, run Claude in the SDK's isolation mode
617
618
  // (load no filesystem settings). Left unset, the SDK loads ~/.claude and any
@@ -658,6 +659,7 @@ function buildClaudeQueryOptions(args, defaultModel, claudeExecutablePath = getC
658
659
  model: args.model ?? defaultModel,
659
660
  effort: defaultEffort,
660
661
  tools: ['Read', 'Grep', 'Glob'],
662
+ strictMcpConfig: true,
661
663
  resumeHandle: args.resumeHandle,
662
664
  claudeExecutablePath,
663
665
  outputSchema: args.schema,
@@ -686,6 +688,7 @@ function buildClaudeJsonBlockQueryOptions(args, defaultModel, claudeExecutablePa
686
688
  model: args.model ?? defaultModel,
687
689
  effort: defaultEffort,
688
690
  tools: ['Read', 'Grep', 'Glob'],
691
+ strictMcpConfig: true,
689
692
  resumeHandle: args.resumeHandle,
690
693
  claudeExecutablePath,
691
694
  events: args.events,
@@ -707,6 +710,7 @@ function buildClaudeJsonBlockRepairQueryOptions(args, defaultModel, claudeExecut
707
710
  model: args.model ?? defaultModel,
708
711
  effort: defaultEffort,
709
712
  tools: [],
713
+ strictMcpConfig: true,
710
714
  claudeExecutablePath,
711
715
  events: args.events,
712
716
  stderrRole: 'structured-advisor',
@@ -108,45 +108,63 @@ export async function writeReviewFindingsFinal(paths, finalMarkdown, acceptedRou
108
108
  '',
109
109
  ].join('\n'));
110
110
  }
111
- // The reviewer/draft turns run through the Claude Agent SDK, whose sessions are
112
- // persisted to ~/.claude/projects but deliberately hidden from the interactive
113
- // `/resume` picker they are only resumable by id. Surface those ids so an
114
- // operator can reopen the reviewer's full context to ask "why did it conclude
115
- // X". Returns [] when no handles were captured (e.g. non-SDK/test providers).
111
+ // The draft and reviewer turns run through provider SDKs (Claude Agent SDK,
112
+ // Codex SDK). Those sessions are persisted by each CLI but hidden from its
113
+ // interactive resume picker, so they are only resumable by id. Surface the ids
114
+ // with the right command per provider so an operator can reopen the full
115
+ // context and ask "why did it conclude X". Returns [] when no handles were
116
+ // captured (e.g. non-SDK/test providers).
116
117
  export function formatReviewResumeSection(cwd, round) {
117
- const resumable = collectResumableHandles(round);
118
+ const resumable = collectResumableSessions(round);
118
119
  if (resumable.length === 0) {
119
120
  return [];
120
121
  }
121
122
  return [
122
123
  '## Resume Sessions',
123
124
  '',
124
- `These reviewer/coder turns ran through the Claude Agent SDK. Such sessions do not appear in the interactive \`/resume\` picker, but you can resume one by id from the reviewed directory (${cwd}):`,
125
+ `These draft/reviewer turns ran through provider SDKs. Their sessions do not appear in the interactive resume pickers, but you can resume one by id from the reviewed directory (${cwd}):`,
125
126
  '',
126
- ...resumable.map(([label, handle]) => `- ${label}: \`claude --resume ${handle}\``),
127
+ ...resumable.map((session) => `- ${formatResumeSessionLine(session)}`),
127
128
  ];
128
129
  }
129
130
  // Compact one-line resume hint for stdout, preferring the reviewer session
130
- // (the one most useful for interrogating the verdict). Null when no handle was
131
- // captured.
131
+ // (the one most useful for interrogating the verdict) and skipping any session
132
+ // whose provider has no known resume command. Null when nothing is resumable.
132
133
  export function formatReviewResumeStdoutLine(cwd, round) {
133
- const reviewer = nonEmptyHandle(round.reviewSessionHandle);
134
- const draft = nonEmptyHandle(round.draftSessionHandle);
135
- const handle = reviewer ?? draft;
136
- if (!handle) {
134
+ const session = collectResumableSessions(round).find((candidate) => resumeCommand(candidate) !== null);
135
+ if (!session) {
137
136
  return null;
138
137
  }
139
- const which = reviewer ? 'reviewer' : 'draft';
140
- return `Resume the ${which} session: (cd ${cwd} && claude --resume ${handle})`;
138
+ return `Resume the ${session.role} session: (cd ${cwd} && ${resumeCommand(session)})`;
139
+ }
140
+ // CLI resume commands by provider id. A provider missing here still gets its
141
+ // session id listed, just without a command. openai-compatible never surfaces
142
+ // handles (no session resume), so it never reaches this table.
143
+ const RESUME_COMMANDS = {
144
+ 'anthropic-claude': (handle) => `claude --resume ${handle}`,
145
+ 'openai-codex': (handle) => `codex resume ${handle}`,
146
+ };
147
+ function resumeCommand(session) {
148
+ const build = session.provider ? RESUME_COMMANDS[session.provider] : undefined;
149
+ return build ? build(session.handle) : null;
150
+ }
151
+ function formatResumeSessionLine(session) {
152
+ const command = resumeCommand(session);
153
+ const who = session.provider ? `${session.label} (${session.provider})` : session.label;
154
+ if (command) {
155
+ return `${who}: \`${command}\``;
156
+ }
157
+ return `${who}: session id \`${session.handle}\` (no known resume command for this provider)`;
141
158
  }
142
- function collectResumableHandles(round) {
143
- const entries = [
144
- ['Reviewer', round.reviewSessionHandle],
145
- ['Draft (coder)', round.draftSessionHandle],
159
+ function collectResumableSessions(round) {
160
+ const candidates = [
161
+ ['reviewer', 'Reviewer', round.reviewSessionHandle, round.reviewSessionProvider],
162
+ ['draft', 'Draft (coder)', round.draftSessionHandle, round.draftSessionProvider],
146
163
  ];
147
- return entries
148
- .map(([label, handle]) => [label, nonEmptyHandle(handle)])
149
- .filter((entry) => entry[1] !== null);
164
+ return candidates.flatMap(([role, label, handle, provider]) => {
165
+ const nonEmpty = nonEmptyHandle(handle);
166
+ return nonEmpty ? [{ role, label, handle: nonEmpty, provider: provider?.trim() ? provider : null }] : [];
167
+ });
150
168
  }
151
169
  function nonEmptyHandle(handle) {
152
170
  return typeof handle === 'string' && handle.trim() !== '' ? handle : null;
@@ -1,4 +1,15 @@
1
1
  const DIFF_PREVIEW_LIMIT = 12000;
2
+ // The accepted findings artifact is read by a human who was not part of the
3
+ // review and may not know the codebase, so both the draft fields and the
4
+ // reviewer's finalMarkdown must be written in plain language. Precision is not
5
+ // negotiable: paths, identifiers, numbers, and SHAs stay exact; only the
6
+ // wording around them gets simpler.
7
+ export const REVIEW_FINDINGS_PLAIN_LANGUAGE_RULES = [
8
+ 'Write for a human reader who was not part of this review and may not know the codebase: short sentences, one idea per sentence, everyday words.',
9
+ 'Do not use jargon, shorthand, or undefined abbreviations when a plain phrase says the same thing. When a technical term is necessary, say in plain words what it means for the reader the first time you use it.',
10
+ 'Keep exact file paths, identifiers, numbers, commit SHAs, and command names. Plain wording must not drop, blur, or soften a technical fact.',
11
+ 'State each claim as what happens and why it matters. State each requiredAction as a concrete step a maintainer can take without re-reading the diff.',
12
+ ];
2
13
  export const REVIEW_FINDINGS_READ_ONLY_RULES = [
3
14
  'Do not mutate the repository.',
4
15
  'Do not make commits, amend commits, rebase, reset, squash, or rewrite history.',
@@ -65,6 +76,10 @@ export function buildReviewFindingsDraftPrompt(context, draftContext = {}) {
65
76
  '## Read-Only Rules',
66
77
  '',
67
78
  ...REVIEW_FINDINGS_READ_ONLY_RULES.map((rule) => `- ${rule}`),
79
+ '',
80
+ '## Plain Language',
81
+ '',
82
+ ...REVIEW_FINDINGS_PLAIN_LANGUAGE_RULES.map((rule) => `- ${rule}`),
68
83
  ...(reviewFindings.length > 0
69
84
  ? [
70
85
  '',
@@ -86,19 +101,23 @@ export function buildReviewFindingsDraftPrompt(context, draftContext = {}) {
86
101
  '',
87
102
  renderContextSummary(context),
88
103
  '',
89
- 'Return a summary, concrete findings, and warnings only. Each finding needs severity, files, claim, evidence, and requiredAction. Do not suggest that Neal applied fixes.',
104
+ 'Return a summary, concrete findings, and warnings only. Each finding needs severity, files, claim, evidence, and requiredAction. Write the summary, every claim, evidence, requiredAction, and warning under the Plain Language rules above. Do not suggest that Neal applied fixes.',
90
105
  ].join('\n');
91
106
  }
92
107
  export function buildReviewFindingsReviewPrompt(context, draft, round = 1) {
93
108
  return [
94
109
  '# Neal Review Findings Review',
95
110
  '',
96
- `Review findings draft round ${round} for missing important findings, weak evidence, false positives, wrong severity, unclear required actions, and insufficient test or integration analysis.`,
111
+ `Review findings draft round ${round} for missing important findings, weak evidence, false positives, wrong severity, unclear required actions, wording that breaks the Plain Language rules below, and insufficient test or integration analysis.`,
97
112
  '',
98
113
  '## Read-Only Rules',
99
114
  '',
100
115
  ...REVIEW_FINDINGS_READ_ONLY_RULES.map((rule) => `- ${rule}`),
101
116
  '',
117
+ '## Plain Language',
118
+ '',
119
+ ...REVIEW_FINDINGS_PLAIN_LANGUAGE_RULES.map((rule) => `- ${rule}`),
120
+ '',
102
121
  '## Review Instruction',
103
122
  '',
104
123
  context.instruction,
@@ -111,7 +130,7 @@ export function buildReviewFindingsReviewPrompt(context, draft, round = 1) {
111
130
  '',
112
131
  renderDraftSummary(draft),
113
132
  '',
114
- 'Return verdict=`accepted` only when the final findings artifact is ready. Return verdict=`revise` with concrete findings when another draft is required. Return verdict=`blocked` only when a safe read-only review cannot be produced. Keep accepted finalMarkdown read-only and artifact-ready. Return empty strings for finalMarkdown or blockedReason when they do not apply.',
133
+ 'Return verdict=`accepted` only when the final findings artifact is ready. Return verdict=`revise` with concrete findings when another draft is required. Return verdict=`blocked` only when a safe read-only review cannot be produced. Keep accepted finalMarkdown read-only, artifact-ready, and written under the Plain Language rules above. Return empty strings for finalMarkdown or blockedReason when they do not apply.',
115
134
  ].join('\n');
116
135
  }
117
136
  function renderContextSummary(context) {
@@ -41,7 +41,7 @@ class AgentReviewFindingsProviderAdapter {
41
41
  }),
42
42
  logger: this.args.logger,
43
43
  });
44
- args.onSessionHandle?.(result.sessionHandle ?? null);
44
+ args.onSessionHandle?.(result.sessionHandle ?? null, this.args.agentConfig.coder.provider);
45
45
  return validateReviewFindingsDraft(result.structured);
46
46
  }
47
47
  async reviewDraft(args) {
@@ -94,7 +94,7 @@ class AgentReviewFindingsProviderAdapter {
94
94
  events,
95
95
  }),
96
96
  });
97
- args.onSessionHandle?.(result.sessionHandle ?? null);
97
+ args.onSessionHandle?.(result.sessionHandle ?? null, this.args.agentConfig.reviewer.provider);
98
98
  return validateReviewFindingsReview(result.structured);
99
99
  }
100
100
  }
@@ -74,14 +74,16 @@ export async function runNealReviewCli(args) {
74
74
  maxRounds,
75
75
  });
76
76
  let draftSessionHandle = null;
77
+ let draftSessionProvider = null;
77
78
  const draftResponse = await provider.draftFindings({
78
79
  context,
79
80
  round: roundNumber,
80
81
  previousDraft,
81
82
  reviewFindings,
82
83
  prompt: draftPrompt,
83
- onSessionHandle: (handle) => {
84
+ onSessionHandle: (handle, sessionProvider) => {
84
85
  draftSessionHandle = handle;
86
+ draftSessionProvider = sessionProvider ?? null;
85
87
  },
86
88
  });
87
89
  await assertReviewReadOnlyStateUnchanged(cwd, beforeState);
@@ -97,13 +99,15 @@ export async function runNealReviewCli(args) {
97
99
  });
98
100
  const reviewPrompt = buildReviewFindingsReviewPrompt(context, draft, roundNumber);
99
101
  let reviewSessionHandle = null;
102
+ let reviewSessionProvider = null;
100
103
  const reviewResponse = await provider.reviewDraft({
101
104
  context,
102
105
  round: roundNumber,
103
106
  draft,
104
107
  prompt: reviewPrompt,
105
- onSessionHandle: (handle) => {
108
+ onSessionHandle: (handle, sessionProvider) => {
106
109
  reviewSessionHandle = handle;
110
+ reviewSessionProvider = sessionProvider ?? null;
107
111
  },
108
112
  });
109
113
  await assertReviewReadOnlyStateUnchanged(cwd, beforeState);
@@ -118,7 +122,9 @@ export async function runNealReviewCli(args) {
118
122
  // Only record handles when captured, so artifacts stay byte-identical
119
123
  // for non-SDK providers.
120
124
  ...(draftSessionHandle ? { draftSessionHandle } : {}),
125
+ ...(draftSessionHandle && draftSessionProvider ? { draftSessionProvider } : {}),
121
126
  ...(reviewSessionHandle ? { reviewSessionHandle } : {}),
127
+ ...(reviewSessionHandle && reviewSessionProvider ? { reviewSessionProvider } : {}),
122
128
  };
123
129
  rounds.push(loopRound);
124
130
  await writeReviewFindingsEvent(paths, 'review.review_completed', {
@@ -56,7 +56,7 @@ All schema targets are `structured_json` with provider surface
56
56
  | `plan_author` | `buildPlanningPrompt`, `buildCoderPlanResponsePrompt` (`reviewMode=plan`, `reviewMode=derived-plan`) | `runCoderPlanRound`, `runCoderPlanResponseRound` | Primary planning: `buildCoderPlanSchema` / `validateCoderPlanPayload`. Response rounds: `buildCoderPlanResponseSchema` / `validateCoderPlanResponsePayload`. | Primary planning routes new/resumed structured sessions by persisted `plannerSessionProtocol`. Legacy marker parsing is retained only for active `legacy_marker_v1` sessions. |
57
57
  | `plan_reviewer` | `buildPlanReviewerPrompt` (`mode=plan`, `mode=derived-plan`) | `runPlanReviewerRound` | `buildPlanReviewerSchema` / `PlanReviewerPayload` | Execution-shape confirmation is part of the contract. Reviews material approach, scope, sequencing, and verification defects without turning the plan into an implementation inventory. |
58
58
  | `scope_coder` | `buildScopePrompt`, `buildCoderResponsePrompt` | `runCoderScopeRound`, `runCoderResponseRound` | Primary execution: `buildCoderScopeSchema` / `validateCoderScopePayload`. Response rounds: `buildCoderResponseSchema` / `validateCoderResponsePayload`. | Primary execution routes new/resumed structured sessions by persisted `coderSessionProtocol`. Legacy marker and progress-payload parsing is retained only for active `legacy_marker_v1` sessions. Also carries an `adjacent`-status blocked-recovery `response` variant (see below). |
59
- | `scope_reviewer` | `buildReviewerPrompt` | `runReviewerRound` | `buildReviewerSchema` / `ReviewerPayload` | Execute-scope review only. `neal review` external ranges use the separate read-only review-findings loop. Meaningful-progress remains a capability variant of `scope_reviewer`, not a new top-level id. Context includes a run-local `scratchDir`, but read-only reviewer prompts omit it. |
59
+ | `scope_reviewer` | `buildReviewerPrompt` | `runReviewerRound` | `buildReviewerSchema` / `ReviewerPayload` | Execute-scope review only. `neal review` external ranges use the separate read-only review-findings loop. Meaningful-progress remains a capability variant of `scope_reviewer`, not a new top-level id. Context includes a run-local `scratchDir`, but read-only reviewer prompts omit it. Context also includes `earlierScopeChanges` when the current diff touches a file an earlier accepted scope changed (see below). |
60
60
  | `completion_coder` | `buildFinalCompletionSummaryPrompt` | `runCoderFinalCompletionSummaryRound` | `buildFinalCompletionSummarySchema` / `parseFinalCompletionSummaryPayload` | Structured advisor round, but still a coder-owned role/task. The completion packet includes aggregate review context when neal can compute it. |
61
61
  | `completion_reviewer` | `buildFinalCompletionReviewerPrompt` | `runReviewerFinalCompletionRound` | `buildFinalCompletionReviewerSchema` / `parseFinalCompletionReviewerPayload` | Whole-plan aggregate review remains distinct from ordinary scope review and keeps its final-completion verdict schema. Context includes a run-local `scratchDir`, but read-only reviewer prompts omit it. |
62
62
  | `consultant` | `buildConsultantPrompt` | `runConsultantRound` | `buildConsultantSchema` / `validateConsultantVerdictPayload` | Single no-read-safe variant for the read-only consultant. It judges entirely from neal-inlined context and its static instructions pass the shared no-read guard. |
@@ -125,6 +125,26 @@ When adding or changing a prompt spec:
125
125
 
126
126
  Final completion has one additional context assembly rule: `buildFinalCompletionPacket()` includes `aggregateReviewContext` for the whole implementation range from `initialBaseCommit` to the resolved final commit. When the range can be read, the packet carries commit subjects, diff stat, and changed files. When it cannot, it carries an explicit `unavailableReason` so the reviewer treats the missing aggregate range as evidence to consider instead of silently accepting completion.
127
127
 
128
+ Execute-scope review has one more context assembly rule. The reviewer session
129
+ persists across scopes while the coder session resets, so the reviewer is the
130
+ only participant that remembers earlier scopes, and its record of each one is
131
+ the coder's own summary in the continuity packet. To keep that memory honest,
132
+ `runExecuteReviewerAdjudication` computes `earlierScopeChanges`: for every file
133
+ in the current scope diff that an earlier accepted scope also changed (from
134
+ `completedScopes[].changedFiles` in run state; blocked scopes and scopes
135
+ replaced by a derived plan are skipped), it collects that earlier scope's diff
136
+ restricted to the file (`git diff <scope.baseCommit>..<scope.finalCommit> --
137
+ <file>`, a fixed-argv helper in `src/neal/git.ts`). `buildReviewerPrompt`
138
+ renders them under "Earlier-scope changes to files in this diff", bounded by
139
+ the same inline-section limit as the inlined range diff, and renders nothing
140
+ when there is no overlap. The prompt always carries the matching doctrine line:
141
+ a change to a file an earlier accepted scope signed off must preserve what that
142
+ review accepted, and weakening or removing a test, assertion, or check an
143
+ earlier scope introduced is a blocking finding unless the plan calls for it.
144
+ The continuity packet lists each completed scope's changed files (capped per
145
+ scope, with a count of what was cut) so the reviewer can also see which files
146
+ an earlier scope owned.
147
+
128
148
  Execute-scope and final-completion reviewer context includes a deterministic
129
149
  run-local `scratchDir` under `.neal/runs/<run-id>/scratch/`. A `tool-access`
130
150
  prompt tells the reviewer to use that directory for temporary verification
package/docs/providers.md CHANGED
@@ -104,8 +104,10 @@ Current built-in capabilities are intentionally conservative:
104
104
 
105
105
  The `coder` capability describes adapter paths, not a global promise that every
106
106
  writer-run workflow is read-only: writer-run coder turns use provider SDKs with
107
- broad local permissions. The separate public `neal review` command enforces its
108
- own read-only boundary in the review command flow.
107
+ broad local permissions. That includes the public `neal review` command's
108
+ draft turn, which runs on the coder capability with full tools and is
109
+ read-only by prompt rules and an after-the-fact worktree check, not by
110
+ mechanism (see [Review boundary](#review-boundary)).
109
111
 
110
112
  ### Coder tool policy enforcement
111
113
 
@@ -151,6 +153,35 @@ Two consequences follow from no reviewer holding shell access:
151
153
  - Reviewers never mutate the checkout. A review produces a verdict and findings,
152
154
  not edits.
153
155
 
156
+ ### What the invariant does and does not cover
157
+
158
+ The invariant covers the tools neal itself hands the reviewer. Beyond that
159
+ it is best effort. What is enforced mechanically today, per provider:
160
+
161
+ - `anthropic-claude`: the SDK `tools` allowlist is exactly `Read`, `Grep`,
162
+ `Glob` (empty for the prompt-only repair turn), and `strictMcpConfig` is set
163
+ so the SDK ignores MCP servers from the operator's user settings, plugins,
164
+ and project `.mcp.json`. Without that flag those servers load into the
165
+ reviewer session on top of the allowlist, and they often include tools that
166
+ write (Jira edits, Drive file writes, a browser).
167
+ - `openai-codex`: every structured-advisor thread runs under the Codex
168
+ `read-only` sandbox. That sandbox covers shell and filesystem access. neal
169
+ does not restrict MCP servers configured in the operator's own Codex config,
170
+ and whether the sandbox applies to them is up to Codex, not neal.
171
+ - `openai-compatible`: the reviewer only ever sees neal's own read-only
172
+ toolset. There is no MCP or other tool path into that loop.
173
+
174
+ What is not covered, on any provider:
175
+
176
+ - The reviewer runs in the same checkout as the coder. There is no separate
177
+ worktree. Read-only comes from which tools the reviewer has, not from a
178
+ separate copy of the tree.
179
+ - Prompt instructions ("do not mutate the repository") are instructions to
180
+ the model, not enforcement. A read-only claim that rests only on prompt text
181
+ is best effort.
182
+ - Anything the provider adds on its own (its own configured tools,
183
+ extensions, or network access) is outside the invariant.
184
+
154
185
  Usage reporting is `opportunistic`: providers emit usage only when the SDK
155
186
  event or result supplies it.
156
187
 
@@ -311,9 +342,13 @@ neal's shared `neal-json-block-v1` runtime appends provider-neutral transport
311
342
  instructions requiring optional useful prose followed by exactly one final
312
343
  fenced `neal-json` JSON block. It then extracts, parses, validates, and repairs
313
344
  that control object with the caller-supplied schema label, schema, validator,
314
- and repair limit. Raw whole-response JSON objects are accepted only as a
315
- compatibility tolerance for older mocks and pre-migration paths. The prompt
316
- contract remains prose plus one final `neal-json` block. State-facing
345
+ and repair limit. Raw whole-response JSON objects, and a single fenced JSON
346
+ object (any fence label) that is the whole response, are accepted only as a
347
+ compatibility tolerance for older mocks, pre-migration paths, and repair turns
348
+ that render the payload as a ```json fence. The prompt contract remains prose
349
+ plus one final `neal-json` block. When a response carries more than one
350
+ `neal-json` block it is still rejected, but the first block's JSON is passed
351
+ to the repair prompt so repair works from the real payload. State-facing
317
352
  `structured_output_received` telemetry is emitted only after neal validation
318
353
  succeeds.
319
354
 
@@ -782,17 +817,22 @@ Two distinct read-only guarantees apply here, and they should not be conflated:
782
817
  - The provider-capability invariant (see
783
818
  [The read-only reviewer invariant](#the-read-only-reviewer-invariant)):
784
819
  every supported `structured-advisor` capability has `write: false` and
785
- `shell: false`, so a writer-run reviewer/structured-advisor round cannot
786
- write or run shell by capability. This holds for both writer runs and
787
- `neal review`.
788
- - The `neal review` command's additional artifact-boundary guard: the command
789
- checks protected writer state and worktree changes after provider rounds and
790
- fails if anything outside its review artifacts changed. This post-round guard
791
- is owned by the `neal review` command flow, not by a provider capability, and
792
- is specific to `neal review`. A writer run does not perform this artifact
793
- diff.
794
-
795
- So the capability invariant guarantees reviewers never write or run shell in
796
- any mode, while `neal review` adds its own command-level enforcement on top.
797
- Do not treat the `neal review` artifact-boundary guard as a provider capability,
798
- and do not assume a writer run performs that post-round artifact check.
820
+ `shell: false`, so the adjudication round cannot write or run shell by
821
+ capability. This covers the reviewer's adjudication round only. The draft
822
+ round runs on the coder capability with the coder's full tools (shell,
823
+ file writes, `gh`, the operator's MCP servers) because drafting needs to
824
+ read things like pull requests and Jira issues. For that round, read-only is
825
+ a prompt instruction, not a mechanism.
826
+ - The `neal review` command's artifact-boundary guard: the command snapshots
827
+ protected writer state (`.neal/current.json`, queue pointers, run-local
828
+ `RUN_STATE.json` files) and `git status` before the loop, compares them after
829
+ each round, and fails if anything outside its review artifacts changed. This
830
+ is detection after the fact. It does not prevent a change and does not
831
+ revert one. It is owned by the `neal review` command flow, not by a provider
832
+ capability, and a writer run does not perform it.
833
+
834
+ So the capability invariant guarantees the adjudication round never writes or
835
+ runs shell, the draft round is read-only as best effort, and `neal review`
836
+ detects (but cannot undo) a violation. Do not treat the
837
+ artifact-boundary guard as a provider capability, and do not assume a writer
838
+ run performs that post-round check.
package/docs/storage.md CHANGED
@@ -86,7 +86,7 @@ neal operations and diagnostics.
86
86
  | `.neal/runs/<run-id>/RUN_NARRATIVE.json` | Internal state | Narrative source data that neal may read to update the human narrative. It is not a public trace artifact. |
87
87
  | `.neal/runs/<run-id>/REVIEW.md` | User-facing human artifact | Scope or plan review history and findings. |
88
88
  | `.neal/runs/<run-id>/REVIEW-<commit>.md` | User-facing human artifact | Archived review history for an accepted scope commit. |
89
- | `.neal/runs/<run-id>/REVIEWER_CONTEXT.md` | Support/debug artifact | Bounded reviewer-continuity packet rendered for inspection. |
89
+ | `.neal/runs/<run-id>/REVIEWER_CONTEXT.md` | Support/debug artifact | Bounded reviewer-continuity packet rendered for inspection: completed scopes with their changed files, findings, inherited plan-review debt, and citations. |
90
90
  | `.neal/runs/<run-id>/REVIEWER_CONTEXT.json` | Support/debug artifact | Machine-readable reviewer-continuity packet without the rendered prompt markdown. |
91
91
  | `.neal/runs/<run-id>/RECOVERY.md` | User-facing human artifact | Interactive blocked-recovery transcript/history for a run. |
92
92
  | `.neal/runs/<run-id>/FINAL_COMPLETION_REVIEW.md` | User-facing human artifact | Whole-plan final completion review. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navels/neal",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
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.20.0",
11
+ "packageManager": "pnpm@11.21.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.20.0"
44
+ "pnpm": ">=11.21.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.27",
59
- "@anthropic-ai/claude-agent-sdk": "0.3.227",
60
- "@openai/codex-sdk": "0.147.0",
61
- "ai": "7.0.58",
58
+ "@ai-sdk/openai-compatible": "3.0.30",
59
+ "@anthropic-ai/claude-agent-sdk": "0.3.235",
60
+ "@openai/codex-sdk": "0.148.0",
61
+ "ai": "7.0.65",
62
62
  "dotenv": "^17.4.2",
63
63
  "yaml": "^2.9.0",
64
64
  "zod": "4.4.3"
@@ -67,9 +67,9 @@
67
67
  "@eslint/js": "^10.0.1",
68
68
  "@types/node": "^24.13.3",
69
69
  "eslint": "^10.8.1",
70
- "tsx": "^4.23.11",
70
+ "tsx": "^4.23.12",
71
71
  "typescript": "^6.0.3",
72
72
  "typescript-7": "npm:typescript@^7.0.2",
73
- "typescript-eslint": "^8.66.0"
73
+ "typescript-eslint": "^8.67.0"
74
74
  }
75
75
  }