@navels/neal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/SECURITY.md +91 -0
  4. package/config.yml +104 -0
  5. package/dist/neal/activity-footer.js +177 -0
  6. package/dist/neal/activity-reporting.js +1 -0
  7. package/dist/neal/adjudicator/artifacts.js +58 -0
  8. package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
  9. package/dist/neal/adjudicator/contracts.js +139 -0
  10. package/dist/neal/adjudicator/execute.js +611 -0
  11. package/dist/neal/adjudicator/final-completion.js +104 -0
  12. package/dist/neal/adjudicator/planning.js +145 -0
  13. package/dist/neal/adjudicator/specs.js +453 -0
  14. package/dist/neal/agents/prompts.js +120 -0
  15. package/dist/neal/agents/rounds.js +706 -0
  16. package/dist/neal/agents/schemas.js +832 -0
  17. package/dist/neal/agents/structured-coder.js +82 -0
  18. package/dist/neal/agents/structured-json.js +528 -0
  19. package/dist/neal/agents.js +4 -0
  20. package/dist/neal/atomic-write.js +18 -0
  21. package/dist/neal/blocked-guidance.js +406 -0
  22. package/dist/neal/cli.js +471 -0
  23. package/dist/neal/commands/check.js +401 -0
  24. package/dist/neal/commands/compat.js +807 -0
  25. package/dist/neal/commands/interactive-activity.js +57 -0
  26. package/dist/neal/commands/new-run.js +79 -0
  27. package/dist/neal/commands/plan-and-execute.js +44 -0
  28. package/dist/neal/commands/recovery-guidance.js +217 -0
  29. package/dist/neal/commands/resume-run.js +395 -0
  30. package/dist/neal/commands/review.js +21 -0
  31. package/dist/neal/commands/runtime.js +557 -0
  32. package/dist/neal/commands/setup.js +596 -0
  33. package/dist/neal/commands/squash.js +113 -0
  34. package/dist/neal/commands/status.js +33 -0
  35. package/dist/neal/commands/writer-exit-codes.js +42 -0
  36. package/dist/neal/commit-message.js +17 -0
  37. package/dist/neal/config.js +432 -0
  38. package/dist/neal/context/artifacts.js +140 -0
  39. package/dist/neal/context/context.js +324 -0
  40. package/dist/neal/context/inline-review-context.js +131 -0
  41. package/dist/neal/context/reviewer-context.js +166 -0
  42. package/dist/neal/context/shared.js +117 -0
  43. package/dist/neal/context/types.js +1 -0
  44. package/dist/neal/diagnostic.js +208 -0
  45. package/dist/neal/execute-finalization.js +5 -0
  46. package/dist/neal/final-completion-review.js +188 -0
  47. package/dist/neal/final-completion.js +229 -0
  48. package/dist/neal/git.js +339 -0
  49. package/dist/neal/index.js +135 -0
  50. package/dist/neal/interactive-controls.js +85 -0
  51. package/dist/neal/logger.js +102 -0
  52. package/dist/neal/manual-gates.js +121 -0
  53. package/dist/neal/orchestrator/artifacts.js +70 -0
  54. package/dist/neal/orchestrator/completion.js +531 -0
  55. package/dist/neal/orchestrator/failures.js +31 -0
  56. package/dist/neal/orchestrator/notifications.js +175 -0
  57. package/dist/neal/orchestrator/phases/coder.js +516 -0
  58. package/dist/neal/orchestrator/phases/planning.js +540 -0
  59. package/dist/neal/orchestrator/phases/recovery.js +798 -0
  60. package/dist/neal/orchestrator/phases/review.js +136 -0
  61. package/dist/neal/orchestrator/phases/shared.js +279 -0
  62. package/dist/neal/orchestrator/run-loop.js +113 -0
  63. package/dist/neal/orchestrator/split-plan.js +235 -0
  64. package/dist/neal/orchestrator/transitions.js +309 -0
  65. package/dist/neal/orchestrator.js +215 -0
  66. package/dist/neal/phase-display.js +27 -0
  67. package/dist/neal/plan-doc.js +154 -0
  68. package/dist/neal/plan-queue.js +1092 -0
  69. package/dist/neal/plan-refinement.js +39 -0
  70. package/dist/neal/plan-validation.js +525 -0
  71. package/dist/neal/progress.js +237 -0
  72. package/dist/neal/prompts/assert-builder.js +13 -0
  73. package/dist/neal/prompts/execute.js +290 -0
  74. package/dist/neal/prompts/guidance.js +70 -0
  75. package/dist/neal/prompts/planning.js +313 -0
  76. package/dist/neal/prompts/review-doctrine.js +142 -0
  77. package/dist/neal/prompts/shared.js +101 -0
  78. package/dist/neal/prompts/specialized.js +212 -0
  79. package/dist/neal/prompts/specs.js +572 -0
  80. package/dist/neal/providers/anthropic-claude.js +1599 -0
  81. package/dist/neal/providers/detection.js +139 -0
  82. package/dist/neal/providers/generic-agentic-tools.js +586 -0
  83. package/dist/neal/providers/generic-agentic.js +1238 -0
  84. package/dist/neal/providers/liveness.js +151 -0
  85. package/dist/neal/providers/openai-codex.js +1014 -0
  86. package/dist/neal/providers/openai-compatible.js +654 -0
  87. package/dist/neal/providers/registry.js +389 -0
  88. package/dist/neal/providers/telemetry.js +208 -0
  89. package/dist/neal/providers/types.js +21 -0
  90. package/dist/neal/recovery-artifacts.js +50 -0
  91. package/dist/neal/resume-decision.js +220 -0
  92. package/dist/neal/resume-planner.js +265 -0
  93. package/dist/neal/retrospective.js +391 -0
  94. package/dist/neal/review-debt.js +18 -0
  95. package/dist/neal/review-findings/artifacts.js +173 -0
  96. package/dist/neal/review-findings/prompts.js +172 -0
  97. package/dist/neal/review-findings/provider.js +330 -0
  98. package/dist/neal/review-findings/run.js +373 -0
  99. package/dist/neal/review-findings/types.js +1 -0
  100. package/dist/neal/review-mode.js +67 -0
  101. package/dist/neal/review.js +137 -0
  102. package/dist/neal/run-lock.js +334 -0
  103. package/dist/neal/run-metrics.js +355 -0
  104. package/dist/neal/run-narrative-types.js +1 -0
  105. package/dist/neal/run-narrative.js +1374 -0
  106. package/dist/neal/run-registry.js +218 -0
  107. package/dist/neal/run-status.js +25 -0
  108. package/dist/neal/scopes.js +451 -0
  109. package/dist/neal/sensitive-text.js +8 -0
  110. package/dist/neal/squash-message.js +379 -0
  111. package/dist/neal/squash.js +591 -0
  112. package/dist/neal/state-invariants.js +496 -0
  113. package/dist/neal/state-views.js +344 -0
  114. package/dist/neal/state.js +887 -0
  115. package/dist/neal/status-footer.js +258 -0
  116. package/dist/neal/status.js +1260 -0
  117. package/dist/neal/storage-paths.js +57 -0
  118. package/dist/neal/support.js +58 -0
  119. package/dist/neal/terminal-narrator.js +435 -0
  120. package/dist/neal/types.js +1 -0
  121. package/dist/neal/verification-events.js +81 -0
  122. package/dist/neal/version.js +37 -0
  123. package/dist/neal/worktree-status.js +137 -0
  124. package/dist/notifier.js +44 -0
  125. package/docs/ADJUDICATOR_INVENTORY.md +310 -0
  126. package/docs/PROMPT_SPECS.md +266 -0
  127. package/docs/README.md +22 -0
  128. package/docs/architecture.md +113 -0
  129. package/docs/assets/neal-execution-flow.png +0 -0
  130. package/docs/automation.md +65 -0
  131. package/docs/comparison.md +105 -0
  132. package/docs/compat.md +269 -0
  133. package/docs/compatible-models.md +135 -0
  134. package/docs/demo.md +55 -0
  135. package/docs/maintenance.md +64 -0
  136. package/docs/plan-format.md +213 -0
  137. package/docs/providers.md +751 -0
  138. package/docs/release.md +147 -0
  139. package/docs/state-machine.md +266 -0
  140. package/docs/storage.md +207 -0
  141. package/docs/troubleshooting.md +152 -0
  142. package/examples/compat/add-edit-verify/PLAN.md +29 -0
  143. package/examples/compat/add-edit-verify/broken.diff +8 -0
  144. package/examples/compat/add-edit-verify/good.diff +8 -0
  145. package/examples/compat/add-edit-verify/package.json +5 -0
  146. package/examples/compat/add-edit-verify/src/add.js +2 -0
  147. package/examples/compat/add-edit-verify/test/add.test.js +9 -0
  148. package/examples/compat/is-even-add-test/PLAN.md +30 -0
  149. package/examples/compat/is-even-add-test/broken.diff +11 -0
  150. package/examples/compat/is-even-add-test/good.diff +11 -0
  151. package/examples/compat/is-even-add-test/package.json +5 -0
  152. package/examples/compat/is-even-add-test/src/is-even.js +3 -0
  153. package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
  154. package/examples/compat/manifest.json +60 -0
  155. package/examples/compat/plan-greeting/ISSUE.md +25 -0
  156. package/examples/compat/plan-greeting/package.json +5 -0
  157. package/examples/compat/plan-greeting/src/greet.js +2 -0
  158. package/examples/compat/plan-greeting/test/greet.test.js +8 -0
  159. package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
  160. package/examples/compat/reverse-grep-edit/broken.diff +12 -0
  161. package/examples/compat/reverse-grep-edit/good.diff +12 -0
  162. package/examples/compat/reverse-grep-edit/package.json +5 -0
  163. package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
  164. package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
  165. package/examples/issue-triage-js/PLAN.md +83 -0
  166. package/examples/issue-triage-js/README.md +76 -0
  167. package/examples/issue-triage-js/package.json +9 -0
  168. package/examples/issue-triage-js/src/issue-triage.js +87 -0
  169. package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
  170. package/package.json +70 -0
@@ -0,0 +1,1599 @@
1
+ import { createRequire } from 'node:module';
2
+ import { resolve } from 'node:path';
3
+ import { query, } from '@anthropic-ai/claude-agent-sdk';
4
+ import { buildStructuredJsonPrompt, runStructuredJsonProtocol, runStructuredJsonRepairLoop, validateStructuredJsonPayload, } from '../agents/structured-json.js';
5
+ import { NealProviderError } from './types.js';
6
+ const ANTHROPIC_CLAUDE_PROVIDER_ID = 'anthropic-claude';
7
+ const requireFromProvider = createRequire(import.meta.url);
8
+ const requireFromClaudeSdk = createRequire(requireFromProvider.resolve('@anthropic-ai/claude-agent-sdk'));
9
+ function getGlibcVersionRuntime() {
10
+ const report = process.report;
11
+ const glibcVersionRuntime = report?.getReport?.().header?.glibcVersionRuntime;
12
+ return typeof glibcVersionRuntime === 'string' && glibcVersionRuntime.length > 0 ? glibcVersionRuntime : undefined;
13
+ }
14
+ function isGlibcRuntime(glibcVersionRuntime) {
15
+ return typeof glibcVersionRuntime === 'string' && glibcVersionRuntime.length > 0;
16
+ }
17
+ export function getClaudeCodeExecutablePath(options = {}) {
18
+ const platform = options.platform ?? process.platform;
19
+ const arch = options.arch ?? process.arch;
20
+ const resolve = options.resolve ?? requireFromClaudeSdk.resolve.bind(requireFromClaudeSdk);
21
+ const glibcVersionRuntime = 'glibcVersionRuntime' in options ? options.glibcVersionRuntime : getGlibcVersionRuntime();
22
+ if (platform !== 'linux' || !isGlibcRuntime(glibcVersionRuntime)) {
23
+ return undefined;
24
+ }
25
+ try {
26
+ return resolve(`@anthropic-ai/claude-agent-sdk-linux-${arch}/claude`);
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ // Extended thinking can run for many minutes before the first visible text or
33
+ // tool call. Surfacing it keeps the turn-liveness watchdog from treating a
34
+ // deep-thinking turn as a silent stall, but thinking deltas arrive far too
35
+ // often to persist one event each, so emission is throttled per log state.
36
+ const THINKING_PROGRESS_EMIT_INTERVAL_MS = 30_000;
37
+ async function emitClaudeThinkingProgress(label, state, events, sessionHandle, role = roleForClaudeLabel(label), estimatedTokens) {
38
+ const now = Date.now();
39
+ if (now - state.lastThinkingEmitMs < THINKING_PROGRESS_EMIT_INTERVAL_MS) {
40
+ return;
41
+ }
42
+ state.lastThinkingEmitMs = now;
43
+ await emitProviderEvent(events, {
44
+ type: 'assistant_thinking',
45
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
46
+ role,
47
+ label,
48
+ sessionHandle,
49
+ ...(estimatedTokens !== undefined ? { estimatedTokens } : {}),
50
+ });
51
+ }
52
+ class ClaudeInactivityTimeoutError extends Error {
53
+ constructor(timeoutMs, label) {
54
+ super(`Claude ${label} timed out after ${Math.round(timeoutMs / 1000)}s without progress`);
55
+ this.name = 'ClaudeInactivityTimeoutError';
56
+ }
57
+ }
58
+ // In-round transient (`api_error`) retries back off exponentially: 500 ms
59
+ // base, doubling per retry, capped at 5 s — the same schedule as the
60
+ // generic-agentic adapter. `sleep` is injectable through the run functions so
61
+ // tests observe the delays without wall-clock waiting.
62
+ const API_RETRY_BASE_DELAY_MS = 500;
63
+ const API_RETRY_MAX_DELAY_MS = 5_000;
64
+ function defaultSleep(ms) {
65
+ return new Promise((resolve) => {
66
+ setTimeout(resolve, ms);
67
+ });
68
+ }
69
+ function getApiRetryDelayMs(retryCount) {
70
+ return Math.min(API_RETRY_BASE_DELAY_MS * 2 ** (retryCount - 1), API_RETRY_MAX_DELAY_MS);
71
+ }
72
+ async function emitProviderEvent(events, event) {
73
+ await events?.(event);
74
+ }
75
+ function getMirroredAssistantText(event, role, label) {
76
+ if (event.type !== 'assistant_text' ||
77
+ event.provider !== ANTHROPIC_CLAUDE_PROVIDER_ID ||
78
+ event.role !== role ||
79
+ event.label !== label) {
80
+ return null;
81
+ }
82
+ const text = event.text.trim();
83
+ return text ? text : null;
84
+ }
85
+ function roleForClaudeLabel(label) {
86
+ return label === 'coder' ? 'coder' : 'structured-advisor';
87
+ }
88
+ async function flushClaudeText(label, state, events, sessionHandle, role = roleForClaudeLabel(label)) {
89
+ const trimmed = state.textBuffer.trim();
90
+ if (!trimmed) {
91
+ state.textBuffer = '';
92
+ return null;
93
+ }
94
+ await emitProviderEvent(events, {
95
+ type: 'assistant_text',
96
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
97
+ role,
98
+ label,
99
+ sessionHandle,
100
+ text: trimmed,
101
+ });
102
+ state.textBuffer = '';
103
+ return trimmed;
104
+ }
105
+ async function appendClaudeText(label, state, text, events, sessionHandle, role = roleForClaudeLabel(label)) {
106
+ state.sawTextDelta = true;
107
+ state.textBuffer += text;
108
+ const emittedTexts = [];
109
+ const lines = state.textBuffer.split('\n');
110
+ state.textBuffer = lines.pop() ?? '';
111
+ for (const line of lines) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed) {
114
+ continue;
115
+ }
116
+ await emitProviderEvent(events, {
117
+ type: 'assistant_text',
118
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
119
+ role,
120
+ label,
121
+ sessionHandle,
122
+ text: trimmed,
123
+ });
124
+ emittedTexts.push(trimmed);
125
+ }
126
+ return emittedTexts;
127
+ }
128
+ async function logClaudeMessage(label, message, events, state, sessionHandle, role = roleForClaudeLabel(label)) {
129
+ switch (message.type) {
130
+ case 'assistant': {
131
+ if (state?.sawTextDelta) {
132
+ return [];
133
+ }
134
+ const textBlocks = message.message.content
135
+ .filter((block) => block.type === 'text')
136
+ .map((block) => block.text.trim())
137
+ .filter(Boolean);
138
+ if (textBlocks.length > 0) {
139
+ for (const text of textBlocks) {
140
+ await emitProviderEvent(events, {
141
+ type: 'assistant_text',
142
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
143
+ role,
144
+ label,
145
+ sessionHandle,
146
+ text,
147
+ });
148
+ }
149
+ return textBlocks;
150
+ }
151
+ break;
152
+ }
153
+ case 'stream_event':
154
+ if (message.event.type === 'content_block_delta') {
155
+ if (message.event.delta.type === 'text_delta' && state) {
156
+ return await appendClaudeText(label, state, message.event.delta.text, events, sessionHandle, role);
157
+ }
158
+ if (message.event.delta.type === 'thinking_delta' && state) {
159
+ await emitClaudeThinkingProgress(label, state, events, sessionHandle, role);
160
+ }
161
+ }
162
+ else if (message.event.type === 'content_block_stop' && state) {
163
+ const flushedText = await flushClaudeText(label, state, events, sessionHandle, role);
164
+ return flushedText ? [flushedText] : [];
165
+ }
166
+ break;
167
+ case 'tool_progress':
168
+ await emitProviderEvent(events, {
169
+ type: 'tool_progress',
170
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
171
+ role,
172
+ label,
173
+ sessionHandle,
174
+ toolName: message.tool_name,
175
+ message: `tool ${message.tool_name} running (${message.elapsed_time_seconds}s)`,
176
+ providerData: {
177
+ sdkMessageType: message.type,
178
+ elapsedTimeSeconds: message.elapsed_time_seconds,
179
+ },
180
+ });
181
+ break;
182
+ case 'tool_use_summary':
183
+ await emitProviderEvent(events, {
184
+ type: 'tool_progress',
185
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
186
+ role,
187
+ label,
188
+ sessionHandle,
189
+ toolName: 'tool_use_summary',
190
+ message: message.summary,
191
+ providerData: { sdkMessageType: message.type },
192
+ });
193
+ break;
194
+ case 'system':
195
+ switch (message.subtype) {
196
+ case 'local_command_output':
197
+ await emitProviderEvent(events, {
198
+ type: 'tool_progress',
199
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
200
+ role,
201
+ label,
202
+ sessionHandle,
203
+ toolName: 'local_command_output',
204
+ message: message.content,
205
+ providerData: { sdkMessageType: message.type, subtype: message.subtype },
206
+ });
207
+ break;
208
+ case 'task_started':
209
+ await emitProviderEvent(events, {
210
+ type: 'tool_started',
211
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
212
+ role,
213
+ label,
214
+ sessionHandle,
215
+ toolName: 'task',
216
+ providerData: { sdkMessageType: message.type, subtype: message.subtype, description: message.description },
217
+ });
218
+ await emitProviderEvent(events, {
219
+ type: 'tool_progress',
220
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
221
+ role,
222
+ label,
223
+ sessionHandle,
224
+ toolName: 'task',
225
+ message: `task started: ${message.description}`,
226
+ providerData: { sdkMessageType: message.type, subtype: message.subtype },
227
+ });
228
+ break;
229
+ case 'task_progress':
230
+ await emitProviderEvent(events, {
231
+ type: 'tool_progress',
232
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
233
+ role,
234
+ label,
235
+ sessionHandle,
236
+ toolName: 'task',
237
+ message: message.description,
238
+ providerData: { sdkMessageType: message.type, subtype: message.subtype },
239
+ });
240
+ break;
241
+ case 'task_notification':
242
+ await emitProviderEvent(events, {
243
+ type: 'tool_progress',
244
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
245
+ role,
246
+ label,
247
+ sessionHandle,
248
+ toolName: 'task',
249
+ message: `task ${message.status}: ${message.summary}`,
250
+ providerData: { sdkMessageType: message.type, subtype: message.subtype, status: message.status },
251
+ });
252
+ break;
253
+ case 'thinking_tokens':
254
+ if (state) {
255
+ await emitClaudeThinkingProgress(label, state, events, sessionHandle, role, message.estimated_tokens);
256
+ }
257
+ break;
258
+ case 'status':
259
+ if (message.status) {
260
+ await emitProviderEvent(events, {
261
+ type: 'tool_progress',
262
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
263
+ role,
264
+ label,
265
+ sessionHandle,
266
+ toolName: 'status',
267
+ message: `status: ${message.status}`,
268
+ providerData: { sdkMessageType: message.type, subtype: message.subtype },
269
+ });
270
+ }
271
+ break;
272
+ default:
273
+ break;
274
+ }
275
+ break;
276
+ case 'result':
277
+ await emitProviderEvent(events, {
278
+ type: 'turn_completed',
279
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
280
+ role,
281
+ label,
282
+ sessionHandle,
283
+ usage: message.usage,
284
+ providerData: { sdkMessageType: message.type, subtype: message.subtype },
285
+ });
286
+ if (message.usage !== undefined) {
287
+ await emitProviderEvent(events, {
288
+ type: 'usage_reported',
289
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
290
+ role,
291
+ label,
292
+ sessionHandle,
293
+ usage: message.usage,
294
+ providerData: { sdkMessageType: message.type, subtype: message.subtype },
295
+ });
296
+ }
297
+ break;
298
+ default:
299
+ break;
300
+ }
301
+ return [];
302
+ }
303
+ // The timeout callback (which aborts the underlying query) runs before the
304
+ // synchronous rejection, so the surfaced error is always the retryable
305
+ // inactivity-timeout error — never the SDK's abort error, which would
306
+ // misclassify the stall as a caller abort. Mirrors the Codex adapter.
307
+ async function nextWithTimeout(promise, timeoutMs, label, onTimeout) {
308
+ return await new Promise((resolvePromise, rejectPromise) => {
309
+ const timer = setTimeout(() => {
310
+ onTimeout?.();
311
+ rejectPromise(new ClaudeInactivityTimeoutError(timeoutMs, label));
312
+ }, timeoutMs);
313
+ promise.then((value) => {
314
+ clearTimeout(timer);
315
+ resolvePromise(value);
316
+ }, (error) => {
317
+ clearTimeout(timer);
318
+ rejectPromise(error);
319
+ });
320
+ });
321
+ }
322
+ function inferClaudeErrorKind(error, message, fallback, subtype) {
323
+ const text = `${subtype ?? ''}\n${message}`.toLowerCase();
324
+ if (error instanceof ClaudeInactivityTimeoutError || /\btimed out after\b/i.test(message)) {
325
+ return 'timeout';
326
+ }
327
+ if (text.includes('permission') || text.includes('denied') || text.includes('forbidden') || text.includes('not authorized')) {
328
+ return 'permission_denied';
329
+ }
330
+ if (text.includes('failed to provide valid structured output')) {
331
+ return 'structured_output_invalid';
332
+ }
333
+ if (text.includes('resume') || text.includes('session not found') || text.includes('session unavailable')) {
334
+ return 'session_unavailable';
335
+ }
336
+ if (isTransientClaudeFailure(subtype ?? null, message)) {
337
+ return 'api_error';
338
+ }
339
+ return fallback;
340
+ }
341
+ function isRetryableClaudeError(kind) {
342
+ return kind === 'timeout' || kind === 'session_unavailable' || kind === 'api_error';
343
+ }
344
+ function createClaudeProviderError(args) {
345
+ const kind = args.kind ?? inferClaudeErrorKind(args.cause, args.message, 'provider_failed', args.subtype);
346
+ return new NealProviderError({
347
+ message: args.message,
348
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
349
+ role: args.role,
350
+ sessionHandle: args.sessionHandle,
351
+ kind,
352
+ retryable: args.retryable ?? isRetryableClaudeError(kind),
353
+ cause: args.cause,
354
+ });
355
+ }
356
+ function normalizeClaudeProviderError(error, args) {
357
+ const message = error instanceof Error ? error.message : String(error);
358
+ if (error instanceof NealProviderError) {
359
+ if (error.sessionHandle || !args.sessionHandle) {
360
+ return error;
361
+ }
362
+ return createClaudeProviderError({
363
+ message: error.message,
364
+ role: error.role,
365
+ sessionHandle: args.sessionHandle,
366
+ kind: error.kind,
367
+ retryable: error.retryable,
368
+ cause: error,
369
+ });
370
+ }
371
+ return createClaudeProviderError({
372
+ message,
373
+ role: args.role,
374
+ sessionHandle: args.sessionHandle,
375
+ kind: args.kind,
376
+ cause: error,
377
+ subtype: args.subtype,
378
+ });
379
+ }
380
+ async function emitClaudeProviderError(events, error, label) {
381
+ await emitProviderEvent(events, {
382
+ type: 'provider_error',
383
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
384
+ role: error.role,
385
+ label: label ?? (error.role === 'coder' ? 'coder' : undefined),
386
+ sessionHandle: error.sessionHandle,
387
+ message: error.message,
388
+ errorKind: error.kind,
389
+ });
390
+ }
391
+ function hasStructuredOutput(message) {
392
+ const raw = message;
393
+ return raw.structured_output !== undefined && raw.structured_output !== null;
394
+ }
395
+ function getPreferredStructuredOutput(result) {
396
+ const structuredResult = result.firstStructuredResult ?? result.lastResult;
397
+ const structuredOutput = structuredResult?.structured_output;
398
+ return structuredOutput === null ? undefined : structuredOutput;
399
+ }
400
+ async function collectClaudeResult(stream, cwd, label, inactivityTimeoutMs, events, role = roleForClaudeLabel(label), abortController) {
401
+ let sessionHandle = null;
402
+ let lastResult = null;
403
+ let firstStructuredResult = null;
404
+ const logState = { textBuffer: '', sawTextDelta: false, lastThinkingEmitMs: 0 };
405
+ const assistantTexts = [];
406
+ const iterator = stream[Symbol.asyncIterator]();
407
+ while (true) {
408
+ let next;
409
+ try {
410
+ // Aborting the per-turn controller cancels the SDK query on inactivity
411
+ // timeout, so a stalled turn stops burning its child process and tokens
412
+ // instead of racing a retry; the SDK owns stream teardown on abort, so
413
+ // no manual iterator.return() is needed (mirroring the Codex adapter).
414
+ next = await nextWithTimeout(iterator.next(), inactivityTimeoutMs, label, () => abortController?.abort());
415
+ }
416
+ catch (error) {
417
+ throw normalizeClaudeProviderError(error, {
418
+ role,
419
+ sessionHandle,
420
+ });
421
+ }
422
+ if (next.done) {
423
+ break;
424
+ }
425
+ const message = next.value;
426
+ const previousSessionHandle = sessionHandle;
427
+ sessionHandle = sessionHandle ?? message.session_id ?? null;
428
+ if (!previousSessionHandle && sessionHandle) {
429
+ await emitProviderEvent(events, {
430
+ type: 'session_started',
431
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
432
+ role,
433
+ label,
434
+ sessionHandle,
435
+ providerData: { sdkMessageType: message.type },
436
+ });
437
+ }
438
+ const emittedAssistantTexts = await logClaudeMessage(label, message, events, logState, sessionHandle, role);
439
+ if (emittedAssistantTexts.length > 0) {
440
+ assistantTexts.push(emittedAssistantTexts.join('\n\n'));
441
+ }
442
+ if (message.type === 'result') {
443
+ lastResult = message;
444
+ if (firstStructuredResult === null && hasStructuredOutput(message)) {
445
+ // Claude can emit late result messages for background tasks after the structured round result.
446
+ firstStructuredResult = message;
447
+ }
448
+ }
449
+ }
450
+ const flushedText = await flushClaudeText(label, logState, events, sessionHandle, role);
451
+ if (flushedText) {
452
+ assistantTexts.push(flushedText);
453
+ }
454
+ return {
455
+ sessionHandle,
456
+ lastResult,
457
+ firstStructuredResult,
458
+ assistantText: assistantTexts.join('\n\n').trim(),
459
+ };
460
+ }
461
+ // Creates the SDK-facing AbortController for one Claude turn so both
462
+ // orchestration-level supervision (via the external caller-owned abort
463
+ // signal, when present) and the per-turn inactivity timeout can cancel an
464
+ // in-flight query through `Options.abortController`. Repair turns receive
465
+ // their own unlinked controllers: repairs are short, prompt-only turns that
466
+ // caller aborts intentionally do not cancel, but the inactivity timeout
467
+ // still must be able to tear them down.
468
+ function createClaudeTurnAbortController(signal) {
469
+ const abortController = new AbortController();
470
+ if (!signal) {
471
+ return abortController;
472
+ }
473
+ if (signal.aborted) {
474
+ abortController.abort(signal.reason);
475
+ }
476
+ else {
477
+ signal.addEventListener('abort', () => abortController.abort(signal.reason), { once: true });
478
+ }
479
+ return abortController;
480
+ }
481
+ // Builder-default derivation: when the run loop does not pass a per-turn
482
+ // controller explicitly, query options carry an abortController only for an
483
+ // external caller-owned abort signal.
484
+ function deriveClaudeAbortController(signal) {
485
+ return signal ? createClaudeTurnAbortController(signal) : undefined;
486
+ }
487
+ function buildClaudeQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController = deriveClaudeAbortController(args.signal)) {
488
+ return {
489
+ cwd: args.cwd,
490
+ ...(abortController ? { abortController } : {}),
491
+ ...((args.model ?? defaultModel) ? { model: args.model ?? defaultModel ?? undefined } : {}),
492
+ ...(defaultEffort ? { effort: defaultEffort } : {}),
493
+ tools: ['Read', 'Grep', 'Glob'],
494
+ permissionMode: 'bypassPermissions',
495
+ allowDangerouslySkipPermissions: true,
496
+ ...(args.resumeHandle ? { resume: args.resumeHandle } : {}),
497
+ ...(claudeExecutablePath ? { pathToClaudeCodeExecutable: claudeExecutablePath } : {}),
498
+ stderr: (data) => {
499
+ void args.events?.({
500
+ type: 'tool_progress',
501
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
502
+ role: 'structured-advisor',
503
+ label: args.label,
504
+ sessionHandle: args.resumeHandle ?? null,
505
+ toolName: 'stderr',
506
+ message: data,
507
+ isError: true,
508
+ providerData: { stream: 'stderr' },
509
+ });
510
+ },
511
+ outputFormat: {
512
+ type: 'json_schema',
513
+ schema: args.schema,
514
+ },
515
+ };
516
+ }
517
+ function buildClaudeQueryStream(args, defaultModel, defaultEffort, abortController) {
518
+ return query({
519
+ prompt: args.prompt,
520
+ options: buildClaudeQueryOptions(args, defaultModel, undefined, defaultEffort, abortController),
521
+ });
522
+ }
523
+ function buildClaudeJsonBlockReviewPrompt(args) {
524
+ if (args.structuredJsonProtocol?.protocol === 'neal-json-block-v1') {
525
+ return buildStructuredJsonPrompt(args.prompt, args.structuredJsonProtocol);
526
+ }
527
+ return args.prompt;
528
+ }
529
+ function buildClaudeJsonBlockQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController = deriveClaudeAbortController(args.signal)) {
530
+ return {
531
+ cwd: args.cwd,
532
+ ...(abortController ? { abortController } : {}),
533
+ ...((args.model ?? defaultModel) ? { model: args.model ?? defaultModel ?? undefined } : {}),
534
+ ...(defaultEffort ? { effort: defaultEffort } : {}),
535
+ tools: ['Read', 'Grep', 'Glob'],
536
+ permissionMode: 'bypassPermissions',
537
+ allowDangerouslySkipPermissions: true,
538
+ ...(args.resumeHandle ? { resume: args.resumeHandle } : {}),
539
+ ...(claudeExecutablePath ? { pathToClaudeCodeExecutable: claudeExecutablePath } : {}),
540
+ stderr: (data) => {
541
+ void args.events?.({
542
+ type: 'tool_progress',
543
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
544
+ role: 'structured-advisor',
545
+ label: args.label,
546
+ sessionHandle: args.resumeHandle ?? null,
547
+ toolName: 'stderr',
548
+ message: data,
549
+ isError: true,
550
+ providerData: { stream: 'stderr' },
551
+ });
552
+ },
553
+ };
554
+ }
555
+ function buildClaudeJsonBlockQueryStream(args, defaultModel, defaultEffort, abortController) {
556
+ return query({
557
+ prompt: buildClaudeJsonBlockReviewPrompt(args),
558
+ options: buildClaudeJsonBlockQueryOptions(args, defaultModel, undefined, defaultEffort, abortController),
559
+ });
560
+ }
561
+ function buildClaudeJsonBlockRepairQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController) {
562
+ return {
563
+ cwd: args.cwd,
564
+ ...(abortController ? { abortController } : {}),
565
+ ...((args.model ?? defaultModel) ? { model: args.model ?? defaultModel ?? undefined } : {}),
566
+ ...(defaultEffort ? { effort: defaultEffort } : {}),
567
+ tools: [],
568
+ permissionMode: 'bypassPermissions',
569
+ allowDangerouslySkipPermissions: true,
570
+ ...(claudeExecutablePath ? { pathToClaudeCodeExecutable: claudeExecutablePath } : {}),
571
+ stderr: (data) => {
572
+ void args.events?.({
573
+ type: 'tool_progress',
574
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
575
+ role: 'structured-advisor',
576
+ label: `${args.label}:structured-json-repair`,
577
+ sessionHandle: null,
578
+ toolName: 'stderr',
579
+ message: data,
580
+ isError: true,
581
+ providerData: { stream: 'stderr' },
582
+ });
583
+ },
584
+ };
585
+ }
586
+ function buildClaudeJsonBlockRepairQueryStream(args, repairPrompt, defaultModel, defaultEffort, abortController) {
587
+ return query({
588
+ prompt: repairPrompt,
589
+ options: buildClaudeJsonBlockRepairQueryOptions(args, defaultModel, undefined, defaultEffort, abortController),
590
+ });
591
+ }
592
+ function getClaudeResultErrorMessage(result) {
593
+ const raw = result;
594
+ if (!raw) {
595
+ return null;
596
+ }
597
+ if (typeof raw.result === 'string' && raw.result.trim()) {
598
+ return raw.result.trim();
599
+ }
600
+ if (raw.error && typeof raw.error === 'object') {
601
+ const message = raw.error.message;
602
+ if (typeof message === 'string' && message.trim()) {
603
+ return message.trim();
604
+ }
605
+ }
606
+ const errors = raw.errors;
607
+ if (Array.isArray(errors)) {
608
+ const messages = errors
609
+ .map((error) => (typeof error === 'string' ? error.trim() : ''))
610
+ .filter(Boolean);
611
+ if (messages.length > 0) {
612
+ return messages.join('; ');
613
+ }
614
+ }
615
+ return null;
616
+ }
617
+ // Callers classifying SDK result messages must gate on `is_error` before
618
+ // calling this: a successful result's `result` field is the assistant's final
619
+ // text, which can legitimately contain failure vocabulary (plans, reviews, and
620
+ // docs that discuss error handling all mention strings like "api_error" or
621
+ // "rate limit"), and matching on it turns a document about retries into an
622
+ // endless retry loop.
623
+ function isTransientClaudeFailure(subtype, message) {
624
+ const text = `${subtype ?? ''}\n${message ?? ''}`.toLowerCase();
625
+ return (text.includes('api_error') ||
626
+ text.includes('api error') ||
627
+ text.includes('internal server error') ||
628
+ text.includes('overloaded') ||
629
+ text.includes('rate limit') ||
630
+ text.includes('temporar') ||
631
+ (subtype === 'error_during_execution' &&
632
+ text.includes('[ede_diagnostic]') &&
633
+ text.includes('result_type=assistant') &&
634
+ text.includes('last_content_type=none') &&
635
+ text.includes('stop_reason=end_turn')) ||
636
+ text.includes('try again'));
637
+ }
638
+ function isStructuredOutputRetryExhaustion(subtype, message) {
639
+ const text = `${subtype ?? ''}\n${message ?? ''}`.toLowerCase();
640
+ return (subtype === 'error_max_structured_output_retries' ||
641
+ text.includes('failed to provide valid structured output'));
642
+ }
643
+ function getClaudeStructuredFailureKind(subtype, message) {
644
+ if (isTransientClaudeFailure(subtype, message)) {
645
+ return 'api_error';
646
+ }
647
+ if (isStructuredOutputRetryExhaustion(subtype, message)) {
648
+ return 'structured_output_invalid';
649
+ }
650
+ return subtype === 'success' || subtype === null ? 'structured_output_missing' : 'provider_failed';
651
+ }
652
+ function isClaudeJsonBlockStructuredAdvisorRound(args) {
653
+ return args.structuredJsonProtocol?.protocol === 'neal-json-block-v1';
654
+ }
655
+ function getClaudeAssistantTextForLocalJson(result, mirroredAssistantText = '') {
656
+ return [
657
+ result.assistantText,
658
+ mirroredAssistantText,
659
+ getClaudeResultErrorMessage(result.lastResult) ?? '',
660
+ ].find((value) => value.trim().length > 0)?.trim() ?? '';
661
+ }
662
+ function getStructuredJsonFailureSummaries(failure) {
663
+ return {
664
+ parseErrorSummary: failure.phase === 'extraction' ? failure.errorSummary : undefined,
665
+ validationErrorSummary: failure.phase === 'validation' ? failure.errorSummary : undefined,
666
+ };
667
+ }
668
+ function buildStructuredJsonProviderData(args) {
669
+ const summaries = args.failure
670
+ ? getStructuredJsonFailureSummaries(args.failure)
671
+ : {};
672
+ return {
673
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
674
+ role: 'structured-advisor',
675
+ label: args.roundArgs.label,
676
+ protocol: args.roundArgs.structuredJsonProtocol.protocol,
677
+ schemaLabel: args.roundArgs.structuredJsonProtocol.schemaLabel,
678
+ attemptNumber: args.attemptNumber,
679
+ repairAttemptLimit: args.roundArgs.structuredJsonProtocol.repairAttemptLimit,
680
+ assistantTextLength: args.assistantTextLength,
681
+ extractedJsonLength: args.rawJson?.length ?? 0,
682
+ ...(args.source ? { source: args.source } : {}),
683
+ ...(summaries.parseErrorSummary ? { parseErrorSummary: summaries.parseErrorSummary } : {}),
684
+ ...(summaries.validationErrorSummary ? { validationErrorSummary: summaries.validationErrorSummary } : {}),
685
+ ...(args.originalSessionHandle !== undefined ? { originalSessionHandle: args.originalSessionHandle } : {}),
686
+ ...(args.repairSessionHandle !== undefined ? { repairSessionHandle: args.repairSessionHandle } : {}),
687
+ };
688
+ }
689
+ async function emitStructuredJsonExtractionStarted(args) {
690
+ await emitProviderEvent(args.roundArgs.events, {
691
+ type: 'tool_progress',
692
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
693
+ role: 'structured-advisor',
694
+ label: args.roundArgs.label,
695
+ sessionHandle: args.sessionHandle,
696
+ toolName: 'structured_json_extraction_started',
697
+ message: 'Extracting Claude review JSON control payload.',
698
+ providerData: buildStructuredJsonProviderData({
699
+ roundArgs: args.roundArgs,
700
+ attemptNumber: args.attemptNumber,
701
+ assistantTextLength: args.assistantText.length,
702
+ originalSessionHandle: args.originalSessionHandle,
703
+ repairSessionHandle: args.repairSessionHandle,
704
+ }),
705
+ });
706
+ }
707
+ async function emitStructuredJsonFailure(args) {
708
+ const toolName = args.failure.phase === 'validation'
709
+ ? 'structured_json_validation_failed'
710
+ : 'structured_json_extraction_failed';
711
+ await emitProviderEvent(args.roundArgs.events, {
712
+ type: 'tool_progress',
713
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
714
+ role: 'structured-advisor',
715
+ label: args.roundArgs.label,
716
+ sessionHandle: args.sessionHandle,
717
+ toolName,
718
+ message: args.failure.phase === 'validation'
719
+ ? 'Claude review JSON control payload failed Neal validation.'
720
+ : 'Claude review JSON control payload could not be extracted or parsed.',
721
+ isError: true,
722
+ providerData: buildStructuredJsonProviderData({
723
+ roundArgs: args.roundArgs,
724
+ attemptNumber: args.attemptNumber,
725
+ assistantTextLength: args.assistantTextLength,
726
+ rawJson: args.failure.extraction.rawJson,
727
+ failure: args.failure,
728
+ originalSessionHandle: args.originalSessionHandle,
729
+ repairSessionHandle: args.repairSessionHandle,
730
+ }),
731
+ });
732
+ }
733
+ async function emitStructuredJsonRepairStarted(args) {
734
+ await emitProviderEvent(args.roundArgs.events, {
735
+ type: 'tool_progress',
736
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
737
+ role: 'structured-advisor',
738
+ label: args.roundArgs.label,
739
+ sessionHandle: args.originalSessionHandle,
740
+ toolName: 'structured_json_repair_started',
741
+ message: `Repairing Claude review JSON control payload (${args.attemptNumber}/${args.attemptLimit}).`,
742
+ providerData: buildStructuredJsonProviderData({
743
+ roundArgs: args.roundArgs,
744
+ attemptNumber: args.attemptNumber,
745
+ assistantTextLength: args.assistantTextLength,
746
+ originalSessionHandle: args.originalSessionHandle,
747
+ }),
748
+ });
749
+ }
750
+ async function emitStructuredJsonOutputReceived(args) {
751
+ await emitProviderEvent(args.roundArgs.events, {
752
+ type: 'structured_output_received',
753
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
754
+ role: 'structured-advisor',
755
+ label: args.roundArgs.label,
756
+ sessionHandle: args.sessionHandle,
757
+ providerData: buildStructuredJsonProviderData({
758
+ roundArgs: args.roundArgs,
759
+ attemptNumber: args.attemptNumber,
760
+ assistantTextLength: args.assistantTextLength,
761
+ source: args.validation.source,
762
+ rawJson: args.validation.rawJson,
763
+ originalSessionHandle: args.originalSessionHandle,
764
+ repairSessionHandle: args.repairSessionHandle,
765
+ }),
766
+ });
767
+ }
768
+ async function validateClaudeJsonBlockResponse(args) {
769
+ await emitStructuredJsonExtractionStarted(args);
770
+ const validation = validateStructuredJsonPayload(args.assistantText, args.roundArgs.structuredJsonProtocol.validator);
771
+ if (!validation.ok) {
772
+ await emitStructuredJsonFailure({
773
+ roundArgs: args.roundArgs,
774
+ failure: validation,
775
+ assistantTextLength: args.assistantText.length,
776
+ sessionHandle: args.sessionHandle,
777
+ attemptNumber: args.attemptNumber,
778
+ originalSessionHandle: args.originalSessionHandle,
779
+ repairSessionHandle: args.repairSessionHandle,
780
+ });
781
+ }
782
+ return validation;
783
+ }
784
+ async function repairClaudeJsonBlockResponse(args) {
785
+ let repairResult;
786
+ try {
787
+ repairResult = await runStructuredJsonRepairLoop({
788
+ originalAssistantText: args.assistantText,
789
+ initialFailure: args.initialFailure,
790
+ schemaLabel: args.roundArgs.structuredJsonProtocol.schemaLabel,
791
+ schema: args.roundArgs.schema,
792
+ attemptLimit: args.roundArgs.structuredJsonProtocol.repairAttemptLimit,
793
+ validator: args.roundArgs.structuredJsonProtocol.validator,
794
+ callRepair: async ({ prompt, attemptNumber, attemptLimit }) => {
795
+ await emitStructuredJsonRepairStarted({
796
+ roundArgs: args.roundArgs,
797
+ attemptNumber,
798
+ attemptLimit,
799
+ assistantTextLength: args.assistantText.length,
800
+ originalSessionHandle: args.originalSessionHandle,
801
+ });
802
+ const repairAbortController = new AbortController();
803
+ const stream = args.createRepairStream(args.roundArgs, prompt, args.defaultModel, args.defaultEffort, repairAbortController);
804
+ const result = await collectClaudeResult(stream, args.roundArgs.cwd, `${args.roundArgs.label}:structured-json-repair`, args.roundArgs.inactivityTimeoutMs, args.roundArgs.events, 'structured-advisor', repairAbortController);
805
+ return {
806
+ assistantText: getClaudeAssistantTextForLocalJson(result),
807
+ sessionHandle: result.sessionHandle,
808
+ };
809
+ },
810
+ });
811
+ }
812
+ catch (error) {
813
+ const providerError = normalizeClaudeProviderError(error, {
814
+ role: 'structured-advisor',
815
+ sessionHandle: args.originalSessionHandle,
816
+ kind: 'structured_output_invalid',
817
+ });
818
+ await emitProviderEvent(args.roundArgs.events, {
819
+ type: 'tool_progress',
820
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
821
+ role: 'structured-advisor',
822
+ label: args.roundArgs.label,
823
+ sessionHandle: providerError.sessionHandle,
824
+ toolName: 'structured_json_repair_failed',
825
+ message: 'Claude review JSON control payload repair failed before producing a valid Neal payload.',
826
+ isError: true,
827
+ providerData: {
828
+ ...buildStructuredJsonProviderData({
829
+ roundArgs: args.roundArgs,
830
+ attemptNumber: 0,
831
+ assistantTextLength: args.assistantText.length,
832
+ failure: args.initialFailure,
833
+ originalSessionHandle: args.originalSessionHandle,
834
+ }),
835
+ errorKind: providerError.kind,
836
+ errorMessage: providerError.message,
837
+ },
838
+ });
839
+ await emitClaudeProviderError(args.roundArgs.events, providerError, args.roundArgs.label);
840
+ throw providerError;
841
+ }
842
+ for (const attempt of repairResult.attempts) {
843
+ if (!attempt.result.ok) {
844
+ await emitStructuredJsonFailure({
845
+ roundArgs: args.roundArgs,
846
+ failure: attempt.result,
847
+ assistantTextLength: args.assistantText.length,
848
+ sessionHandle: args.originalSessionHandle ?? attempt.sessionHandle,
849
+ attemptNumber: attempt.attemptNumber,
850
+ originalSessionHandle: args.originalSessionHandle,
851
+ repairSessionHandle: attempt.sessionHandle,
852
+ });
853
+ }
854
+ }
855
+ if (repairResult.ok) {
856
+ const returnedSessionHandle = args.originalSessionHandle ?? repairResult.sessionHandle;
857
+ await emitProviderEvent(args.roundArgs.events, {
858
+ type: 'tool_progress',
859
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
860
+ role: 'structured-advisor',
861
+ label: args.roundArgs.label,
862
+ sessionHandle: returnedSessionHandle,
863
+ toolName: 'structured_json_repair_succeeded',
864
+ message: 'Claude review JSON control payload repair produced a valid Neal payload.',
865
+ providerData: buildStructuredJsonProviderData({
866
+ roundArgs: args.roundArgs,
867
+ attemptNumber: repairResult.attemptNumber,
868
+ assistantTextLength: args.assistantText.length,
869
+ source: repairResult.result.source,
870
+ rawJson: repairResult.result.rawJson,
871
+ originalSessionHandle: args.originalSessionHandle,
872
+ repairSessionHandle: repairResult.sessionHandle,
873
+ }),
874
+ });
875
+ await emitStructuredJsonOutputReceived({
876
+ roundArgs: args.roundArgs,
877
+ validation: repairResult.result,
878
+ assistantTextLength: args.assistantText.length,
879
+ sessionHandle: returnedSessionHandle,
880
+ attemptNumber: repairResult.attemptNumber,
881
+ originalSessionHandle: args.originalSessionHandle,
882
+ repairSessionHandle: repairResult.sessionHandle,
883
+ });
884
+ return {
885
+ sessionHandle: returnedSessionHandle,
886
+ structured: repairResult.result.structured,
887
+ };
888
+ }
889
+ await emitProviderEvent(args.roundArgs.events, {
890
+ type: 'tool_progress',
891
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
892
+ role: 'structured-advisor',
893
+ label: args.roundArgs.label,
894
+ sessionHandle: args.originalSessionHandle,
895
+ toolName: 'structured_json_repair_failed',
896
+ message: 'Claude review JSON control payload repair attempts were exhausted.',
897
+ isError: true,
898
+ providerData: buildStructuredJsonProviderData({
899
+ roundArgs: args.roundArgs,
900
+ attemptNumber: repairResult.attempts.at(-1)?.attemptNumber ?? args.roundArgs.structuredJsonProtocol.repairAttemptLimit,
901
+ assistantTextLength: args.assistantText.length,
902
+ rawJson: repairResult.lastFailure.extraction.rawJson,
903
+ failure: repairResult.lastFailure,
904
+ originalSessionHandle: args.originalSessionHandle,
905
+ repairSessionHandle: repairResult.attempts.at(-1)?.sessionHandle ?? null,
906
+ }),
907
+ });
908
+ const providerError = createClaudeProviderError({
909
+ message: `Claude review JSON control payload remained invalid after ${args.roundArgs.structuredJsonProtocol.repairAttemptLimit} repair attempt(s): ${repairResult.lastFailure.errorSummary}`,
910
+ role: 'structured-advisor',
911
+ sessionHandle: args.originalSessionHandle,
912
+ kind: 'structured_output_invalid',
913
+ retryable: false,
914
+ cause: repairResult.lastFailure,
915
+ });
916
+ await emitClaudeProviderError(args.roundArgs.events, providerError, args.roundArgs.label);
917
+ throw providerError;
918
+ }
919
+ async function validateOrRepairClaudeJsonBlockResponse(args) {
920
+ try {
921
+ return await runStructuredJsonProtocol({
922
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
923
+ role: 'structured-advisor',
924
+ label: args.roundArgs.label,
925
+ protocol: args.roundArgs.structuredJsonProtocol,
926
+ prompt: args.roundArgs.prompt,
927
+ events: args.roundArgs.events,
928
+ initialSessionHandle: args.sessionHandle,
929
+ runInitial: async () => ({
930
+ assistantText: args.assistantText,
931
+ sessionHandle: args.sessionHandle,
932
+ }),
933
+ runRepair: async (prompt) => {
934
+ const repairAbortController = new AbortController();
935
+ const stream = args.createRepairStream(args.roundArgs, prompt, args.defaultModel, args.defaultEffort, repairAbortController);
936
+ const result = await collectClaudeResult(stream, args.roundArgs.cwd, `${args.roundArgs.label}:structured-json-repair`, args.roundArgs.inactivityTimeoutMs, args.roundArgs.events, 'structured-advisor', repairAbortController);
937
+ return {
938
+ assistantText: getClaudeAssistantTextForLocalJson(result),
939
+ sessionHandle: result.sessionHandle,
940
+ };
941
+ },
942
+ createProviderError: (errorArgs) => createClaudeProviderError({
943
+ message: errorArgs.message,
944
+ role: 'structured-advisor',
945
+ sessionHandle: errorArgs.sessionHandle,
946
+ kind: errorArgs.kind,
947
+ retryable: false,
948
+ cause: errorArgs.cause,
949
+ }),
950
+ });
951
+ }
952
+ catch (error) {
953
+ const providerError = normalizeClaudeProviderError(error, {
954
+ role: 'structured-advisor',
955
+ sessionHandle: args.sessionHandle,
956
+ kind: 'structured_output_invalid',
957
+ });
958
+ await emitClaudeProviderError(args.roundArgs.events, providerError, args.roundArgs.label);
959
+ throw providerError;
960
+ }
961
+ }
962
+ async function runClaudeJsonBlockStructuredAdvisorRound(args, defaultModel, createStream = buildClaudeJsonBlockQueryStream, createRepairStream = buildClaudeJsonBlockRepairQueryStream, defaultEffort, sleep = defaultSleep) {
963
+ let sessionHandle = args.resumeHandle ?? null;
964
+ let apiRetryCount = 0;
965
+ const apiRetryLimit = args.apiRetryLimit;
966
+ while (true) {
967
+ const mirroredAssistantTexts = [];
968
+ const primaryEvents = args.events
969
+ ? async (event) => {
970
+ const text = getMirroredAssistantText(event, 'structured-advisor', args.label);
971
+ if (text) {
972
+ mirroredAssistantTexts.push(text);
973
+ }
974
+ await args.events?.(event);
975
+ }
976
+ : undefined;
977
+ let result;
978
+ try {
979
+ const turnAbortController = createClaudeTurnAbortController(args.signal);
980
+ result = await collectClaudeResult(createStream(args, defaultModel, defaultEffort, turnAbortController), args.cwd, args.label, args.inactivityTimeoutMs, primaryEvents, 'structured-advisor', turnAbortController);
981
+ }
982
+ catch (error) {
983
+ const providerError = normalizeClaudeProviderError(error, {
984
+ role: 'structured-advisor',
985
+ sessionHandle,
986
+ });
987
+ if (providerError.kind === 'api_error' && apiRetryCount < apiRetryLimit) {
988
+ apiRetryCount += 1;
989
+ await emitProviderEvent(args.events, {
990
+ type: 'tool_progress',
991
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
992
+ role: 'structured-advisor',
993
+ label: args.label,
994
+ sessionHandle,
995
+ toolName: 'api_retry',
996
+ message: `transient API failure; retrying review (${apiRetryCount}/${apiRetryLimit})`,
997
+ isError: true,
998
+ providerData: {
999
+ retryCount: apiRetryCount,
1000
+ retryLimit: apiRetryLimit,
1001
+ message: providerError.message,
1002
+ },
1003
+ });
1004
+ await sleep(getApiRetryDelayMs(apiRetryCount));
1005
+ continue;
1006
+ }
1007
+ const mirroredAssistantText = mirroredAssistantTexts.join('\n\n').trim();
1008
+ if (mirroredAssistantText &&
1009
+ (providerError.kind === 'structured_output_invalid' || providerError.kind === 'structured_output_missing')) {
1010
+ return await validateOrRepairClaudeJsonBlockResponse({
1011
+ roundArgs: args,
1012
+ assistantText: mirroredAssistantText,
1013
+ defaultModel,
1014
+ defaultEffort,
1015
+ sessionHandle: providerError.sessionHandle ?? sessionHandle,
1016
+ createRepairStream,
1017
+ });
1018
+ }
1019
+ await emitClaudeProviderError(args.events, providerError, args.label);
1020
+ throw providerError;
1021
+ }
1022
+ sessionHandle = result.sessionHandle ?? sessionHandle;
1023
+ const subtype = result.lastResult?.subtype ?? null;
1024
+ const resultErrorMessage = getClaudeResultErrorMessage(result.lastResult);
1025
+ const isErrorResult = result.lastResult?.is_error === true;
1026
+ if (isErrorResult && isTransientClaudeFailure(subtype, resultErrorMessage) && apiRetryCount < apiRetryLimit) {
1027
+ apiRetryCount += 1;
1028
+ await emitProviderEvent(args.events, {
1029
+ type: 'tool_progress',
1030
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1031
+ role: 'structured-advisor',
1032
+ label: args.label,
1033
+ sessionHandle,
1034
+ toolName: 'api_retry',
1035
+ message: `transient Claude error result; retrying review (${apiRetryCount}/${apiRetryLimit})`,
1036
+ isError: true,
1037
+ providerData: {
1038
+ retryCount: apiRetryCount,
1039
+ retryLimit: apiRetryLimit,
1040
+ subtype,
1041
+ message: resultErrorMessage,
1042
+ },
1043
+ });
1044
+ await sleep(getApiRetryDelayMs(apiRetryCount));
1045
+ continue;
1046
+ }
1047
+ const structuredRetryExhausted = isStructuredOutputRetryExhaustion(subtype, resultErrorMessage);
1048
+ if (subtype && subtype !== 'success' && !structuredRetryExhausted) {
1049
+ const providerError = createClaudeProviderError({
1050
+ message: resultErrorMessage
1051
+ ? `Claude ${args.label} did not return a successful result${subtype ? ` (${subtype})` : ''}: ${resultErrorMessage}`
1052
+ : `Claude ${args.label} did not return a successful result${subtype ? ` (${subtype})` : ''}`,
1053
+ role: 'structured-advisor',
1054
+ sessionHandle,
1055
+ kind: getClaudeStructuredFailureKind(subtype, resultErrorMessage),
1056
+ subtype,
1057
+ });
1058
+ await emitClaudeProviderError(args.events, providerError, args.label);
1059
+ throw providerError;
1060
+ }
1061
+ const assistantText = getClaudeAssistantTextForLocalJson(result, mirroredAssistantTexts.join('\n\n').trim());
1062
+ return await validateOrRepairClaudeJsonBlockResponse({
1063
+ roundArgs: args,
1064
+ assistantText,
1065
+ defaultModel,
1066
+ defaultEffort,
1067
+ sessionHandle,
1068
+ createRepairStream,
1069
+ });
1070
+ }
1071
+ }
1072
+ async function runClaudeStructuredAdvisorRound(args, defaultModel, createStream = buildClaudeQueryStream, defaultEffort, sleep = defaultSleep) {
1073
+ if (isClaudeJsonBlockStructuredAdvisorRound(args)) {
1074
+ return await runClaudeJsonBlockStructuredAdvisorRound(args, defaultModel, undefined, undefined, defaultEffort, sleep);
1075
+ }
1076
+ let sessionHandle = args.resumeHandle ?? null;
1077
+ let apiRetryCount = 0;
1078
+ const apiRetryLimit = args.apiRetryLimit;
1079
+ while (true) {
1080
+ let result;
1081
+ const mirroredAssistantTexts = [];
1082
+ const primaryEvents = args.events
1083
+ ? async (event) => {
1084
+ const text = getMirroredAssistantText(event, 'structured-advisor', args.label);
1085
+ if (text) {
1086
+ mirroredAssistantTexts.push(text);
1087
+ }
1088
+ await args.events?.(event);
1089
+ }
1090
+ : undefined;
1091
+ try {
1092
+ const turnAbortController = createClaudeTurnAbortController(args.signal);
1093
+ const stream = createStream(args, defaultModel, defaultEffort, turnAbortController);
1094
+ result = await collectClaudeResult(stream, args.cwd, args.label, args.inactivityTimeoutMs, primaryEvents, 'structured-advisor', turnAbortController);
1095
+ }
1096
+ catch (error) {
1097
+ const providerError = normalizeClaudeProviderError(error, {
1098
+ role: 'structured-advisor',
1099
+ sessionHandle,
1100
+ });
1101
+ const mirroredAssistantText = mirroredAssistantTexts.join('\n\n').trim();
1102
+ if (providerError.kind === 'structured_output_invalid') {
1103
+ await emitProviderEvent(args.events, {
1104
+ type: 'tool_progress',
1105
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1106
+ role: 'structured-advisor',
1107
+ label: args.label,
1108
+ sessionHandle: providerError.sessionHandle ?? sessionHandle,
1109
+ toolName: 'structured_output_stream_error',
1110
+ message: 'Claude SDK structured output stream failed before Neal could process a final structured result.',
1111
+ isError: true,
1112
+ providerData: {
1113
+ errorKind: providerError.kind,
1114
+ errorMessage: providerError.message,
1115
+ collectedAssistantTextLength: mirroredAssistantText.length,
1116
+ mirroredAssistantTextCount: mirroredAssistantTexts.length,
1117
+ },
1118
+ });
1119
+ }
1120
+ if (providerError.kind === 'api_error' && apiRetryCount < apiRetryLimit) {
1121
+ apiRetryCount += 1;
1122
+ await emitProviderEvent(args.events, {
1123
+ type: 'tool_progress',
1124
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1125
+ role: 'structured-advisor',
1126
+ label: args.label,
1127
+ sessionHandle,
1128
+ toolName: 'api_retry',
1129
+ message: `transient API failure; retrying review (${apiRetryCount}/${apiRetryLimit})`,
1130
+ isError: true,
1131
+ providerData: {
1132
+ retryCount: apiRetryCount,
1133
+ retryLimit: apiRetryLimit,
1134
+ message: providerError.message,
1135
+ },
1136
+ });
1137
+ await sleep(getApiRetryDelayMs(apiRetryCount));
1138
+ continue;
1139
+ }
1140
+ await emitClaudeProviderError(args.events, providerError, args.label);
1141
+ throw providerError;
1142
+ }
1143
+ sessionHandle = result.sessionHandle ?? sessionHandle;
1144
+ const lastResult = result.lastResult;
1145
+ const structured = getPreferredStructuredOutput(result);
1146
+ if (structured !== undefined) {
1147
+ await emitProviderEvent(args.events, {
1148
+ type: 'structured_output_received',
1149
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1150
+ role: 'structured-advisor',
1151
+ label: args.label,
1152
+ sessionHandle,
1153
+ });
1154
+ return {
1155
+ sessionHandle,
1156
+ structured,
1157
+ };
1158
+ }
1159
+ const subtype = lastResult?.subtype ?? null;
1160
+ const resultErrorMessage = getClaudeResultErrorMessage(lastResult);
1161
+ const structuredRetryExhausted = isStructuredOutputRetryExhaustion(subtype, resultErrorMessage);
1162
+ if (structuredRetryExhausted) {
1163
+ await emitProviderEvent(args.events, {
1164
+ type: 'tool_progress',
1165
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1166
+ role: 'structured-advisor',
1167
+ label: args.label,
1168
+ sessionHandle,
1169
+ toolName: 'structured_output_retry_exhausted',
1170
+ message: 'Claude SDK structured output failed after retries.',
1171
+ isError: true,
1172
+ providerData: {
1173
+ subtype,
1174
+ resultMessage: resultErrorMessage,
1175
+ collectedAssistantTextLength: result.assistantText.length,
1176
+ mirroredAssistantTextCount: mirroredAssistantTexts.length,
1177
+ },
1178
+ });
1179
+ }
1180
+ if (lastResult?.is_error === true && isTransientClaudeFailure(subtype, resultErrorMessage) && apiRetryCount < apiRetryLimit) {
1181
+ apiRetryCount += 1;
1182
+ await emitProviderEvent(args.events, {
1183
+ type: 'tool_progress',
1184
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1185
+ role: 'structured-advisor',
1186
+ label: args.label,
1187
+ sessionHandle,
1188
+ toolName: 'api_retry',
1189
+ message: `transient Claude error result; retrying review (${apiRetryCount}/${apiRetryLimit})`,
1190
+ isError: true,
1191
+ providerData: {
1192
+ retryCount: apiRetryCount,
1193
+ retryLimit: apiRetryLimit,
1194
+ subtype,
1195
+ message: resultErrorMessage,
1196
+ },
1197
+ });
1198
+ await sleep(getApiRetryDelayMs(apiRetryCount));
1199
+ continue;
1200
+ }
1201
+ const providerError = createClaudeProviderError({
1202
+ message: resultErrorMessage
1203
+ ? `Claude ${args.label} did not return a successful result${subtype ? ` (${subtype})` : ''}: ${resultErrorMessage}`
1204
+ : `Claude ${args.label} did not return a successful result${subtype ? ` (${subtype})` : ''}`,
1205
+ role: 'structured-advisor',
1206
+ sessionHandle,
1207
+ kind: getClaudeStructuredFailureKind(subtype, resultErrorMessage),
1208
+ subtype,
1209
+ });
1210
+ await emitClaudeProviderError(args.events, providerError, args.label);
1211
+ throw providerError;
1212
+ }
1213
+ }
1214
+ class AnthropicClaudeStructuredAdvisorAdapter {
1215
+ options;
1216
+ constructor(options = {}) {
1217
+ this.options = options;
1218
+ }
1219
+ async runStructuredRound(args) {
1220
+ return runClaudeStructuredAdvisorRound(args, this.options.model, undefined, this.options.effort ?? undefined);
1221
+ }
1222
+ }
1223
+ // Tool names that write to the local filesystem. Edit and Write are the only
1224
+ // write-class tools Neal exposes to the coder; the extra names keep the write
1225
+ // jail closed if the exposed toolset ever widens.
1226
+ const CLAUDE_WRITE_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
1227
+ function getClaudeWriteToolPath(toolInput) {
1228
+ if (toolInput === null || typeof toolInput !== 'object') {
1229
+ return null;
1230
+ }
1231
+ const record = toolInput;
1232
+ const filePath = record.file_path ?? record.notebook_path;
1233
+ return typeof filePath === 'string' && filePath.trim() ? filePath : null;
1234
+ }
1235
+ // Mechanical enforcement of `toolPolicy.allowedWritePaths` for coder turns.
1236
+ // The coder runs with `permissionMode: 'bypassPermissions'`, which skips the
1237
+ // SDK's permission checks entirely — the `canUseTool` permission callback is
1238
+ // never consulted — but PreToolUse hooks still run before every tool call and
1239
+ // their denies are honored, so the hook is the SDK's mechanism for a per-path
1240
+ // write jail. Paths are compared after resolving against the turn cwd (the
1241
+ // allowlisted paths arrive absolute from the plan rounds); a write-class call
1242
+ // without a recognizable file path is denied, failing closed. Shell is closed
1243
+ // off separately: `allowRun: false` removes Bash from the tools list, so a
1244
+ // jailed turn cannot route writes around this hook through a shell command.
1245
+ function buildClaudeWritePathGuardHooks(cwd, allowedWritePaths) {
1246
+ const allowedPaths = new Set(allowedWritePaths.map((path) => resolve(cwd, path)));
1247
+ const guard = async (input) => {
1248
+ if (input.hook_event_name !== 'PreToolUse' || !CLAUDE_WRITE_TOOL_NAMES.has(input.tool_name)) {
1249
+ return { continue: true };
1250
+ }
1251
+ const filePath = getClaudeWriteToolPath(input.tool_input);
1252
+ if (filePath !== null && allowedPaths.has(resolve(cwd, filePath))) {
1253
+ return { continue: true };
1254
+ }
1255
+ return {
1256
+ hookSpecificOutput: {
1257
+ hookEventName: 'PreToolUse',
1258
+ permissionDecision: 'deny',
1259
+ permissionDecisionReason: `This turn may only write to: ${[...allowedPaths].join(', ')}. ` +
1260
+ `${input.tool_name} targeting ${filePath ?? 'an unrecognized path'} is not allowed.`,
1261
+ },
1262
+ };
1263
+ };
1264
+ return { PreToolUse: [{ hooks: [guard] }] };
1265
+ }
1266
+ function buildClaudeCoderQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController = deriveClaudeAbortController(args.signal)) {
1267
+ return {
1268
+ cwd: args.cwd,
1269
+ ...(abortController ? { abortController } : {}),
1270
+ ...(defaultModel ? { model: defaultModel } : {}),
1271
+ ...(defaultEffort ? { effort: defaultEffort } : {}),
1272
+ // `toolPolicy.allowRun: false` (plan-authoring turns) removes Bash so the
1273
+ // "no shell" jail is enforced by tool absence, not prompt text.
1274
+ tools: args.toolPolicy?.allowRun === false
1275
+ ? ['Read', 'Grep', 'Glob', 'Edit', 'Write']
1276
+ : ['Read', 'Grep', 'Glob', 'Bash', 'Edit', 'Write'],
1277
+ ...(args.toolPolicy?.allowedWritePaths
1278
+ ? { hooks: buildClaudeWritePathGuardHooks(args.cwd, args.toolPolicy.allowedWritePaths) }
1279
+ : {}),
1280
+ permissionMode: 'bypassPermissions',
1281
+ allowDangerouslySkipPermissions: true,
1282
+ ...(args.resumeHandle ? { resume: args.resumeHandle } : {}),
1283
+ ...(claudeExecutablePath ? { pathToClaudeCodeExecutable: claudeExecutablePath } : {}),
1284
+ ...(args.outputSchema
1285
+ ? {
1286
+ outputFormat: {
1287
+ type: 'json_schema',
1288
+ schema: args.outputSchema,
1289
+ },
1290
+ }
1291
+ : {}),
1292
+ stderr: (data) => {
1293
+ void args.events?.({
1294
+ type: 'tool_progress',
1295
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1296
+ role: 'coder',
1297
+ label: 'coder',
1298
+ sessionHandle: args.resumeHandle ?? null,
1299
+ toolName: 'stderr',
1300
+ message: data,
1301
+ isError: true,
1302
+ providerData: { stream: 'stderr' },
1303
+ });
1304
+ },
1305
+ };
1306
+ }
1307
+ function buildClaudeCoderQueryStream(args, defaultModel, defaultEffort, abortController) {
1308
+ return query({
1309
+ prompt: args.prompt,
1310
+ options: buildClaudeCoderQueryOptions(args, defaultModel, undefined, defaultEffort, abortController),
1311
+ });
1312
+ }
1313
+ function buildClaudeCoderRepairQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController) {
1314
+ return {
1315
+ cwd: args.cwd,
1316
+ ...(abortController ? { abortController } : {}),
1317
+ ...(defaultModel ? { model: defaultModel } : {}),
1318
+ ...(defaultEffort ? { effort: defaultEffort } : {}),
1319
+ tools: [],
1320
+ permissionMode: 'bypassPermissions',
1321
+ allowDangerouslySkipPermissions: true,
1322
+ ...(claudeExecutablePath ? { pathToClaudeCodeExecutable: claudeExecutablePath } : {}),
1323
+ stderr: (data) => {
1324
+ void args.events?.({
1325
+ type: 'tool_progress',
1326
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1327
+ role: 'coder',
1328
+ label: `${args.label}:structured-json-repair`,
1329
+ sessionHandle: null,
1330
+ toolName: 'stderr',
1331
+ message: data,
1332
+ isError: true,
1333
+ providerData: { stream: 'stderr' },
1334
+ });
1335
+ },
1336
+ };
1337
+ }
1338
+ function buildClaudeCoderRepairQueryStream(args, repairPrompt, defaultModel, defaultEffort, abortController) {
1339
+ return query({
1340
+ prompt: repairPrompt,
1341
+ options: buildClaudeCoderRepairQueryOptions(args, defaultModel, undefined, defaultEffort, abortController),
1342
+ });
1343
+ }
1344
+ async function runClaudeStructuredCoderPrompt(args, defaultModel, createStream = buildClaudeCoderQueryStream, createRepairStream = buildClaudeCoderRepairQueryStream, defaultEffort, sleep = defaultSleep) {
1345
+ let sessionHandle = args.resumeHandle ?? null;
1346
+ let apiRetryCount = 0;
1347
+ const apiRetryLimit = args.apiRetryLimit ?? 0;
1348
+ // Mirrors the structured-advisor rounds: a transient error result (e.g. a
1349
+ // stream idle timeout surfaced in the result message) must restart the round
1350
+ // instead of feeding the error text into JSON extraction. Budget-gated so an
1351
+ // exhausted budget falls through to today's behavior.
1352
+ const throwIfRetryableTransientResult = (result, label) => {
1353
+ if (apiRetryCount >= apiRetryLimit) {
1354
+ return;
1355
+ }
1356
+ if (result.lastResult?.is_error !== true) {
1357
+ return;
1358
+ }
1359
+ const subtype = result.lastResult?.subtype ?? null;
1360
+ const resultErrorMessage = getClaudeResultErrorMessage(result.lastResult);
1361
+ if (isTransientClaudeFailure(subtype, resultErrorMessage)) {
1362
+ throw createClaudeProviderError({
1363
+ message: resultErrorMessage
1364
+ ? `Claude ${label} returned a transient error result${subtype ? ` (${subtype})` : ''}: ${resultErrorMessage}`
1365
+ : `Claude ${label} returned a transient error result${subtype ? ` (${subtype})` : ''}`,
1366
+ role: 'coder',
1367
+ sessionHandle,
1368
+ kind: 'api_error',
1369
+ retryable: true,
1370
+ subtype,
1371
+ });
1372
+ }
1373
+ };
1374
+ while (true) {
1375
+ try {
1376
+ return await runStructuredJsonProtocol({
1377
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1378
+ role: 'coder',
1379
+ label: args.label,
1380
+ protocol: args.structuredJsonProtocol,
1381
+ prompt: args.prompt,
1382
+ events: args.events,
1383
+ initialSessionHandle: args.resumeHandle ?? null,
1384
+ runInitial: async (prompt) => {
1385
+ const turnAbortController = createClaudeTurnAbortController(args.signal);
1386
+ const result = await collectClaudeResult(createStream({
1387
+ ...args,
1388
+ prompt,
1389
+ }, defaultModel, defaultEffort, turnAbortController), args.cwd, args.label, args.inactivityTimeoutMs, args.events, 'coder', turnAbortController);
1390
+ throwIfRetryableTransientResult(result, args.label);
1391
+ sessionHandle = result.sessionHandle ?? sessionHandle;
1392
+ if (sessionHandle && sessionHandle !== args.resumeHandle) {
1393
+ await args.onSessionStarted?.(sessionHandle);
1394
+ }
1395
+ return {
1396
+ assistantText: getClaudeAssistantTextForLocalJson(result),
1397
+ sessionHandle,
1398
+ };
1399
+ },
1400
+ runRepair: async (prompt) => {
1401
+ const repairAbortController = new AbortController();
1402
+ const result = await collectClaudeResult(createRepairStream(args, prompt, defaultModel, defaultEffort, repairAbortController), args.cwd, `${args.label}:structured-json-repair`, args.inactivityTimeoutMs, args.events, 'coder', repairAbortController);
1403
+ throwIfRetryableTransientResult(result, `${args.label}:structured-json-repair`);
1404
+ return {
1405
+ assistantText: getClaudeAssistantTextForLocalJson(result),
1406
+ sessionHandle: result.sessionHandle,
1407
+ };
1408
+ },
1409
+ createProviderError: (errorArgs) => createClaudeProviderError({
1410
+ message: errorArgs.message,
1411
+ role: 'coder',
1412
+ sessionHandle: errorArgs.sessionHandle,
1413
+ kind: errorArgs.kind,
1414
+ retryable: false,
1415
+ cause: errorArgs.cause,
1416
+ }),
1417
+ });
1418
+ }
1419
+ catch (error) {
1420
+ const providerError = normalizeClaudeProviderError(error, {
1421
+ role: 'coder',
1422
+ sessionHandle,
1423
+ kind: 'structured_output_invalid',
1424
+ });
1425
+ if (providerError.kind === 'api_error' && providerError.retryable && apiRetryCount < apiRetryLimit) {
1426
+ apiRetryCount += 1;
1427
+ await emitProviderEvent(args.events, {
1428
+ type: 'tool_progress',
1429
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1430
+ role: 'coder',
1431
+ label: args.label,
1432
+ sessionHandle,
1433
+ toolName: 'api_retry',
1434
+ message: `transient Claude failure; retrying ${args.label} (${apiRetryCount}/${apiRetryLimit})`,
1435
+ isError: true,
1436
+ providerData: {
1437
+ retryCount: apiRetryCount,
1438
+ retryLimit: apiRetryLimit,
1439
+ message: providerError.message,
1440
+ },
1441
+ });
1442
+ await sleep(getApiRetryDelayMs(apiRetryCount));
1443
+ // Fresh attempt: never resume the failed attempt's session.
1444
+ sessionHandle = args.resumeHandle ?? null;
1445
+ continue;
1446
+ }
1447
+ await emitClaudeProviderError(args.events, providerError, args.label);
1448
+ throw providerError;
1449
+ }
1450
+ }
1451
+ }
1452
+ class AnthropicClaudeCoderAdapter {
1453
+ options;
1454
+ constructor(options = {}) {
1455
+ this.options = options;
1456
+ }
1457
+ async runPrompt(args) {
1458
+ let sessionHandle = args.resumeHandle ?? null;
1459
+ let result;
1460
+ try {
1461
+ const turnAbortController = createClaudeTurnAbortController(args.signal);
1462
+ const stream = buildClaudeCoderQueryStream(args, this.options.model, this.options.effort ?? undefined, turnAbortController);
1463
+ result = await collectClaudeResult(stream, args.cwd, 'coder', args.inactivityTimeoutMs, args.events, 'coder', turnAbortController);
1464
+ sessionHandle = result.sessionHandle ?? sessionHandle;
1465
+ }
1466
+ catch (error) {
1467
+ const providerError = normalizeClaudeProviderError(error, {
1468
+ role: 'coder',
1469
+ sessionHandle,
1470
+ });
1471
+ await emitClaudeProviderError(args.events, providerError, 'coder');
1472
+ throw providerError;
1473
+ }
1474
+ if (sessionHandle && sessionHandle !== args.resumeHandle) {
1475
+ await args.onSessionStarted?.(sessionHandle);
1476
+ }
1477
+ const statusResult = args.outputSchema && result.firstStructuredResult ? result.firstStructuredResult : result.lastResult;
1478
+ const subtype = statusResult?.subtype ?? null;
1479
+ const resultErrorMessage = getClaudeResultErrorMessage(statusResult);
1480
+ if (subtype && subtype !== 'success') {
1481
+ const providerError = createClaudeProviderError({
1482
+ message: resultErrorMessage
1483
+ ? `Claude coder did not return a successful result (${subtype}): ${resultErrorMessage}`
1484
+ : `Claude coder did not return a successful result (${subtype})`,
1485
+ sessionHandle,
1486
+ subtype,
1487
+ role: 'coder',
1488
+ });
1489
+ await emitClaudeProviderError(args.events, providerError, 'coder');
1490
+ throw providerError;
1491
+ }
1492
+ if (args.outputSchema) {
1493
+ const structuredOutput = getPreferredStructuredOutput(result);
1494
+ if (structuredOutput === undefined) {
1495
+ const providerError = createClaudeProviderError({
1496
+ message: 'Claude coder did not return structured output',
1497
+ role: 'coder',
1498
+ sessionHandle,
1499
+ kind: 'structured_output_missing',
1500
+ subtype,
1501
+ });
1502
+ await emitClaudeProviderError(args.events, providerError, 'coder');
1503
+ throw providerError;
1504
+ }
1505
+ await emitProviderEvent(args.events, {
1506
+ type: 'structured_output_received',
1507
+ provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
1508
+ role: 'coder',
1509
+ label: 'coder',
1510
+ sessionHandle,
1511
+ });
1512
+ return {
1513
+ sessionHandle: sessionHandle,
1514
+ finalResponse: JSON.stringify(structuredOutput),
1515
+ };
1516
+ }
1517
+ const finalResponse = result.assistantText || getClaudeResultErrorMessage(result.lastResult) || '';
1518
+ if (!finalResponse.trim()) {
1519
+ const providerError = createClaudeProviderError({
1520
+ message: 'Claude coder returned no final response',
1521
+ role: 'coder',
1522
+ sessionHandle,
1523
+ kind: 'structured_output_missing',
1524
+ subtype,
1525
+ });
1526
+ await emitClaudeProviderError(args.events, providerError, 'coder');
1527
+ throw providerError;
1528
+ }
1529
+ return {
1530
+ sessionHandle: sessionHandle,
1531
+ finalResponse,
1532
+ };
1533
+ }
1534
+ async runStructuredPrompt(args) {
1535
+ return runClaudeStructuredCoderPrompt(args, this.options.model, undefined, undefined, this.options.effort ?? undefined);
1536
+ }
1537
+ }
1538
+ export function createAnthropicClaudeStructuredAdvisorAdapter(options = {}) {
1539
+ return new AnthropicClaudeStructuredAdvisorAdapter(options);
1540
+ }
1541
+ export function createAnthropicClaudeCoderAdapter(options = {}) {
1542
+ return new AnthropicClaudeCoderAdapter(options);
1543
+ }
1544
+ export const anthropicClaudeProviderDefinition = {
1545
+ id: 'anthropic-claude',
1546
+ displayName: 'Anthropic Claude',
1547
+ capabilities: {
1548
+ coder: {
1549
+ supported: true,
1550
+ toolAccess: {
1551
+ read: true,
1552
+ write: true,
1553
+ shell: true,
1554
+ },
1555
+ supportsSessionResume: true,
1556
+ supportsModelOverride: true,
1557
+ supportsStructuredOutput: true,
1558
+ usageReporting: 'opportunistic',
1559
+ supportedEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
1560
+ },
1561
+ 'structured-advisor': {
1562
+ supported: true,
1563
+ // Reviewers run read-only: read tools only, no write, no shell. The
1564
+ // reviewer option builders expose only ['Read','Grep','Glob'] (repair
1565
+ // exposes none), so a reviewer can inspect the checkout but never edit it
1566
+ // or run commands.
1567
+ toolAccess: {
1568
+ read: true,
1569
+ write: false,
1570
+ shell: false,
1571
+ },
1572
+ supportsSessionResume: true,
1573
+ supportsModelOverride: true,
1574
+ supportsStructuredOutput: true,
1575
+ usageReporting: 'opportunistic',
1576
+ supportedEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
1577
+ },
1578
+ },
1579
+ createCoderAdapter: createAnthropicClaudeCoderAdapter,
1580
+ createStructuredAdvisorAdapter: createAnthropicClaudeStructuredAdvisorAdapter,
1581
+ };
1582
+ export const anthropicClaudeProviderTestHooks = {
1583
+ buildClaudeCoderQueryOptions,
1584
+ buildClaudeCoderRepairQueryOptions,
1585
+ buildClaudeJsonBlockQueryOptions,
1586
+ buildClaudeJsonBlockRepairQueryOptions,
1587
+ buildClaudeJsonBlockReviewPrompt,
1588
+ buildClaudeQueryOptions,
1589
+ collectClaudeResult,
1590
+ getClaudeCodeExecutablePath,
1591
+ getMirroredAssistantText,
1592
+ getPreferredStructuredOutput,
1593
+ isGlibcRuntime,
1594
+ logClaudeMessage,
1595
+ THINKING_PROGRESS_EMIT_INTERVAL_MS,
1596
+ runClaudeJsonBlockStructuredAdvisorRound,
1597
+ runClaudeStructuredCoderPrompt,
1598
+ runClaudeStructuredAdvisorRound,
1599
+ };