salmon-loop 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/cli/authorization/provider.js +2 -10
  2. package/dist/cli/commands/config.js +2 -2
  3. package/dist/cli/commands/mode.js +2 -2
  4. package/dist/cli/commands/run/handler.js +3 -1
  5. package/dist/cli/commands/run/loop-params.js +1 -0
  6. package/dist/cli/commands/run/runtime-options.js +3 -1
  7. package/dist/cli/config.js +0 -8
  8. package/dist/cli/locales/en.js +2 -2
  9. package/dist/cli/reporters/standard.js +10 -0
  10. package/dist/core/adapters/fs/file-adapter.js +3 -1
  11. package/dist/core/adapters/git/git-adapter.js +6 -3
  12. package/dist/core/adapters/git/git-runner.js +5 -2
  13. package/dist/core/adapters/git/lock-manager.js +7 -4
  14. package/dist/core/checkpoint-domain/manifest-store.js +21 -13
  15. package/dist/core/checkpoint-domain/service.js +3 -1
  16. package/dist/core/config/limits.js +1 -1
  17. package/dist/core/config/model-pricing.js +61 -0
  18. package/dist/core/context/ast/skeleton-extractor.js +225 -0
  19. package/dist/core/context/ast/source-outline.js +24 -1
  20. package/dist/core/context/budget/dynamic-adjuster.js +20 -5
  21. package/dist/core/context/builder.js +7 -3
  22. package/dist/core/context/cache/store-factory.js +3 -1
  23. package/dist/core/context/dependencies.js +2 -1
  24. package/dist/core/context/effectiveness/persistence.js +50 -0
  25. package/dist/core/context/effectiveness/tracker.js +24 -0
  26. package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
  27. package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
  28. package/dist/core/context/gatherers/ast-gatherer.js +30 -28
  29. package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
  30. package/dist/core/context/gatherers/knowledge-gatherer.js +18 -2
  31. package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
  32. package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
  33. package/dist/core/context/service.js +4 -2
  34. package/dist/core/context/steps/context-gather.js +14 -3
  35. package/dist/core/context/steps/context-targets.js +1 -0
  36. package/dist/core/context/targeting/target-resolver.js +29 -11
  37. package/dist/core/context/token/cache.js +5 -2
  38. package/dist/core/context/truncation/strategies/json.js +5 -2
  39. package/dist/core/context/truncation/type-detector.js +3 -1
  40. package/dist/core/extensions/paths.js +2 -2
  41. package/dist/core/facades/cli-authorization-provider.js +1 -0
  42. package/dist/core/feedback/parsers.js +290 -1
  43. package/dist/core/grizzco/dsl/llm-strategy.js +1 -1
  44. package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
  45. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -3
  46. package/dist/core/grizzco/engine/transaction/attempt-failure.js +44 -20
  47. package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
  48. package/dist/core/grizzco/execution/RejectionManager.js +7 -5
  49. package/dist/core/grizzco/runtime/apply-back-runtime.js +3 -1
  50. package/dist/core/grizzco/services/implementations/default/GitConfigService.js +2 -1
  51. package/dist/core/grizzco/steps/autopilot.js +21 -32
  52. package/dist/core/grizzco/steps/explore.js +5 -2
  53. package/dist/core/grizzco/steps/generateReview.js +3 -1
  54. package/dist/core/grizzco/steps/research.js +3 -1
  55. package/dist/core/grizzco/steps/verify.js +7 -1
  56. package/dist/core/grizzco/validation/AstValidationService.js +3 -1
  57. package/dist/core/history/input-history.js +3 -1
  58. package/dist/core/intent/chat-intent.js +3 -1
  59. package/dist/core/llm/ai-sdk/message-mapper.js +13 -8
  60. package/dist/core/llm/ai-sdk/request-params.js +1 -3
  61. package/dist/core/llm/ai-sdk/retry-classifier.js +12 -4
  62. package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
  63. package/dist/core/llm/errors.js +5 -4
  64. package/dist/core/llm/retry-utils.js +8 -2
  65. package/dist/core/llm/stream-utils.js +5 -3
  66. package/dist/core/llm/sub-agent-factory.js +3 -0
  67. package/dist/core/mcp/bridge/resource-context-provider.js +3 -1
  68. package/dist/core/mcp/catalog/discovery.js +3 -1
  69. package/dist/core/mcp/client/connection-manager.js +4 -2
  70. package/dist/core/mcp/client/transport-factory.js +7 -3
  71. package/dist/core/observability/audit-file.js +2 -1
  72. package/dist/core/observability/audit-trail.js +3 -1
  73. package/dist/core/observability/logger.js +2 -1
  74. package/dist/core/observability/monitor.js +24 -0
  75. package/dist/core/observability/run-outcome-reporter.js +1 -0
  76. package/dist/core/permission-gate/default-gate.js +5 -8
  77. package/dist/core/plan/storage.js +7 -4
  78. package/dist/core/plugin/loader.js +3 -1
  79. package/dist/core/prompts/registry.js +1 -1
  80. package/dist/core/prompts/runtime.js +3 -1
  81. package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
  82. package/dist/core/protocols/a2a/sdk/executor.js +3 -1
  83. package/dist/core/protocols/a2a/sdk/server.js +3 -1
  84. package/dist/core/protocols/acp/acp-command-runner.js +7 -6
  85. package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
  86. package/dist/core/protocols/acp/formal-agent.js +3 -2
  87. package/dist/core/protocols/acp/permission-provider.js +3 -2
  88. package/dist/core/reflection/engine.js +114 -14
  89. package/dist/core/runtime/batch-runner.js +81 -0
  90. package/dist/core/runtime/initialize.js +2 -1
  91. package/dist/core/runtime/loop-finalize.js +3 -0
  92. package/dist/core/runtime/loop-session-runner.js +5 -0
  93. package/dist/core/runtime/loop.js +4 -0
  94. package/dist/core/runtime/paths.js +9 -6
  95. package/dist/core/runtime/spawn-interactive.js +5 -4
  96. package/dist/core/security/redaction.js +3 -2
  97. package/dist/core/session/compression.js +3 -1
  98. package/dist/core/session/manager.js +2 -1
  99. package/dist/core/session/pruning-strategy.js +2 -1
  100. package/dist/core/session/token-tracker.js +11 -4
  101. package/dist/core/skills/permissions.js +2 -2
  102. package/dist/core/strata/checkpoint/manager.js +16 -10
  103. package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
  104. package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
  105. package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
  106. package/dist/core/strata/interaction/file-system-provider.js +2 -1
  107. package/dist/core/strata/layers/file-state-resolver.js +9 -7
  108. package/dist/core/strata/layers/immutable-git-layer.js +3 -1
  109. package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
  110. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
  111. package/dist/core/strata/layers/worktree.js +2 -1
  112. package/dist/core/strata/runtime/environment.js +2 -1
  113. package/dist/core/strata/runtime/synchronizer.js +18 -17
  114. package/dist/core/structured-output/json-extract.js +3 -1
  115. package/dist/core/sub-agent/artifacts/store.js +2 -1
  116. package/dist/core/sub-agent/core/manager.js +24 -1
  117. package/dist/core/sub-agent/registry-defaults.js +2 -2
  118. package/dist/core/sub-agent/summary.js +96 -0
  119. package/dist/core/sub-agent/tools/task-spawn.js +7 -4
  120. package/dist/core/target-runtime/profile.js +3 -1
  121. package/dist/core/tools/audit.js +3 -2
  122. package/dist/core/tools/budget.js +3 -1
  123. package/dist/core/tools/builtin/ast.js +144 -0
  124. package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
  125. package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
  126. package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
  127. package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
  128. package/dist/core/tools/builtin/fs.js +76 -1
  129. package/dist/core/tools/builtin/git.js +242 -0
  130. package/dist/core/tools/builtin/glob.js +79 -0
  131. package/dist/core/tools/builtin/index.js +12 -4
  132. package/dist/core/tools/builtin/knowledge.js +146 -4
  133. package/dist/core/tools/builtin/proposal.js +3 -1
  134. package/dist/core/tools/builtin/verify.js +35 -3
  135. package/dist/core/tools/permissions/permission-rules.js +3 -1
  136. package/dist/core/tools/router.js +88 -5
  137. package/dist/core/tools/session.js +10 -5
  138. package/dist/core/types/batch.js +2 -0
  139. package/dist/core/utils/sanitizer.js +5 -2
  140. package/dist/core/utils/serialize.js +5 -2
  141. package/dist/core/verification/detect-runner.js +86 -0
  142. package/dist/core/verification/runner.js +76 -0
  143. package/dist/core/version.js +3 -1
  144. package/dist/languages/python/index.js +154 -0
  145. package/dist/locales/en.js +6 -0
  146. package/package.json +2 -1
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../observability/logger.js';
1
2
  export function parseTscOutput(output) {
2
3
  const diagnostics = [];
3
4
  const lines = output.split('\n');
@@ -46,8 +47,296 @@ export function parsePythonError(output) {
46
47
  }
47
48
  return diagnostics;
48
49
  }
50
+ export function parsePytestOutput(output) {
51
+ const diagnostics = [];
52
+ const lines = output.split('\n');
53
+ // Pattern 1: FAILED lines (--tb=short -q format)
54
+ // e.g. FAILED tests/test_foo.py::test_bar - AssertionError: expected 5, got 3
55
+ // e.g. FAILED tests/test_foo.py::TestClass::test_method - ValueError: bad value
56
+ const failedRegex = /^FAILED\s+(\S+?)\s+-\s+(.+)$/;
57
+ // Pattern 2: Assertion detail lines (E prefix)
58
+ // e.g. E AssertionError: expected 5, got 3
59
+ const assertionRegex = /^\s*E\s{2,}(\w+(?:Error|Exception)?):\s*(.+)$/;
60
+ // Pattern 3: File:line reference in traceback
61
+ // e.g. tests/test_module.py:42: in test_function
62
+ const fileLineRegex = /^\s*(\S+\.py):(\d+): in /;
63
+ // Pattern 4: One-line format (--tb=line)
64
+ // e.g. tests/test_module.py:42: AssertionError
65
+ const lineFormatRegex = /^(\S+\.py):(\d+):\s+(\w+(?:Error|Exception)?)$/;
66
+ // Track current file context for enhancing FAILED diagnostics
67
+ let currentFile = null;
68
+ let currentLine = null;
69
+ // Map test_id -> { file, line, assertion } for merging
70
+ const assertionMap = new Map();
71
+ for (let i = 0; i < lines.length; i++) {
72
+ const line = lines[i];
73
+ // Collect file:line context from tracebacks
74
+ const fileMatch = line.match(fileLineRegex);
75
+ if (fileMatch) {
76
+ currentFile = fileMatch[1];
77
+ currentLine = parseInt(fileMatch[2]);
78
+ continue;
79
+ }
80
+ // Collect assertion details
81
+ const assertionMatch = line.match(assertionRegex);
82
+ if (assertionMatch && currentFile) {
83
+ // Associate with the most recent file context
84
+ const key = `${currentFile}:${currentLine}`;
85
+ if (!assertionMap.has(key)) {
86
+ assertionMap.set(key, {
87
+ file: currentFile,
88
+ line: currentLine ?? undefined,
89
+ assertion: `${assertionMatch[1]}: ${assertionMatch[2]}`,
90
+ });
91
+ }
92
+ continue;
93
+ }
94
+ // Parse FAILED lines — the primary signal
95
+ const failedMatch = line.match(failedRegex);
96
+ if (failedMatch) {
97
+ const testId = failedMatch[1]; // e.g. tests/test_foo.py::test_bar
98
+ const errorMessage = failedMatch[2];
99
+ // Extract file from test_id (everything before ::)
100
+ const testFile = testId.split('::')[0];
101
+ diagnostics.push({
102
+ file: testFile,
103
+ severity: 'error',
104
+ message: errorMessage,
105
+ source: 'pytest',
106
+ });
107
+ continue;
108
+ }
109
+ // Parse one-line format
110
+ const lineFormatMatch = line.match(lineFormatRegex);
111
+ if (lineFormatMatch) {
112
+ diagnostics.push({
113
+ file: lineFormatMatch[1],
114
+ line: parseInt(lineFormatMatch[2]),
115
+ severity: 'error',
116
+ message: lineFormatMatch[3],
117
+ source: 'pytest',
118
+ });
119
+ }
120
+ }
121
+ // Enhance FAILED diagnostics with file:line info from tracebacks
122
+ if (diagnostics.length > 0 && assertionMap.size > 0) {
123
+ for (const diag of diagnostics) {
124
+ if (diag.source === 'pytest' && !diag.line) {
125
+ // Find matching assertion detail by file
126
+ for (const [, info] of assertionMap) {
127
+ if (info.file === diag.file && info.line) {
128
+ diag.line = info.line;
129
+ if (info.assertion && diag.message.length < info.assertion.length) {
130
+ diag.message = info.assertion;
131
+ }
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ return diagnostics;
139
+ }
140
+ export function parseJestOutput(output) {
141
+ const diagnostics = [];
142
+ const lines = output.split('\n');
143
+ // Pattern 1: FAIL src/foo.test.ts
144
+ const suiteFailRegex = /^(?:FAIL|✗|✘)\s+(\S+)$/;
145
+ // Pattern 2: ● Test suite failed to run / ● test_name
146
+ const bulletRegex = /^\s*●\s+(.+)$/;
147
+ // Pattern 3: inline error: src/foo.ts:10:5 - error TS2322: ...
148
+ const inlineRegex = /^(\S+\.tsx?):(\d+):(\d+)\s+-\s+(error|warning)\s+(.+)$/;
149
+ let currentSuite = null;
150
+ for (const line of lines) {
151
+ const suiteMatch = line.match(suiteFailRegex);
152
+ if (suiteMatch) {
153
+ currentSuite = suiteMatch[1];
154
+ continue;
155
+ }
156
+ const inlineMatch = line.match(inlineRegex);
157
+ if (inlineMatch) {
158
+ diagnostics.push({
159
+ file: inlineMatch[1],
160
+ line: parseInt(inlineMatch[2]),
161
+ column: parseInt(inlineMatch[3]),
162
+ severity: inlineMatch[4],
163
+ message: inlineMatch[5],
164
+ source: 'jest',
165
+ });
166
+ continue;
167
+ }
168
+ const bulletMatch = line.match(bulletRegex);
169
+ if (bulletMatch && currentSuite) {
170
+ diagnostics.push({
171
+ file: currentSuite,
172
+ severity: 'error',
173
+ message: bulletMatch[1],
174
+ source: 'jest',
175
+ });
176
+ }
177
+ }
178
+ return diagnostics;
179
+ }
180
+ // ── Structured JSON parsers ──────────────────────────────────────────────────
181
+ /** Parse jest --json output (single JSON object). */
182
+ export function parseJestJson(output) {
183
+ const diagnostics = [];
184
+ let parsed;
185
+ try {
186
+ parsed = JSON.parse(output);
187
+ }
188
+ catch (error) {
189
+ getLogger().debug(`[Parsers] Failed to parse jest JSON output: ${error instanceof Error ? error.message : String(error)}`);
190
+ return [];
191
+ }
192
+ for (const suite of parsed.testResults ?? []) {
193
+ for (const assertion of suite.assertionResults ?? []) {
194
+ if (assertion.status === 'failed') {
195
+ diagnostics.push({
196
+ file: suite.name,
197
+ severity: 'error',
198
+ message: (assertion.failureMessages ?? []).join('\n') ||
199
+ `Test failed: ${(assertion.ancestorTitles ?? []).join(' > ')} > ${assertion.fullName ?? assertion.title}`,
200
+ source: 'jest',
201
+ });
202
+ }
203
+ }
204
+ }
205
+ return diagnostics;
206
+ }
207
+ /** Extract test summary from jest --json output. */
208
+ export function parseJestJsonSummary(output) {
209
+ try {
210
+ const parsed = JSON.parse(output);
211
+ if (typeof parsed.numTotalTests === 'number') {
212
+ const passed = parsed.numPassedTests ?? 0;
213
+ const failed = parsed.numFailedTests ?? 0;
214
+ const total = parsed.numTotalTests;
215
+ return { total, passed, failed, skipped: total - passed - failed };
216
+ }
217
+ }
218
+ catch (error) {
219
+ /* not JSON */
220
+ getLogger().debug(`[Parsers] Failed to parse jest JSON summary: ${error instanceof Error ? error.message : String(error)}`);
221
+ }
222
+ return undefined;
223
+ }
224
+ /** Parse eslint --format json output (JSON array). */
225
+ export function parseEslintJson(output) {
226
+ const diagnostics = [];
227
+ let parsed;
228
+ try {
229
+ parsed = JSON.parse(output);
230
+ }
231
+ catch (error) {
232
+ getLogger().debug(`[Parsers] Failed to parse eslint JSON output: ${error instanceof Error ? error.message : String(error)}`);
233
+ return [];
234
+ }
235
+ if (!Array.isArray(parsed))
236
+ return [];
237
+ for (const file of parsed) {
238
+ for (const msg of file.messages ?? []) {
239
+ diagnostics.push({
240
+ file: file.filePath,
241
+ line: msg.line,
242
+ column: msg.column,
243
+ severity: msg.severity === 2 ? 'error' : 'warning',
244
+ message: msg.message + (msg.ruleId ? ` (${msg.ruleId})` : ''),
245
+ source: 'eslint',
246
+ });
247
+ }
248
+ }
249
+ return diagnostics;
250
+ }
251
+ /** Parse bun test --json NDJSON output. */
252
+ export function parseBunTestNdjson(output) {
253
+ const diagnostics = [];
254
+ for (const line of output.split('\n')) {
255
+ const trimmed = line.trim();
256
+ if (!trimmed.startsWith('{'))
257
+ continue;
258
+ try {
259
+ const evt = JSON.parse(trimmed);
260
+ if (evt.type === 'test-fail' || evt.data?.status === 'fail') {
261
+ const data = evt.data ?? {};
262
+ diagnostics.push({
263
+ file: data.file ?? data.sourceFile ?? 'unknown',
264
+ severity: 'error',
265
+ message: data.error?.message ?? data.message ?? 'Test failed',
266
+ source: 'bun',
267
+ });
268
+ }
269
+ }
270
+ catch (error) {
271
+ /* skip non-JSON lines */
272
+ getLogger().debug(`[Parsers] Failed to parse bun test NDJSON line: ${error instanceof Error ? error.message : String(error)}`);
273
+ }
274
+ }
275
+ return diagnostics;
276
+ }
277
+ /** Parse go test -json NDJSON output. */
278
+ export function parseGoTestNdjson(output) {
279
+ const diagnostics = [];
280
+ for (const line of output.split('\n')) {
281
+ const trimmed = line.trim();
282
+ if (!trimmed.startsWith('{'))
283
+ continue;
284
+ try {
285
+ const evt = JSON.parse(trimmed);
286
+ if (evt.Action === 'fail' && evt.Test) {
287
+ diagnostics.push({
288
+ file: evt.Package ?? 'unknown',
289
+ severity: 'error',
290
+ message: `Test failed: ${evt.Test}`,
291
+ source: 'go',
292
+ });
293
+ }
294
+ }
295
+ catch (error) {
296
+ /* skip non-JSON lines */
297
+ getLogger().debug(`[Parsers] Failed to parse go test NDJSON line: ${error instanceof Error ? error.message : String(error)}`);
298
+ }
299
+ }
300
+ return diagnostics;
301
+ }
302
+ /** Dispatch to the correct structured parser based on runner type. */
303
+ export function parseRunnerOutput(output, runner) {
304
+ switch (runner) {
305
+ case 'jest':
306
+ case 'vitest':
307
+ return parseJestJson(output);
308
+ case 'eslint':
309
+ return parseEslintJson(output);
310
+ case 'bun':
311
+ return parseBunTestNdjson(output);
312
+ case 'go':
313
+ return parseGoTestNdjson(output);
314
+ case 'pytest':
315
+ case 'tsc':
316
+ case 'unknown':
317
+ default:
318
+ return parseGenericOutput(output);
319
+ }
320
+ }
321
+ /** Extract test summary from structured JSON output when available. */
322
+ export function parseStructuredSummary(output, runner) {
323
+ switch (runner) {
324
+ case 'jest':
325
+ case 'vitest':
326
+ return parseJestJsonSummary(output);
327
+ default:
328
+ return undefined;
329
+ }
330
+ }
331
+ // ── Text-based heuristic dispatcher (fallback) ──────────────────────────────
49
332
  export function parseGenericOutput(output) {
50
- // Fallback or combined parser
333
+ // Try pytest first (SWE-bench's primary test runner)
334
+ const pytest = parsePytestOutput(output);
335
+ if (pytest.length > 0)
336
+ return pytest;
337
+ const jest = parseJestOutput(output);
338
+ if (jest.length > 0)
339
+ return jest;
51
340
  const tsc = parseTscOutput(output);
52
341
  if (tsc.length > 0)
53
342
  return tsc;
@@ -7,7 +7,7 @@ function defaultMaxRoundsForPhase(phase) {
7
7
  if (phase === Phase.EXPLORE)
8
8
  return 8;
9
9
  if (phase === Phase.AUTOPILOT)
10
- return 8;
10
+ return 12;
11
11
  if (phase === Phase.RESEARCH)
12
12
  return 8;
13
13
  if (phase === Phase.PLAN)
@@ -1,4 +1,5 @@
1
1
  import { LIMITS } from '../../../config/limits.js';
2
+ import { getLogger } from '../../../observability/logger.js';
2
3
  export class LoopTelemetry {
3
4
  now;
4
5
  logs = [];
@@ -16,11 +17,13 @@ export class LoopTelemetry {
16
17
  try {
17
18
  return JSON.stringify(output);
18
19
  }
19
- catch {
20
+ catch (error) {
21
+ getLogger().debug(`[LoopTelemetry] JSON.stringify failed, falling back to String(): ${error instanceof Error ? error.message : String(error)}`);
20
22
  try {
21
23
  return String(output);
22
24
  }
23
- catch {
25
+ catch (stringError) {
26
+ getLogger().debug(`[LoopTelemetry] String() conversion also failed: ${stringError instanceof Error ? stringError.message : String(stringError)}`);
24
27
  return '[Unserializable]';
25
28
  }
26
29
  }
@@ -23,6 +23,12 @@ function toRootCauseCode(code) {
23
23
  ? code
24
24
  : undefined;
25
25
  }
26
+ function applyPassthroughFields(result, options) {
27
+ if (options.tags)
28
+ result.tags = options.tags;
29
+ if (options.providerMeta)
30
+ result.providerMeta = options.providerMeta;
31
+ }
26
32
  export function buildLoopResultFromTransaction({ executionReport, flowMode, options, telemetry, auditPath, }) {
27
33
  const profile = resolveExecutionProfile(flowMode);
28
34
  const rootCause = toRootCauseCode(executionReport.lastErrorCode);
@@ -93,7 +99,7 @@ export function buildLoopResultFromTransaction({ executionReport, flowMode, opti
93
99
  const usage = getTokenUsageFromAuditTrail() ?? undefined;
94
100
  const budgetSummary = getBudgetRunSummary() ?? undefined;
95
101
  if (options.dryRun || profile.readOnly) {
96
- return {
102
+ const result = {
97
103
  success: true,
98
104
  reason: text.loop.operationCompleted,
99
105
  reasonCode: options.dryRun ? 'DRY_RUN' : 'SUCCESS',
@@ -115,8 +121,10 @@ export function buildLoopResultFromTransaction({ executionReport, flowMode, opti
115
121
  fsMode: executionReport.flowReport.fsMode ?? flowMode,
116
122
  budgetSummary,
117
123
  };
124
+ applyPassthroughFields(result, options);
125
+ return result;
118
126
  }
119
- return {
127
+ const result = {
120
128
  success: true,
121
129
  reason: text.loop.operationCompleted,
122
130
  reasonCode: 'SUCCESS',
@@ -138,6 +146,8 @@ export function buildLoopResultFromTransaction({ executionReport, flowMode, opti
138
146
  fsMode: executionReport.flowReport.fsMode ?? flowMode,
139
147
  budgetSummary,
140
148
  };
149
+ applyPassthroughFields(result, options);
150
+ return result;
141
151
  }
142
152
  const retryFailureReason = executionReport.history.at(-1)?.error ?? text.loop.loopExecutionFailed;
143
153
  const failureReason = executionReport.retryExhausted
@@ -164,7 +174,7 @@ export function buildLoopResultFromTransaction({ executionReport, flowMode, opti
164
174
  remediationSteps,
165
175
  fallbackMessage: failureReason,
166
176
  });
167
- return {
177
+ const result = {
168
178
  success: false,
169
179
  reason: resultReason,
170
180
  reasonCode,
@@ -195,6 +205,8 @@ export function buildLoopResultFromTransaction({ executionReport, flowMode, opti
195
205
  ? executionReport.terminalInputRequired
196
206
  : undefined,
197
207
  };
208
+ applyPassthroughFields(result, options);
209
+ return result;
198
210
  }
199
211
  export function buildLoopFailureResult({ message, flowMode, telemetry, auditPath, reasonCode, failurePhase, errorCode, }) {
200
212
  const usage = getTokenUsageFromAuditTrail() ?? undefined;
@@ -7,6 +7,8 @@ import { isRecoverableToolInputErrorCode } from '../../../tools/recoverable-tool
7
7
  import { EXECUTION_PHASES } from '../../../types/runtime.js';
8
8
  import { isRecord } from '../../../utils/serialize.js';
9
9
  import { classifyError, isRetryable } from '../../../verification/runner.js';
10
+ // Used by non-autopilot modes to determine retry eligibility.
11
+ // Autopilot overrides via finalize() — this set has no effect there.
10
12
  const RETRYABLE_PHASES = new Set([
11
13
  'CONTEXT',
12
14
  'EXPLORE',
@@ -30,7 +32,7 @@ function inferFailurePhase(flowReport) {
30
32
  }
31
33
  return 'VERIFY';
32
34
  }
33
- function extractErrorCode(error) {
35
+ export function extractErrorCode(error) {
34
36
  if (typeof error === 'object' && error !== null) {
35
37
  const err = error;
36
38
  // Prioritize code (from SalmonError) for better specificity
@@ -57,7 +59,19 @@ function extractInputRequired(error) {
57
59
  return undefined;
58
60
  if (typeof value.prompt !== 'string' || typeof value.type !== 'string')
59
61
  return undefined;
60
- return value;
62
+ const result = { type: value.type, prompt: value.prompt };
63
+ if (value.reason === 'approval' ||
64
+ value.reason === 'clarification' ||
65
+ value.reason === 'reopen') {
66
+ result.reason = value.reason;
67
+ }
68
+ if (Array.isArray(value.questions)) {
69
+ result.questions = value.questions;
70
+ }
71
+ if (typeof value.responseFormat === 'string') {
72
+ result.responseFormat = value.responseFormat;
73
+ }
74
+ return result;
61
75
  }
62
76
  function extractInterrupt(error) {
63
77
  if (!isRecord(error))
@@ -67,11 +81,23 @@ function extractInterrupt(error) {
67
81
  return undefined;
68
82
  if (typeof value.type !== 'string')
69
83
  return undefined;
70
- return value;
84
+ const result = {
85
+ type: value.type,
86
+ };
87
+ if (typeof value.reason === 'string')
88
+ result.reason = value.reason;
89
+ if (typeof value.prompt === 'string')
90
+ result.prompt = value.prompt;
91
+ if (isRecord(value.data))
92
+ result.data = value.data;
93
+ return result;
71
94
  }
72
95
  export function resolveAttemptFailure(params) {
73
96
  const { flowReport, context, flowMode } = params;
74
97
  const profile = resolveExecutionProfile(flowMode);
98
+ // Autopilot is one-shot: never retry at the transaction-runner level.
99
+ const finalize = (failure) => flowMode === 'autopilot' ? { ...failure, retryable: false } : failure;
100
+ const effectiveVerifyPolicy = params.verifyPolicy ?? profile.verifyPolicy;
75
101
  const interrupt = extractInterrupt(flowReport.error);
76
102
  const interruptCode = extractErrorCode(flowReport.error);
77
103
  if (interruptCode === 'INTERRUPT_REQUIRED' && interrupt?.type === 'awaiting_input') {
@@ -93,7 +119,7 @@ export function resolveAttemptFailure(params) {
93
119
  };
94
120
  }
95
121
  const autopilotCompletion = flowMode === 'autopilot' && context && 'completion' in context ? context.completion : undefined;
96
- const verifyOk = profile.verifyPolicy === 'never' ? true : context?.verifyResult?.ok !== false;
122
+ const verifyOk = effectiveVerifyPolicy === 'never' ? true : context?.verifyResult?.ok !== false;
97
123
  const applyBackResult = context && 'applyBackResult' in context ? context.applyBackResult : undefined;
98
124
  const applyBackFailed = profile.failurePolicy === 'rollback' &&
99
125
  applyBackResult?.success === false &&
@@ -123,7 +149,7 @@ export function resolveAttemptFailure(params) {
123
149
  return undefined;
124
150
  }
125
151
  const errorCode = extractErrorCode(flowReport.error) ?? extractErrorCodeFromTraces(flowReport);
126
- if (profile.verifyPolicy !== 'never' && context?.verifyResult?.ok === false) {
152
+ if (effectiveVerifyPolicy !== 'never' && context?.verifyResult?.ok === false) {
127
153
  const verifyOutput = context.verifyResult.output || text.loop.loopExecutionFailed;
128
154
  const errorType = classifyError(verifyOutput);
129
155
  const fallbackReason = sanitizeReason(context.lastError || verifyOutput);
@@ -135,7 +161,7 @@ export function resolveAttemptFailure(params) {
135
161
  environmentMode,
136
162
  fallbackReason,
137
163
  });
138
- return {
164
+ return finalize({
139
165
  reason: guidance.safeHint,
140
166
  reasonCode: 'VERIFY_FAILED',
141
167
  failurePhase: 'VERIFY',
@@ -144,7 +170,7 @@ export function resolveAttemptFailure(params) {
144
170
  diagnosticCode: guidance.diagnosticCode,
145
171
  safeHint: guidance.safeHint,
146
172
  remediationSteps: guidance.remediationSteps,
147
- };
173
+ });
148
174
  }
149
175
  if (flowMode === 'autopilot' && autopilotCompletion) {
150
176
  if (autopilotCompletion.status === 'changed' ||
@@ -152,13 +178,11 @@ export function resolveAttemptFailure(params) {
152
178
  if (flowReport.success && verifyOk)
153
179
  return undefined;
154
180
  }
155
- const reasonCode = autopilotCompletion.status === 'verification_missing'
156
- ? 'VERIFY_COMMAND_MISSING'
157
- : autopilotCompletion.status === 'tool_failure' &&
158
- isRecoverableToolInputErrorCode(autopilotCompletion.errorCode)
159
- ? 'TOOL_CORRECTION_REQUIRED'
160
- : 'LOOP_FAILED';
161
- const failurePhase = autopilotCompletion.status === 'verification_missing' ? 'VERIFY' : 'AUTOPILOT';
181
+ const reasonCode = autopilotCompletion.status === 'tool_failure' &&
182
+ isRecoverableToolInputErrorCode(autopilotCompletion.errorCode)
183
+ ? 'TOOL_CORRECTION_REQUIRED'
184
+ : 'LOOP_FAILED';
185
+ const failurePhase = 'AUTOPILOT';
162
186
  const fallbackReason = autopilotCompletion.reason ||
163
187
  (autopilotCompletion.status === 'no_effect'
164
188
  ? 'Autopilot completed without changing files or producing an answer.'
@@ -170,7 +194,7 @@ export function resolveAttemptFailure(params) {
170
194
  environmentMode,
171
195
  fallbackReason,
172
196
  });
173
- return {
197
+ return finalize({
174
198
  reason: guidance.safeHint,
175
199
  reasonCode,
176
200
  failurePhase,
@@ -179,7 +203,7 @@ export function resolveAttemptFailure(params) {
179
203
  diagnosticCode: guidance.diagnosticCode,
180
204
  safeHint: guidance.safeHint,
181
205
  remediationSteps: guidance.remediationSteps,
182
- };
206
+ });
183
207
  }
184
208
  if (errorCode === 'PREFLIGHT_NOT_GIT') {
185
209
  const fallbackReason = sanitizeReason(flowReport.error);
@@ -272,7 +296,7 @@ export function resolveAttemptFailure(params) {
272
296
  environmentMode,
273
297
  fallbackReason,
274
298
  });
275
- return {
299
+ return finalize({
276
300
  reason: guidance.safeHint,
277
301
  reasonCode: 'TOOL_CORRECTION_REQUIRED',
278
302
  failurePhase,
@@ -281,7 +305,7 @@ export function resolveAttemptFailure(params) {
281
305
  diagnosticCode: guidance.diagnosticCode,
282
306
  safeHint: guidance.safeHint,
283
307
  remediationSteps: guidance.remediationSteps,
284
- };
308
+ });
285
309
  }
286
310
  const guidance = buildFailureGuidance({
287
311
  reasonCode: failurePhase === 'ROLLBACK' ? 'ROLLBACK_FAILED' : 'LOOP_FAILED',
@@ -304,7 +328,7 @@ export function resolveAttemptFailure(params) {
304
328
  remediationSteps: guidance.remediationSteps,
305
329
  };
306
330
  }
307
- return {
331
+ return finalize({
308
332
  reason: guidance.safeHint,
309
333
  reasonCode: 'LOOP_FAILED',
310
334
  failurePhase,
@@ -313,6 +337,6 @@ export function resolveAttemptFailure(params) {
313
337
  diagnosticCode: guidance.diagnosticCode,
314
338
  safeHint: guidance.safeHint,
315
339
  remediationSteps: guidance.remediationSteps,
316
- };
340
+ });
317
341
  }
318
342
  //# sourceMappingURL=attempt-failure.js.map