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
@@ -3,7 +3,7 @@ import { mapErrorForAudit } from '../../../observability/error-mapping.js';
3
3
  import { ReflectionEngine } from '../../../reflection/engine.js';
4
4
  import { resolveExecutionProfile } from '../../../runtime/execution-profile.js';
5
5
  import { executeFlowAttempt } from '../../flows/flow-dispatch.js';
6
- import { resolveAttemptFailure } from './attempt-failure.js';
6
+ import { extractErrorCode, resolveAttemptFailure } from './attempt-failure.js';
7
7
  import { buildAuthorizationSummary } from './authorization-summary.js';
8
8
  import { mapRetryExhaustedReport, mapSuccessReport, mapTerminalFailureReport, } from './report-mapper.js';
9
9
  import { evaluateRetryPolicy } from './retry-policy.js';
@@ -128,7 +128,6 @@ export class FlowTransactionRunner {
128
128
  lastRecentReadArtifacts;
129
129
  lastToolResultPreviewArtifacts;
130
130
  lastReplacementState;
131
- pendingAutopilotVerification;
132
131
  constructor(params) {
133
132
  this.params = params;
134
133
  this.lastVerifyArtifact = params.options.artifactHints?.verifyArtifact;
@@ -183,7 +182,6 @@ export class FlowTransactionRunner {
183
182
  : undefined,
184
183
  },
185
184
  replacementState: this.lastReplacementState,
186
- pendingVerification: this.pendingAutopilotVerification,
187
185
  lastError: this.currentLastError,
188
186
  applyBackRuntime: {
189
187
  activeRepoPath: this.params.env.activeRepoPath,
@@ -212,20 +210,9 @@ export class FlowTransactionRunner {
212
210
  flowReport: result,
213
211
  context: shrinkCtx,
214
212
  flowMode: this.params.flowMode,
213
+ verifyPolicy: this.params.options.verifyPolicy,
215
214
  });
216
215
  lastAttemptFailure = attemptFailure;
217
- if (this.params.flowMode === 'autopilot') {
218
- if (attemptFailure?.reasonCode === 'VERIFY_FAILED') {
219
- this.pendingAutopilotVerification = {
220
- changedFiles: terminalCtx && 'changedFiles' in terminalCtx
221
- ? (terminalCtx.changedFiles ?? undefined)
222
- : undefined,
223
- };
224
- }
225
- else if (!attemptFailure) {
226
- this.pendingAutopilotVerification = undefined;
227
- }
228
- }
229
216
  const entry = {
230
217
  attempt,
231
218
  plan: shrinkCtx?.plan ?? null,
@@ -245,27 +232,24 @@ export class FlowTransactionRunner {
245
232
  ? 'SHRINK'
246
233
  : 'APPLY_BACK';
247
234
  recordAuditEvent('loop.attempt.success', { attempt, flowMode: this.params.flowMode }, { phase: successPhase, severity: 'low', scope: 'session' });
248
- // Reflection Mechanism: trigger when multiple attempts were needed
249
- if (attempt > 1 && this.params.options.llm) {
250
- const reflectionEngine = new ReflectionEngine(this.params.options.llm);
251
- const reflectionInput = {
235
+ this.emitReflection({
236
+ success: true,
237
+ attempt,
238
+ input: {
252
239
  instruction: this.params.options.instruction,
253
240
  history: this.historyEntries,
254
241
  success: true,
255
242
  metadata: shrinkCtx?.context?.projectMetadata,
256
243
  finalPlan: shrinkCtx?.plan,
257
244
  finalPatch: shrinkCtx?.diff,
258
- };
259
- reflectionEngine
260
- .reflect(reflectionInput, this.params.options.repoPath)
261
- .catch((e) => recordAuditEvent('reflection.error', { error: String(e) }, { severity: 'medium' }));
262
- }
245
+ },
246
+ });
263
247
  return mapSuccessReport({
264
248
  attempt,
265
249
  flowReport: result,
266
250
  history: this.historyEntries,
267
251
  authorizationSummary: this.authorizationSummary,
268
- lastErrorCode: this.extractErrorCode(result.error),
252
+ lastErrorCode: extractErrorCode(result.error),
269
253
  lastContext: terminalCtx,
270
254
  lastVerifyArtifact: this.lastVerifyArtifact,
271
255
  lastSubAgentPatchArtifacts: this.lastSubAgentPatchArtifacts,
@@ -317,12 +301,20 @@ export class FlowTransactionRunner {
317
301
  if (retryDecision.retryExhausted) {
318
302
  break;
319
303
  }
304
+ this.emitReflection({
305
+ success: false,
306
+ input: {
307
+ instruction: this.params.options.instruction,
308
+ history: this.historyEntries,
309
+ success: false,
310
+ },
311
+ });
320
312
  return mapTerminalFailureReport({
321
313
  attempt,
322
314
  flowReport: result,
323
315
  history: this.historyEntries,
324
316
  authorizationSummary: this.authorizationSummary,
325
- lastErrorCode: attemptFailure.errorCode ?? this.extractErrorCode(result.error),
317
+ lastErrorCode: attemptFailure.errorCode ?? extractErrorCode(result.error),
326
318
  lastContext: terminalCtx,
327
319
  lastVerifyArtifact: this.lastVerifyArtifact,
328
320
  lastSubAgentPatchArtifacts: this.lastSubAgentPatchArtifacts,
@@ -333,7 +325,7 @@ export class FlowTransactionRunner {
333
325
  });
334
326
  }
335
327
  this.currentContext = shrinkCtx?.context;
336
- this.currentLastError = attemptFailure.reason;
328
+ this.currentLastError = shrinkCtx?.lastError ?? attemptFailure.reason;
337
329
  this.params.emit({
338
330
  type: 'retry',
339
331
  fromAttempt: attempt,
@@ -346,13 +338,21 @@ export class FlowTransactionRunner {
346
338
  if (!lastReport) {
347
339
  throw new Error('SalmonLoop execution terminated without a FlowReport');
348
340
  }
341
+ this.emitReflection({
342
+ success: false,
343
+ input: {
344
+ instruction: this.params.options.instruction,
345
+ history: this.historyEntries,
346
+ success: false,
347
+ },
348
+ });
349
349
  return mapRetryExhaustedReport({
350
350
  attempts: retries,
351
351
  flowReport: lastReport,
352
352
  history: this.historyEntries,
353
353
  authorizationSummary: this.authorizationSummary,
354
354
  failure: lastAttemptFailure,
355
- lastErrorCode: this.extractErrorCode(lastReport.error),
355
+ lastErrorCode: extractErrorCode(lastReport.error),
356
356
  lastContext: this.lastContext,
357
357
  lastVerifyArtifact: this.lastVerifyArtifact,
358
358
  lastSubAgentPatchArtifacts: this.lastSubAgentPatchArtifacts,
@@ -361,13 +361,19 @@ export class FlowTransactionRunner {
361
361
  lastToolResultPreviewArtifacts: this.lastToolResultPreviewArtifacts,
362
362
  });
363
363
  }
364
- extractErrorCode(error) {
365
- if (typeof error === 'object' && error !== null) {
366
- return (error.llmCode ??
367
- error.code ??
368
- error.name);
364
+ emitReflection(params) {
365
+ if (!this.params.options.llm)
366
+ return;
367
+ const engine = new ReflectionEngine(this.params.options.llm);
368
+ const { repoPath } = this.params.options;
369
+ const fallback = (e) => recordAuditEvent('reflection.error', { error: String(e) }, { severity: 'medium' });
370
+ if (params.success) {
371
+ const method = (params.attempt ?? 1) > 1 ? 'reflect' : 'reflectOnSuccess';
372
+ engine[method](params.input, repoPath).catch(fallback);
373
+ }
374
+ else {
375
+ engine.reflectOnFailure(params.input, repoPath).catch(fallback);
369
376
  }
370
- return undefined;
371
377
  }
372
378
  }
373
379
  //# sourceMappingURL=transaction-runner.js.map
@@ -1,5 +1,6 @@
1
1
  import * as path from 'path';
2
2
  import * as fs from '../../adapters/fs/node-fs.js';
3
+ import { getLogger } from '../../observability/logger.js';
3
4
  /**
4
5
  * RejectionManager
5
6
  * Handles creation and management of .rej files for failed merges.
@@ -46,13 +47,14 @@ export class RejectionManager {
46
47
  ...header,
47
48
  });
48
49
  }
49
- catch {
50
- // Ignore malformed files
50
+ catch (error) {
51
+ getLogger().debug(`[RejectionManager] Malformed rejection file ${file}: ${error instanceof Error ? error.message : String(error)}`);
51
52
  }
52
53
  }
53
54
  return rejections;
54
55
  }
55
- catch {
56
+ catch (error) {
57
+ getLogger().debug(`[RejectionManager] Failed to read rejection directory: ${error instanceof Error ? error.message : String(error)}`);
56
58
  return [];
57
59
  }
58
60
  }
@@ -63,8 +65,8 @@ export class RejectionManager {
63
65
  try {
64
66
  await fs.rm(this.rejectDir, { recursive: true, force: true });
65
67
  }
66
- catch {
67
- // Ignore
68
+ catch (error) {
69
+ getLogger().warn(`[RejectionManager] Failed to clear rejection directory: ${error instanceof Error ? error.message : String(error)}`);
68
70
  }
69
71
  }
70
72
  }
@@ -2,6 +2,7 @@ import { text } from '../../../locales/index.js';
2
2
  import { recordAuditEvent } from '../../observability/audit-trail.js';
3
3
  import { writeDebugArtifact } from '../../observability/debug-artifacts.js';
4
4
  import { buildErrorEnvelope, toSafeErrorSummary } from '../../observability/error-envelope.js';
5
+ import { getLogger } from '../../observability/logger.js';
5
6
  import { isRecord } from '../../utils/serialize.js';
6
7
  import { collectSidecarPaths } from './apply-back-utils.js';
7
8
  export async function runApplyBackPhase(params) {
@@ -95,8 +96,9 @@ export async function runApplyBackPhase(params) {
95
96
  ].join('\n'),
96
97
  });
97
98
  }
98
- catch {
99
+ catch (artifactError) {
99
100
  // Best-effort: do not mask the primary failure if artifact writing fails.
101
+ getLogger().debug(`[ApplyBackRuntime] Failed to write debug artifact: ${artifactError instanceof Error ? artifactError.message : String(artifactError)}`);
100
102
  }
101
103
  const envelope = buildErrorEnvelope({
102
104
  domain: 'applyBack',
@@ -10,7 +10,8 @@ export class GitConfigService {
10
10
  try {
11
11
  return await git.exec(['config', '--get', key], { allowError: true });
12
12
  }
13
- catch {
13
+ catch (error) {
14
+ getLogger().debug(`[GitConfigService] git config --get failed for key "${key}": ${error instanceof Error ? error.message : String(error)}`);
14
15
  return null;
15
16
  }
16
17
  };
@@ -6,6 +6,7 @@ import { GitAdapter } from '../../adapters/git/git-adapter.js';
6
6
  import { LIMITS } from '../../config/limits.js';
7
7
  import { supportsLlmStreaming } from '../../llm/capabilities.js';
8
8
  import { emitLlmOutput } from '../../llm/output-policy.js';
9
+ import { getLogger } from '../../observability/logger.js';
9
10
  import { getAutopilotSystemPrompt } from '../../prompts/runtime.js';
10
11
  import { SessionReplacementPreviewProvider } from '../../session/replacement-preview-provider.js';
11
12
  import { chatWithTools, chatWithToolsStreaming } from '../../tools/session.js';
@@ -286,13 +287,11 @@ function isRuntimeGeneratedPath(path) {
286
287
  return false;
287
288
  }
288
289
  function lastFailedToolAuditEntry(entries) {
289
- if (!Array.isArray(entries))
290
+ if (!Array.isArray(entries) || entries.length === 0)
290
291
  return undefined;
291
- for (let index = entries.length - 1; index >= 0; index -= 1) {
292
- const entry = entries[index];
293
- if (entry?.toolResultStatus && entry.toolResultStatus !== 'ok') {
294
- return entry;
295
- }
292
+ const last = entries[entries.length - 1];
293
+ if (last?.toolResultStatus && last.toolResultStatus !== 'ok') {
294
+ return last;
296
295
  }
297
296
  return undefined;
298
297
  }
@@ -370,12 +369,16 @@ export async function runAutopilot(ctx) {
370
369
  try {
371
370
  workspaceFingerprintBefore = await captureWorkspaceFingerprintForContext(ctx);
372
371
  }
373
- catch {
372
+ catch (error) {
373
+ getLogger().debug(`[Autopilot] Workspace fingerprint capture failed (before): ${error instanceof Error ? error.message : String(error)}`);
374
374
  samplingFailedClosed = true;
375
375
  }
376
376
  }
377
- const assistant = supportsTools
378
- ? await (supportsStreaming ? chatWithToolsStreaming : chatWithTools)(shared.baseMessages, {
377
+ let assistant = null;
378
+ let content = '';
379
+ const messages = shared.baseMessages;
380
+ assistant = supportsTools
381
+ ? await (supportsStreaming ? chatWithToolsStreaming : chatWithTools)(messages, {
379
382
  phase: AUTOPILOT_TOOL_PHASE,
380
383
  providerHints: shared.envelope.providerHints,
381
384
  temperature: 0.2,
@@ -400,7 +403,7 @@ export async function runAutopilot(ctx) {
400
403
  },
401
404
  emit: (event) => ctx.emit({ ...event, timestamp: event.timestamp ?? new Date() }),
402
405
  })
403
- : await llmClient.chat(shared.baseMessages, {
406
+ : await llmClient.chat(messages, {
404
407
  phase: 'AUTOPILOT',
405
408
  providerHints: shared.envelope.providerHints,
406
409
  temperature: 0.2,
@@ -408,7 +411,7 @@ export async function runAutopilot(ctx) {
408
411
  tools: [],
409
412
  toolChoice: 'none',
410
413
  });
411
- const content = String(assistant?.content ?? '').trim();
414
+ content = String(assistant?.content ?? '').trim();
412
415
  if (!supportsTools) {
413
416
  emitLlmOutput({
414
417
  emit: ctx.emit,
@@ -435,28 +438,18 @@ export async function runAutopilot(ctx) {
435
438
  workspaceFingerprintBefore.head !== workspaceFingerprintAfter.head ||
436
439
  workspaceFingerprintBefore.index !== workspaceFingerprintAfter.index;
437
440
  }
438
- catch {
441
+ catch (error) {
442
+ getLogger().debug(`[Autopilot] Workspace fingerprint capture failed (after): ${error instanceof Error ? error.message : String(error)}`);
439
443
  mutated = true;
440
444
  }
441
445
  }
442
446
  }
443
- const pendingChangedFiles = ctx.pendingVerification?.changedFiles && ctx.pendingVerification.changedFiles.length > 0
444
- ? ctx.pendingVerification.changedFiles
445
- : undefined;
446
- const effectiveChangedFiles = changedFiles && changedFiles.length > 0
447
- ? changedFiles
448
- : pendingChangedFiles && pendingChangedFiles.length > 0
449
- ? pendingChangedFiles
450
- : undefined;
451
- const effectiveMutated = mutated || Boolean(ctx.pendingVerification);
447
+ const effectiveChangedFiles = changedFiles && changedFiles.length > 0 ? changedFiles : undefined;
452
448
  return {
453
449
  ...ctx,
454
- mutated: effectiveMutated,
450
+ mutated,
455
451
  changedFiles: effectiveChangedFiles,
456
- pendingVerification: effectiveMutated
457
- ? { changedFiles: effectiveChangedFiles }
458
- : ctx.pendingVerification,
459
- completion: resolveAutopilotCompletion({ content, mutated: effectiveMutated, localAudit }),
452
+ completion: resolveAutopilotCompletion({ content, mutated, localAudit }),
460
453
  toolCallingAudit: mergedAudit,
461
454
  report: {
462
455
  kind: 'answer',
@@ -473,13 +466,10 @@ export async function runAutopilotVerifyGate(ctx) {
473
466
  };
474
467
  }
475
468
  if (!ctx.options.verify) {
469
+ // Autopilot is LLM-driven: the agent verifies on its own via shell.exec.
470
+ // Skip the gate instead of failing — the agent's tool calls are the verification.
476
471
  return {
477
472
  ...ctx,
478
- completion: {
479
- status: 'verification_missing',
480
- reason: 'Autopilot changed the workspace but no verification command was configured.',
481
- errorCode: 'VERIFY_COMMAND_MISSING',
482
- },
483
473
  verifyResult: undefined,
484
474
  };
485
475
  }
@@ -491,7 +481,6 @@ export async function runAutopilotVerifyGate(ctx) {
491
481
  const nextCtx = {
492
482
  ...ctx,
493
483
  verifyResult,
494
- pendingVerification: verifyResult.ok ? undefined : ctx.pendingVerification,
495
484
  };
496
485
  return verifyArtifact ? { ...nextCtx, verifyArtifact } : nextCtx;
497
486
  }
@@ -2,6 +2,7 @@ import path from 'path';
2
2
  import { text } from '../../../locales/index.js';
3
3
  import { supportsLlmStreaming } from '../../llm/capabilities.js';
4
4
  import { recordAuditEvent } from '../../observability/audit-trail.js';
5
+ import { getLogger } from '../../observability/logger.js';
5
6
  import { getExplorePrompt, getExploreSystemPrompt } from '../../prompts/runtime.js';
6
7
  import { SessionReplacementPreviewProvider } from '../../session/replacement-preview-provider.js';
7
8
  import { chatWithTools, chatWithToolsStreaming } from '../../tools/session.js';
@@ -119,8 +120,9 @@ export const exploreCodebase = async (ctx) => {
119
120
  capturedFiles.set(filePath, content);
120
121
  }
121
122
  }
122
- catch {
123
+ catch (error) {
123
124
  // Ignore parsing errors, just don't capture
125
+ getLogger().debug(`[Explore] Failed to parse tool arguments for file capture: ${error instanceof Error ? error.message : String(error)}`);
124
126
  }
125
127
  }
126
128
  }
@@ -196,8 +198,9 @@ export const exploreCodebase = async (ctx) => {
196
198
  capturedFiles.set(rel, content);
197
199
  }
198
200
  }
199
- catch {
201
+ catch (error) {
200
202
  // Best-effort; failure here should not abort exploration.
203
+ getLogger().debug(`[Explore] Failed to read inferred file "${rel}": ${error instanceof Error ? error.message : String(error)}`);
201
204
  }
202
205
  }
203
206
  }
@@ -1,5 +1,6 @@
1
1
  import { text } from '../../../locales/index.js';
2
2
  import { emitLlmOutput } from '../../llm/output-policy.js';
3
+ import { getLogger } from '../../observability/logger.js';
3
4
  import { getReviewPrompt } from '../../prompts/runtime.js';
4
5
  function normalizeReviewSuggestions(value) {
5
6
  if (value == null)
@@ -23,7 +24,8 @@ function parseReviewResponse(content) {
23
24
  try {
24
25
  return normalizeReviewSuggestions(JSON.parse(content));
25
26
  }
26
- catch {
27
+ catch (error) {
28
+ getLogger().debug(`[GenerateReview] Failed to parse review response as JSON: ${error instanceof Error ? error.message : String(error)}`);
27
29
  return normalizeReviewSuggestions(content);
28
30
  }
29
31
  }
@@ -2,6 +2,7 @@ import { text } from '../../../locales/index.js';
2
2
  import { supportsLlmStreaming } from '../../llm/capabilities.js';
3
3
  import { emitLlmOutput } from '../../llm/output-policy.js';
4
4
  import { recordAuditEvent } from '../../observability/audit-trail.js';
5
+ import { getLogger } from '../../observability/logger.js';
5
6
  import { getResearchPrompt, getResearchSystemPrompt } from '../../prompts/runtime.js';
6
7
  import { SessionReplacementPreviewProvider } from '../../session/replacement-preview-provider.js';
7
8
  import { chatWithTools, chatWithToolsStreaming } from '../../tools/session.js';
@@ -50,7 +51,8 @@ function parseResearchResponse(content, fallbackSources) {
50
51
  try {
51
52
  parsed = JSON.parse(content);
52
53
  }
53
- catch {
54
+ catch (error) {
55
+ getLogger().debug(`[Research] Failed to parse research response as JSON: ${error instanceof Error ? error.message : String(error)}`);
54
56
  parsed = undefined;
55
57
  }
56
58
  const researchText = String(parsed?.researchText ?? content ?? text.grizzco.research.empty ?? '');
@@ -1,5 +1,6 @@
1
1
  import { text } from '../../../locales/index.js';
2
2
  import { collectBudgetMetrics, evaluateBudgetAlert, getGlobalAdjuster, recordBudgetAlert, } from '../../context/budget/integration.js';
3
+ import { getEffectivenessTracker } from '../../context/effectiveness/tracker.js';
3
4
  import { recordAuditEvent } from '../../observability/audit-trail.js';
4
5
  import { executeVerifyForWorkspace } from './verify-shared.js';
5
6
  function extractCommandProgram(command) {
@@ -32,7 +33,12 @@ export const runVerify = async (ctx) => {
32
33
  scope: 'session',
33
34
  phase: 'VERIFY',
34
35
  });
35
- // Collect budget metrics after verification
36
+ // Collect budget metrics and effectiveness data after verification
37
+ const effectivenessTracker = getEffectivenessTracker();
38
+ effectivenessTracker.recordExecution(verifyResult.ok, 0);
39
+ if (!verifyResult.ok) {
40
+ effectivenessTracker.recordFailure('missing_context', `Verification failed: ${verifyResult.output?.slice(0, 200) ?? 'unknown'}`);
41
+ }
36
42
  if (ctx.contextResult) {
37
43
  const metrics = collectBudgetMetrics({
38
44
  contextResult: ctx.contextResult,
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { readFile, rm } from '../../adapters/fs/node-fs.js';
5
5
  import { GitAdapter } from '../../adapters/git/git-adapter.js';
6
6
  import { AstParser } from '../../ast/index.js';
7
+ import { getLogger } from '../../observability/logger.js';
7
8
  import { convertDiffToShadowOperations } from '../../patch/diff.js';
8
9
  import { tryGetPluginRegistry } from '../../plugin/registry.js';
9
10
  import { OpType } from '../domain/grizzco-types.js';
@@ -70,7 +71,8 @@ async function defaultBuildProposedSource(workPath, operation) {
70
71
  return null;
71
72
  return showResult.stdout.toString('utf8');
72
73
  }
73
- catch {
74
+ catch (error) {
75
+ getLogger().debug(`[AstValidationService] Failed to build proposed source for "${operation.path}": ${error instanceof Error ? error.message : String(error)}`);
74
76
  return null;
75
77
  }
76
78
  finally {
@@ -1,5 +1,6 @@
1
1
  import { join } from 'path';
2
2
  import { FileAdapter } from '../adapters/fs/index.js';
3
+ import { getLogger } from '../observability/logger.js';
3
4
  /**
4
5
  * Manages persistence of user input history isolated by Session.
5
6
  * Storage path: .salmonloop/ui-history/{sessionId}.json
@@ -26,7 +27,8 @@ export class InputHistoryManager {
26
27
  const data = await this.fileAdapter.readFile(filePath);
27
28
  return JSON.parse(data);
28
29
  }
29
- catch {
30
+ catch (error) {
31
+ getLogger().debug(`[InputHistory] Failed to load history for session "${sessionId}": ${error instanceof Error ? error.message : String(error)}`);
30
32
  return [];
31
33
  }
32
34
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { getLogger } from '../observability/logger.js';
2
3
  const LlmDecisionSchema = z
3
4
  .object({
4
5
  intent: z.enum(['answer', 'review', 'patch', 'debug', 'research']),
@@ -165,8 +166,9 @@ async function routeByLlm(input, options) {
165
166
  try {
166
167
  return JSON.parse(candidate);
167
168
  }
168
- catch {
169
+ catch (error) {
169
170
  // Best-effort: extract the first JSON object from mixed output.
171
+ getLogger().debug(`[ChatIntent] Primary JSON parse failed, attempting extraction: ${error instanceof Error ? error.message : String(error)}`);
170
172
  const start = candidate.indexOf('{');
171
173
  const end = candidate.lastIndexOf('}');
172
174
  if (start >= 0 && end > start) {
@@ -1,6 +1,7 @@
1
1
  import { jsonSchema, tool } from 'ai';
2
2
  import { z } from 'zod';
3
3
  import { zodToJsonSchema } from 'zod-to-json-schema';
4
+ import { getLogger } from '../../observability/logger.js';
4
5
  import { toolToOpenAI } from '../../tools/mapper.js';
5
6
  import { isRecord } from '../../utils/serialize.js';
6
7
  function formatOutputSchema(schema) {
@@ -20,8 +21,9 @@ function formatOutputSchema(schema) {
20
21
  return JSON.stringify(cleanSchema);
21
22
  }
22
23
  }
23
- catch {
24
+ catch (error) {
24
25
  // Fallback to generic description for invalid/unsupported schema.
26
+ getLogger().debug(`[MessageMapper] Failed to format output schema to JSON: ${error instanceof Error ? error.message : String(error)}`);
25
27
  }
26
28
  return 'complex object';
27
29
  }
@@ -31,8 +33,8 @@ function safeParseJsonObject(textValue) {
31
33
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
32
34
  return parsed;
33
35
  }
34
- catch {
35
- // ignored
36
+ catch (error) {
37
+ getLogger().debug(`[MessageMapper] Failed to parse JSON object: ${error instanceof Error ? error.message : String(error)}`);
36
38
  }
37
39
  return {};
38
40
  }
@@ -43,7 +45,8 @@ function deepCloneJson(value, fallback) {
43
45
  return fallback;
44
46
  return JSON.parse(serialized);
45
47
  }
46
- catch {
48
+ catch (error) {
49
+ getLogger().debug(`[MessageMapper] deepCloneJson failed: ${error instanceof Error ? error.message : String(error)}`);
47
50
  return fallback;
48
51
  }
49
52
  }
@@ -102,7 +105,8 @@ export function toAiSdkMessages(messages) {
102
105
  try {
103
106
  parsedContent = JSON.parse(m.content);
104
107
  }
105
- catch {
108
+ catch (error) {
109
+ getLogger().debug(`[MessageMapper] Failed to parse tool message content as JSON: ${error instanceof Error ? error.message : String(error)}`);
106
110
  parsedContent = m.content;
107
111
  }
108
112
  if (isToolApprovalResponse(parsedContent)) {
@@ -247,14 +251,15 @@ export function toOpenAiToolCalls(toolCalls) {
247
251
  try {
248
252
  parsed = JSON.parse(nested);
249
253
  }
250
- catch {
251
- // ignored
254
+ catch (error) {
255
+ getLogger().debug(`[MessageMapper] Failed to parse nested JSON string: ${error instanceof Error ? error.message : String(error)}`);
252
256
  }
253
257
  }
254
258
  }
255
259
  return parsed;
256
260
  }
257
- catch {
261
+ catch (error) {
262
+ getLogger().debug(`[MessageMapper] Failed to normalize tool input JSON: ${error instanceof Error ? error.message : String(error)}`);
258
263
  return raw;
259
264
  }
260
265
  };
@@ -113,9 +113,7 @@ export function buildAiSdkRequestParams(params) {
113
113
  responseFormat: resolveResponseFormat(params.options),
114
114
  toolChoice: (params.options.toolChoice === 'none'
115
115
  ? 'none'
116
- : params.tools
117
- ? 'auto'
118
- : undefined),
116
+ : (params.options.toolChoice ?? (params.tools ? 'auto' : undefined))),
119
117
  providerOptions: mergeProviderOptions({
120
118
  providerOptions: params.options.providerOptions,
121
119
  providerHints: params.options.providerHints,
@@ -37,14 +37,15 @@ function findNetworkCode(err) {
37
37
  }
38
38
  return undefined;
39
39
  }
40
- function isAbortLikeError(err) {
40
+ function isUserAbortError(err) {
41
41
  const unwrapped = unwrapRetryError(err);
42
42
  const name = unwrapped instanceof Error ? unwrapped.name : '';
43
- const msg = String((isRecord(unwrapped) ? unwrapped.message : undefined) ?? unwrapped).toLowerCase();
44
- return name === 'AbortError' || msg.includes('aborted');
43
+ // Only treat as user abort if the error name is AbortError (from AbortController).
44
+ // Provider-side "aborted" messages are transient and should be retried.
45
+ return name === 'AbortError';
45
46
  }
46
47
  export function classifyRetryableApiError(err) {
47
- if (isAbortLikeError(err))
48
+ if (isUserAbortError(err))
48
49
  return { retryable: false, reason: 'aborted' };
49
50
  const statusCode = findStatusCode(err);
50
51
  const networkCode = findNetworkCode(err);
@@ -69,6 +70,13 @@ export function classifyRetryableApiError(err) {
69
70
  if (msg.includes('overloaded')) {
70
71
  return { retryable: true, reason: 'overloaded', statusCode, networkCode };
71
72
  }
73
+ // Provider-side aborts and unexpected errors are transient — retry them.
74
+ if (msg.includes('aborted')) {
75
+ return { retryable: true, reason: 'provider_abort', statusCode, networkCode };
76
+ }
77
+ if (msg.includes('unexpected error')) {
78
+ return { retryable: true, reason: 'unexpected', statusCode, networkCode };
79
+ }
72
80
  if (typeof networkCode === 'string') {
73
81
  const normalized = networkCode.toUpperCase();
74
82
  if (normalized === 'ECONNRESET' ||
@@ -2,7 +2,7 @@ import { toLlmError } from '../errors.js';
2
2
  import { withRetry, withStreamRetry } from '../retry-utils.js';
3
3
  import { createAiSdkRetryLogger, isRetryableAiSdkError } from './request-runtime.js';
4
4
  const DEFAULT_AI_SDK_RETRY_OPTIONS = {
5
- maxRetries: 2,
5
+ maxRetries: 3,
6
6
  jitterRatio: 0.2,
7
7
  };
8
8
  export async function executeWithAiSdkRetry(params) {
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../observability/logger.js';
1
2
  import { SalmonError } from '../types/errors.js';
2
3
  import { sanitizeErrorMessage } from '../utils/sanitizer.js';
3
4
  import { isRecord } from '../utils/serialize.js';
@@ -45,8 +46,8 @@ function extractProviderDetails(err) {
45
46
  details.providerMessage = sanitizeError(parsed.message);
46
47
  }
47
48
  }
48
- catch {
49
- // ignore
49
+ catch (error) {
50
+ getLogger().debug(`[LlmErrors] Failed to parse responseBody as JSON: ${error instanceof Error ? error.message : String(error)}`);
50
51
  }
51
52
  }
52
53
  else if (candidate.responseBody) {
@@ -73,8 +74,8 @@ function extractProviderDetails(err) {
73
74
  details.providerMessage = sanitizeError(parsed.error.message);
74
75
  }
75
76
  }
76
- catch {
77
- // ignore
77
+ catch (error) {
78
+ getLogger().debug(`[LlmErrors] Failed to parse embedded JSON in message: ${error instanceof Error ? error.message : String(error)}`);
78
79
  }
79
80
  }
80
81
  }