salmon-loop 0.3.2 → 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 (227) hide show
  1. package/dist/cli/authorization/non-interactive.js +9 -13
  2. package/dist/cli/authorization/provider.js +2 -10
  3. package/dist/cli/chat.js +12 -6
  4. package/dist/cli/commands/allowlist.js +1 -1
  5. package/dist/cli/commands/chat.js +13 -13
  6. package/dist/cli/commands/config.js +2 -2
  7. package/dist/cli/commands/mode.js +2 -2
  8. package/dist/cli/commands/parallel.js +1 -1
  9. package/dist/cli/commands/run/handler.js +9 -4
  10. package/dist/cli/commands/run/loop-params.js +2 -0
  11. package/dist/cli/commands/run/parse-options.js +14 -26
  12. package/dist/cli/commands/run/runtime-llm.js +15 -12
  13. package/dist/cli/commands/run/runtime-options.js +3 -1
  14. package/dist/cli/config.js +0 -8
  15. package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
  16. package/dist/cli/locales/en.js +2 -2
  17. package/dist/cli/reporters/standard.js +12 -3
  18. package/dist/cli/reporters/stream-json.js +2 -1
  19. package/dist/cli/slash/runtime.js +2 -2
  20. package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
  21. package/dist/cli/ui/hooks/useLoopState.js +1 -1
  22. package/dist/core/adapters/fs/file-adapter.js +3 -1
  23. package/dist/core/adapters/git/git-adapter.js +6 -3
  24. package/dist/core/adapters/git/git-runner.js +5 -2
  25. package/dist/core/adapters/git/lock-manager.js +7 -4
  26. package/dist/core/ast/parser.js +18 -9
  27. package/dist/core/checkpoint-domain/manifest-store.js +21 -13
  28. package/dist/core/checkpoint-domain/service.js +3 -1
  29. package/dist/core/config/limits.js +1 -1
  30. package/dist/core/config/model-pricing.js +61 -0
  31. package/dist/core/config/schema.js +738 -0
  32. package/dist/core/config/validate.js +11 -922
  33. package/dist/core/context/ast/skeleton-extractor.js +225 -0
  34. package/dist/core/context/ast/source-outline.js +24 -1
  35. package/dist/core/context/budget/dynamic-adjuster.js +20 -5
  36. package/dist/core/context/builder.js +7 -3
  37. package/dist/core/context/cache/store-factory.js +3 -1
  38. package/dist/core/context/dependencies.js +2 -1
  39. package/dist/core/context/effectiveness/persistence.js +50 -0
  40. package/dist/core/context/effectiveness/tracker.js +24 -0
  41. package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
  42. package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
  43. package/dist/core/context/gatherers/ast-gatherer.js +34 -40
  44. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
  45. package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
  46. package/dist/core/context/gatherers/knowledge-gatherer.js +21 -2
  47. package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
  48. package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
  49. package/dist/core/context/service.js +12 -2
  50. package/dist/core/context/steps/context-gather.js +14 -3
  51. package/dist/core/context/steps/context-targets.js +1 -0
  52. package/dist/core/context/targeting/target-resolver.js +29 -11
  53. package/dist/core/context/token/cache.js +5 -2
  54. package/dist/core/context/token/encoding-registry.js +7 -6
  55. package/dist/core/context/truncation/strategies/json.js +5 -2
  56. package/dist/core/context/truncation/type-detector.js +3 -1
  57. package/dist/core/extensions/index.js +48 -3
  58. package/dist/core/extensions/load.js +3 -2
  59. package/dist/core/extensions/merge.js +5 -1
  60. package/dist/core/extensions/paths.js +8 -2
  61. package/dist/core/extensions/schemas.js +21 -0
  62. package/dist/core/facades/cli-authorization-provider.js +1 -0
  63. package/dist/core/facades/cli-command-chat.js +2 -0
  64. package/dist/core/facades/cli-run-handler.js +1 -0
  65. package/dist/core/facades/cli-utils-serialize.js +2 -0
  66. package/dist/core/feedback/parsers.js +290 -1
  67. package/dist/core/grizzco/dsl/llm-strategy.js +4 -3
  68. package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
  69. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +30 -13
  70. package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
  71. package/dist/core/grizzco/engine/transaction/attempt-failure.js +49 -24
  72. package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
  73. package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
  74. package/dist/core/grizzco/execution/RejectionManager.js +7 -5
  75. package/dist/core/grizzco/runtime/apply-back-runtime.js +5 -2
  76. package/dist/core/grizzco/services/implementations/default/GitConfigService.js +2 -1
  77. package/dist/core/grizzco/services/registry.js +18 -0
  78. package/dist/core/grizzco/steps/audit.js +20 -10
  79. package/dist/core/grizzco/steps/autopilot.js +21 -32
  80. package/dist/core/grizzco/steps/display-report.js +4 -11
  81. package/dist/core/grizzco/steps/explore.js +14 -4
  82. package/dist/core/grizzco/steps/generateReview.js +3 -1
  83. package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
  84. package/dist/core/grizzco/steps/patch.js +1 -0
  85. package/dist/core/grizzco/steps/plan.js +58 -49
  86. package/dist/core/grizzco/steps/research.js +3 -1
  87. package/dist/core/grizzco/steps/tool-runtime.js +3 -0
  88. package/dist/core/grizzco/steps/verify.js +7 -1
  89. package/dist/core/grizzco/validation/AstValidationService.js +3 -1
  90. package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
  91. package/dist/core/history/input-history.js +3 -1
  92. package/dist/core/intent/chat-intent.js +3 -1
  93. package/dist/core/llm/ai-sdk/message-mapper.js +37 -26
  94. package/dist/core/llm/ai-sdk/request-params.js +2 -6
  95. package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
  96. package/dist/core/llm/ai-sdk/retry-classifier.js +17 -7
  97. package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
  98. package/dist/core/llm/contracts/repair.js +16 -8
  99. package/dist/core/llm/errors.js +18 -14
  100. package/dist/core/llm/output-policy.js +8 -0
  101. package/dist/core/llm/redact.js +1 -3
  102. package/dist/core/llm/retry-utils.js +8 -2
  103. package/dist/core/llm/stream-utils.js +5 -3
  104. package/dist/core/llm/sub-agent-factory.js +51 -0
  105. package/dist/core/llm/tool-calling-stub.js +48 -0
  106. package/dist/core/llm/utils.js +17 -6
  107. package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
  108. package/dist/core/mcp/bridge/resource-context-provider.js +3 -1
  109. package/dist/core/mcp/bridge/tool-bridge.js +5 -14
  110. package/dist/core/mcp/catalog/discovery.js +3 -1
  111. package/dist/core/mcp/client/connection-manager.js +7 -4
  112. package/dist/core/mcp/client/transport-factory.js +7 -3
  113. package/dist/core/mcp/host/sampling-provider.js +1 -1
  114. package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
  115. package/dist/core/memory/relevant-retrieval.js +6 -4
  116. package/dist/core/observability/audit-file.js +2 -1
  117. package/dist/core/observability/audit-trail.js +3 -1
  118. package/dist/core/observability/authorization-decisions.js +13 -12
  119. package/dist/core/observability/error-mapping.js +2 -1
  120. package/dist/core/observability/logger.js +2 -1
  121. package/dist/core/observability/monitor.js +24 -0
  122. package/dist/core/observability/run-outcome-reporter.js +1 -0
  123. package/dist/core/observability/token-usage.js +5 -4
  124. package/dist/core/permission-gate/default-gate.js +5 -8
  125. package/dist/core/plan/storage.js +7 -4
  126. package/dist/core/plugin/loader.js +8 -5
  127. package/dist/core/prompts/registry.js +12 -30
  128. package/dist/core/prompts/runtime.js +3 -1
  129. package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
  130. package/dist/core/protocols/a2a/sdk/executor.js +3 -1
  131. package/dist/core/protocols/a2a/sdk/server.js +5 -4
  132. package/dist/core/protocols/acp/acp-command-runner.js +7 -6
  133. package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
  134. package/dist/core/protocols/acp/formal-agent.js +13 -6
  135. package/dist/core/protocols/acp/permission-provider.js +3 -2
  136. package/dist/core/protocols/acp/stdio-server.js +6 -6
  137. package/dist/core/reflection/engine.js +114 -14
  138. package/dist/core/runtime/agent-server-runtime.js +3 -2
  139. package/dist/core/runtime/batch-runner.js +81 -0
  140. package/dist/core/runtime/initialize.js +71 -6
  141. package/dist/core/runtime/loop-finalize.js +3 -0
  142. package/dist/core/runtime/loop-session-runner.js +5 -0
  143. package/dist/core/runtime/loop.js +4 -0
  144. package/dist/core/runtime/paths.js +9 -6
  145. package/dist/core/runtime/spawn-interactive.js +5 -4
  146. package/dist/core/security/redaction.js +3 -2
  147. package/dist/core/session/compaction/index.js +4 -3
  148. package/dist/core/session/compression.js +3 -1
  149. package/dist/core/session/manager.js +26 -38
  150. package/dist/core/session/pruning-strategy.js +2 -1
  151. package/dist/core/session/token-tracker.js +27 -9
  152. package/dist/core/skills/parser.js +3 -2
  153. package/dist/core/skills/permissions.js +2 -2
  154. package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
  155. package/dist/core/skills/runtime/SkillRunner.js +5 -2
  156. package/dist/core/slash/steps/slash-execute.js +7 -5
  157. package/dist/core/slash/strategy.js +1 -1
  158. package/dist/core/strata/checkpoint/manager.js +16 -10
  159. package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
  160. package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
  161. package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
  162. package/dist/core/strata/interaction/file-system-provider.js +2 -1
  163. package/dist/core/strata/layers/file-state-resolver.js +9 -7
  164. package/dist/core/strata/layers/immutable-git-layer.js +3 -1
  165. package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
  166. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
  167. package/dist/core/strata/layers/worktree.js +9 -10
  168. package/dist/core/strata/runtime/environment.js +2 -1
  169. package/dist/core/strata/runtime/synchronizer.js +28 -26
  170. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
  171. package/dist/core/structured-output/json-extract.js +3 -1
  172. package/dist/core/structured-output/json-schema-validator.js +1 -13
  173. package/dist/core/sub-agent/artifacts/store.js +2 -1
  174. package/dist/core/sub-agent/context-snapshot.js +12 -6
  175. package/dist/core/sub-agent/controller.js +70 -1
  176. package/dist/core/sub-agent/core/loop.js +25 -3
  177. package/dist/core/sub-agent/core/manager.js +343 -117
  178. package/dist/core/sub-agent/registry-defaults.js +12 -0
  179. package/dist/core/sub-agent/registry.js +8 -0
  180. package/dist/core/sub-agent/summary.js +96 -0
  181. package/dist/core/sub-agent/team.js +98 -0
  182. package/dist/core/sub-agent/tools/task-await.js +109 -0
  183. package/dist/core/sub-agent/tools/task-spawn.js +52 -7
  184. package/dist/core/sub-agent/tools/team.js +92 -0
  185. package/dist/core/sub-agent/types.js +11 -2
  186. package/dist/core/target-runtime/profile.js +3 -1
  187. package/dist/core/tools/audit.js +3 -2
  188. package/dist/core/tools/budget.js +7 -12
  189. package/dist/core/tools/builtin/ast.js +144 -0
  190. package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
  191. package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
  192. package/dist/core/tools/builtin/code-search/executor.js +46 -43
  193. package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
  194. package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
  195. package/dist/core/tools/builtin/fs.js +90 -7
  196. package/dist/core/tools/builtin/git.js +242 -0
  197. package/dist/core/tools/builtin/glob.js +79 -0
  198. package/dist/core/tools/builtin/index.js +53 -111
  199. package/dist/core/tools/builtin/interaction.js +13 -15
  200. package/dist/core/tools/builtin/knowledge.js +146 -4
  201. package/dist/core/tools/builtin/proposal.js +14 -3
  202. package/dist/core/tools/builtin/verify.js +35 -3
  203. package/dist/core/tools/capability/executor.js +5 -5
  204. package/dist/core/tools/headless-payload.js +1 -3
  205. package/dist/core/tools/mapper.js +8 -42
  206. package/dist/core/tools/parallel/persistence.js +17 -5
  207. package/dist/core/tools/parallel/scheduler.js +23 -21
  208. package/dist/core/tools/permissions/permission-rules.js +69 -115
  209. package/dist/core/tools/plugins/loader.js +4 -3
  210. package/dist/core/tools/router.js +112 -58
  211. package/dist/core/tools/session.js +64 -102
  212. package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
  213. package/dist/core/tools/tool-visibility.js +2 -1
  214. package/dist/core/tools/types.js +10 -0
  215. package/dist/core/types/batch.js +2 -0
  216. package/dist/core/utils/error.js +79 -0
  217. package/dist/core/utils/sanitizer.js +5 -2
  218. package/dist/core/utils/serialize.js +66 -0
  219. package/dist/core/utils/zod.js +29 -0
  220. package/dist/core/verification/detect-runner.js +86 -0
  221. package/dist/core/verification/runner.js +76 -0
  222. package/dist/core/version.js +3 -1
  223. package/dist/core/workspace/capabilities.js +3 -2
  224. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
  225. package/dist/languages/python/index.js +154 -0
  226. package/dist/locales/en.js +8 -1
  227. package/package.json +2 -1
@@ -147,6 +147,14 @@ export function emitLlmStreamDelta(params) {
147
147
  timestamp,
148
148
  });
149
149
  }
150
+ /**
151
+ * Clean up stream state without emitting end events.
152
+ * Call this when a stream errors and emitLlmStreamEnd won't be reached.
153
+ */
154
+ export function cleanupLlmStream(streamId) {
155
+ STREAM_CANONICAL_EMITTERS.delete(streamId);
156
+ STREAM_SANITIZATION_STATE.delete(streamId);
157
+ }
150
158
  export function emitLlmStreamEnd(params) {
151
159
  const { emit, policy, kind, step, streamId, finishReason } = params;
152
160
  if (!emit)
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '../utils/serialize.js';
1
2
  const SECRET_KEY_REGEX = /(api[-_]?key|authorization|token|secret|password|cookie)/i;
2
3
  const STRING_SECRET_PATTERNS = [
3
4
  {
@@ -17,9 +18,6 @@ const STRING_SECRET_PATTERNS = [
17
18
  replacement: '$1=[REDACTED]',
18
19
  },
19
20
  ];
20
- function isRecord(value) {
21
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
22
- }
23
21
  function truncate(value, max = 500) {
24
22
  if (value.length <= max)
25
23
  return value;
@@ -1,4 +1,8 @@
1
+ /**
2
+ * Utility for retrying asynchronous operations with exponential backoff.
3
+ */
1
4
  import { LIMITS } from '../config/limits.js';
5
+ import { getLogger } from '../observability/logger.js';
2
6
  const DEFAULT_OPTIONS = {
3
7
  maxRetries: LIMITS.retry.api.maxAttempts,
4
8
  initialDelayMs: LIMITS.retry.api.initialDelayMs,
@@ -42,8 +46,9 @@ export async function withRetry(fn, options = {}) {
42
46
  try {
43
47
  await opts.onRetry({ attempt: attempt + 1, delayMs: effectiveDelay, error });
44
48
  }
45
- catch {
49
+ catch (retryHandlerError) {
46
50
  // Ignore onRetry handler failures.
51
+ getLogger().debug(`[RetryUtils] onRetry handler failed: ${retryHandlerError instanceof Error ? retryHandlerError.message : String(retryHandlerError)}`);
47
52
  }
48
53
  await new Promise((resolve, reject) => {
49
54
  const timer = setTimeout(() => {
@@ -91,8 +96,9 @@ export async function* withStreamRetry(streamFactory, options = {}) {
91
96
  try {
92
97
  await opts.onRetry({ attempt: attempt + 1, delayMs: effectiveDelay, error });
93
98
  }
94
- catch {
99
+ catch (retryHandlerError) {
95
100
  // Ignore onRetry handler failures.
101
+ getLogger().debug(`[RetryUtils] onRetry handler failed (stream): ${retryHandlerError instanceof Error ? retryHandlerError.message : String(retryHandlerError)}`);
96
102
  }
97
103
  await new Promise((resolve, reject) => {
98
104
  const timer = setTimeout(() => {
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../observability/logger.js';
1
2
  function normalizeToolInput(raw) {
2
3
  if (typeof raw !== 'string')
3
4
  return raw;
@@ -12,14 +13,15 @@ function normalizeToolInput(raw) {
12
13
  try {
13
14
  parsed = JSON.parse(nested);
14
15
  }
15
- catch {
16
- // ignored
16
+ catch (error) {
17
+ getLogger().debug(`[StreamUtils] Failed to parse nested JSON string: ${error instanceof Error ? error.message : String(error)}`);
17
18
  }
18
19
  }
19
20
  }
20
21
  return parsed;
21
22
  }
22
- catch {
23
+ catch (error) {
24
+ getLogger().debug(`[StreamUtils] Failed to normalize tool input JSON: ${error instanceof Error ? error.message : String(error)}`);
23
25
  return raw;
24
26
  }
25
27
  }
@@ -0,0 +1,51 @@
1
+ import { AiSdkLLM } from './ai-sdk.js';
2
+ import { StubLLM } from './openai.js';
3
+ /**
4
+ * Model alias → concrete model ID mapping.
5
+ *
6
+ * These follow the Anthropic model naming conventions.
7
+ * Providers that don't support these IDs will fall back to StubLLM.
8
+ */
9
+ const MODEL_ALIAS_MAP = {
10
+ haiku: 'claude-3-5-haiku-20241022',
11
+ sonnet: 'claude-sonnet-4-20250514',
12
+ opus: 'claude-opus-4-20250514',
13
+ };
14
+ function resolveModelId(alias) {
15
+ return MODEL_ALIAS_MAP[alias] ?? alias;
16
+ }
17
+ /**
18
+ * Create a SubAgentLlmFactory that produces model-specific LLM instances
19
+ * from a base provider configuration.
20
+ *
21
+ * The factory reuses the parent provider's connection settings (API key,
22
+ * base URL, headers) and only overrides the model ID.
23
+ */
24
+ export function createSubAgentLlmFactory(baseProvider) {
25
+ return (modelAlias) => {
26
+ // 'inherit' means use the parent LLM — don't create a new instance.
27
+ if (!modelAlias || modelAlias === 'inherit')
28
+ return undefined;
29
+ const modelId = resolveModelId(modelAlias);
30
+ if (baseProvider.type === 'openai-compatible' || baseProvider.type === 'openai') {
31
+ const clientPackage = baseProvider.clientPackage === '@ai-sdk/openai'
32
+ ? '@ai-sdk/openai'
33
+ : '@ai-sdk/openai-compatible';
34
+ if (!baseProvider.api.apiKey) {
35
+ return new StubLLM();
36
+ }
37
+ return new AiSdkLLM({
38
+ clientPackage,
39
+ providerName: baseProvider.id,
40
+ apiKey: baseProvider.api.apiKey,
41
+ baseUrl: baseProvider.api.baseUrl,
42
+ modelId,
43
+ headers: baseProvider.api.headers,
44
+ timeoutMs: baseProvider.api.timeoutMs,
45
+ capabilities: baseProvider.capabilities,
46
+ });
47
+ }
48
+ return undefined;
49
+ };
50
+ }
51
+ //# sourceMappingURL=sub-agent-factory.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ToolCallingStubLLM — A deterministic LLM stub that emits tool_calls
3
+ * to drive the chatWithTools loop in tests and evaluation harnesses.
4
+ *
5
+ * Unlike StubLLM (toolCalling: false, no tool_calls), this stub populates
6
+ * the assistant.tool_calls field so that chatWithTools executes tools and
7
+ * continues the loop.
8
+ */
9
+ export class ToolCallingStubLLM {
10
+ toolCalling = true;
11
+ turns;
12
+ callCount = 0;
13
+ constructor(turns) {
14
+ this.turns = turns;
15
+ }
16
+ getCapabilities() {
17
+ return {
18
+ toolCalling: true,
19
+ responseFormatJsonObject: false,
20
+ streaming: false,
21
+ };
22
+ }
23
+ async chat(_messages) {
24
+ const idx = this.callCount;
25
+ const turn = this.turns[idx] ?? { content: '[stub: no more turns]' };
26
+ this.callCount++;
27
+ return {
28
+ role: 'assistant',
29
+ content: turn.content ?? '',
30
+ tool_calls: turn.toolCalls,
31
+ };
32
+ }
33
+ getCallCount() {
34
+ return this.callCount;
35
+ }
36
+ async createPlan(_context, instruction) {
37
+ return {
38
+ goal: `Stub plan for: ${instruction}`,
39
+ files: [],
40
+ changes: [],
41
+ verify: 'echo ok',
42
+ };
43
+ }
44
+ async createPatch() {
45
+ return '';
46
+ }
47
+ }
48
+ //# sourceMappingURL=tool-calling-stub.js.map
@@ -13,21 +13,32 @@ export function formatContextForPrompt(context, options = {}) {
13
13
  export function parsePlanFromLLMContent(content) {
14
14
  const trimmed = String(content ?? '').trim();
15
15
  if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
16
- throw new Error(text.llm.planInvalidJson);
16
+ const preview = trimmed.slice(0, 80);
17
+ throw new Error(`${text.llm.planInvalidJson} — Content must start with { and end with }. Got: ${preview}${trimmed.length > 80 ? '…' : ''}`);
17
18
  }
18
19
  let parsed;
19
20
  try {
20
21
  parsed = JSON.parse(trimmed);
21
22
  }
22
- catch {
23
- throw new Error(text.llm.planInvalidJson);
23
+ catch (jsonError) {
24
+ const errorMsg = jsonError instanceof Error ? jsonError.message : String(jsonError);
25
+ throw new Error(`${text.llm.planInvalidJson} — JSON parse error: ${errorMsg}`);
24
26
  }
25
27
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
26
- throw new Error(text.llm.planInvalidJson);
28
+ throw new Error(`${text.llm.planInvalidJson} — Expected object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);
27
29
  }
28
30
  const plan = parsed;
29
- if (!plan.goal || !Array.isArray(plan.files) || !Array.isArray(plan.changes) || !plan.verify) {
30
- throw new Error(text.llm.planInvalid);
31
+ const missingKeys = [];
32
+ if (!plan.goal)
33
+ missingKeys.push('goal');
34
+ if (!Array.isArray(plan.files))
35
+ missingKeys.push('files (must be array)');
36
+ if (!Array.isArray(plan.changes))
37
+ missingKeys.push('changes (must be array)');
38
+ if (!plan.verify)
39
+ missingKeys.push('verify');
40
+ if (missingKeys.length > 0) {
41
+ throw new Error(`${text.llm.planInvalid} — Missing or invalid keys: ${missingKeys.join(', ')}`);
31
42
  }
32
43
  return plan;
33
44
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { isRecord } from '../../utils/serialize.js';
2
3
  import { jsonSchemaToZod } from '../schema/json-schema-to-zod.js';
3
4
  const SAFE_TOKEN_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
4
5
  function normalizeToken(value) {
@@ -16,11 +17,11 @@ function safeToken(value, fallback) {
16
17
  return fallback;
17
18
  }
18
19
  function isPromptOptions(value) {
19
- return Boolean(value &&
20
- typeof value === 'object' &&
20
+ return Boolean(isRecord(value) &&
21
21
  'serverName' in value &&
22
22
  'client' in value &&
23
- typeof value.client?.listPrompts === 'function');
23
+ isRecord(value.client) &&
24
+ typeof value.client.listPrompts === 'function');
24
25
  }
25
26
  function buildFallbackSchemaFromArguments(args = []) {
26
27
  const shape = {};
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../../observability/logger.js';
1
2
  import { ResourceCache } from '../cache/resource-cache.js';
2
3
  export class ResourceContextProviderError extends Error {
3
4
  diagnostic;
@@ -252,7 +253,8 @@ function normalizeText(text, mimeType) {
252
253
  try {
253
254
  return JSON.stringify(JSON.parse(text), null, 2);
254
255
  }
255
- catch {
256
+ catch (error) {
257
+ getLogger().debug(`[ResourceContextProvider] Failed to normalize JSON text: ${error instanceof Error ? error.message : String(error)}`);
256
258
  return text;
257
259
  }
258
260
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { LIMITS } from '../../config/limits.js';
3
3
  import { Phase } from '../../types/runtime.js';
4
+ import { safeStringify } from '../../utils/serialize.js';
4
5
  import { classifyMcpTool } from '../policy/classifier.js';
5
6
  import { jsonSchemaToZod } from '../schema/json-schema-to-zod.js';
6
7
  const MCP_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
@@ -111,10 +112,9 @@ export function wrapMcpToolResult(result) {
111
112
  }
112
113
  export function mcpToolToToolSpec(input) {
113
114
  const override = findOverride(input.server.capabilities, input.tool.name);
114
- const classification = classifyMcpTool({
115
- tool: input.tool,
115
+ const classification = classifyMcpTool(input.tool, {
116
116
  trust: input.server.trust,
117
- override,
117
+ override: override ? { sideEffects: override } : undefined,
118
118
  });
119
119
  const phase = input.server.capabilities.tools.phases[0] ?? Phase.VERIFY;
120
120
  const grantDecision = input.policy.decideTool({
@@ -169,7 +169,7 @@ export async function registerMcpV2Tools(input) {
169
169
  if (!catalog)
170
170
  continue;
171
171
  for (const tool of catalog.tools) {
172
- const classification = classifyMcpTool({ tool: tool, trust: server.trust });
172
+ const classification = classifyMcpTool(tool, { trust: server.trust });
173
173
  const decision = input.policy.decideTool({
174
174
  server: server.name,
175
175
  toolName: tool.name,
@@ -267,16 +267,7 @@ function summarizeMcpAuthorization(input, args, riskLevel, sideEffects) {
267
267
  : { kind: 'classified', reason: input.classification.reason },
268
268
  args,
269
269
  };
270
- return safeStringify(payload);
271
- }
272
- function safeStringify(value, maxLength = 1200) {
273
- try {
274
- const raw = JSON.stringify(value);
275
- return raw.length <= maxLength ? raw : `${raw.slice(0, maxLength)}...`;
276
- }
277
- catch {
278
- return '[Unserializable]';
279
- }
270
+ return safeStringify(payload, { maxLength: 1200 });
280
271
  }
281
272
  function coerceRecord(input) {
282
273
  if (!input || typeof input !== 'object' || Array.isArray(input)) {
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../../observability/logger.js';
1
2
  import { withPromptServer } from './prompt-catalog.js';
2
3
  import { withResourceServer, withResourceTemplateServer } from './resource-catalog.js';
3
4
  import { withToolServer } from './tool-catalog.js';
@@ -5,7 +6,8 @@ async function safeList(fn, fallback) {
5
6
  try {
6
7
  return await fn();
7
8
  }
8
- catch {
9
+ catch (error) {
10
+ getLogger().debug(`[McpDiscovery] safeList call failed, returning fallback: ${error instanceof Error ? error.message : String(error)}`);
9
11
  return fallback;
10
12
  }
11
13
  }
@@ -2,6 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { CallToolResultSchema, PromptListChangedNotificationSchema, ReadResourceResultSchema, ResourceListChangedNotificationSchema, ResourceUpdatedNotificationSchema, ToolListChangedNotificationSchema, GetPromptResultSchema, } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { LIMITS } from '../../config/limits.js';
4
4
  import { getLogger } from '../../observability/logger.js';
5
+ import { errorMessage } from '../../utils/error.js';
5
6
  import { PACKAGE_VERSION } from '../../version.js';
6
7
  import { discoverMcpCatalog } from '../catalog/discovery.js';
7
8
  import { McpNotificationRouter } from '../catalog/notification-router.js';
@@ -93,7 +94,7 @@ export class McpConnectionManager {
93
94
  }
94
95
  catch (error) {
95
96
  entry.status = 'degraded';
96
- entry.error = error instanceof Error ? error.message : String(error);
97
+ entry.error = errorMessage(error);
97
98
  getLogger().warn(`Failed to connect MCP server ${server.name}: ${entry.error}`);
98
99
  }
99
100
  return this.view(entry);
@@ -183,7 +184,7 @@ export class McpConnectionManager {
183
184
  entry.subscribedResources.add(uri);
184
185
  }
185
186
  catch (error) {
186
- const message = error instanceof Error ? error.message : String(error);
187
+ const message = errorMessage(error);
187
188
  getLogger().warn(`MCP server ${entry.server.name} resource subscription failed for ${uri}: ${message}`);
188
189
  }
189
190
  }
@@ -207,8 +208,9 @@ export class McpConnectionManager {
207
208
  try {
208
209
  await entry.client.unsubscribeResource({ uri }, { timeout: LIMITS.defaultToolTimeoutMs });
209
210
  }
210
- catch {
211
+ catch (error) {
211
212
  // best-effort unsubscribe during shutdown
213
+ getLogger().debug(`[McpConnectionManager] Failed to unsubscribe resource ${uri}: ${error instanceof Error ? error.message : String(error)}`);
212
214
  }
213
215
  }
214
216
  entry.subscribedResources.clear();
@@ -223,8 +225,9 @@ export class McpConnectionManager {
223
225
  }
224
226
  await entry.client.close();
225
227
  }
226
- catch {
228
+ catch (error) {
227
229
  // best-effort shutdown
230
+ getLogger().debug(`[McpConnectionManager] Error during connection shutdown: ${error instanceof Error ? error.message : String(error)}`);
228
231
  }
229
232
  }
230
233
  view(entry) {
@@ -1,6 +1,7 @@
1
1
  import { PassThrough } from 'node:stream';
2
2
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
3
  import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/sdk/shared/stdio.js';
4
+ import { getLogger } from '../../observability/logger.js';
4
5
  import { spawnInteractiveProcess } from '../../runtime/process-runner.js';
5
6
  export function buildStrictStdioEnvironment(env) {
6
7
  return { ...env };
@@ -81,16 +82,18 @@ export class StrictStdioClientTransport {
81
82
  try {
82
83
  child.stdin?.end?.();
83
84
  }
84
- catch {
85
+ catch (error) {
85
86
  // best-effort shutdown
87
+ getLogger().debug(`[TransportFactory] Error ending stdin: ${error instanceof Error ? error.message : String(error)}`);
86
88
  }
87
89
  await Promise.race([closePromise, unrefTimeout(2_000)]);
88
90
  if (child.exitCode === null) {
89
91
  try {
90
92
  child.kill('SIGTERM');
91
93
  }
92
- catch {
94
+ catch (error) {
93
95
  // best-effort shutdown
96
+ getLogger().debug(`[TransportFactory] Error sending SIGTERM: ${error instanceof Error ? error.message : String(error)}`);
94
97
  }
95
98
  await Promise.race([closePromise, unrefTimeout(2_000)]);
96
99
  }
@@ -98,8 +101,9 @@ export class StrictStdioClientTransport {
98
101
  try {
99
102
  child.kill('SIGKILL');
100
103
  }
101
- catch {
104
+ catch (error) {
102
105
  // best-effort shutdown
106
+ getLogger().debug(`[TransportFactory] Error sending SIGKILL: ${error instanceof Error ? error.message : String(error)}`);
103
107
  }
104
108
  }
105
109
  this.readBuffer.clear();
@@ -120,7 +120,7 @@ export class McpSamplingProvider {
120
120
  throw new Error('MCP_SAMPLING_TOKEN_LIMIT_EXCEEDED');
121
121
  }
122
122
  const result = await this.llm.chat([{ role: 'user', content: input.prompt }]);
123
- return typeof result === 'string' ? result : String(result?.content ?? '');
123
+ return typeof result === 'string' ? result : String(result.content ?? '');
124
124
  }
125
125
  async createMessage(params, options = {}) {
126
126
  const sanitizedParams = sanitizeForAudit(params, this.maxDepth);
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
  import { z } from 'zod';
3
+ import { isRecord } from '../../utils/serialize.js';
3
4
  export function jsonSchemaToZod(jsonSchema) {
4
5
  return jsonSchemaToZodWithContext(jsonSchema, jsonSchema);
5
6
  }
@@ -499,7 +500,7 @@ function propertyNamesToZod(jsonSchema, rootSchema) {
499
500
  return jsonSchemaToZodWithContext(jsonSchema, rootSchema);
500
501
  }
501
502
  function isPlainObject(value) {
502
- return typeof value === 'object' && value !== null && !Array.isArray(value);
503
+ return isRecord(value);
503
504
  }
504
505
  function isMultipleOf(value, divisor) {
505
506
  const quotient = value / divisor;
@@ -77,11 +77,12 @@ export function buildRelevantMemoryCandidates(context) {
77
77
  tags: ['rules', 'project'],
78
78
  });
79
79
  }
80
- if (trimToUndefined(knowledge?.user_preferences)) {
80
+ const userPrefs = trimToUndefined(knowledge?.user_preferences);
81
+ if (userPrefs) {
81
82
  candidates.push({
82
83
  path: '.salmonloop/knowledge/user_preferences',
83
84
  title: 'User preferences',
84
- summary: knowledge.user_preferences,
85
+ summary: userPrefs,
85
86
  tags: ['preferences', 'user'],
86
87
  });
87
88
  }
@@ -96,11 +97,12 @@ export function buildRelevantMemoryCandidates(context) {
96
97
  tags: ['architecture', ...(decision.related_files ?? []).map((file) => file.toLowerCase())],
97
98
  });
98
99
  }
99
- if (trimToUndefined(metadata?.aiInstructions)) {
100
+ const aiInstructions = trimToUndefined(metadata?.aiInstructions);
101
+ if (aiInstructions) {
100
102
  candidates.push({
101
103
  path: '.salmonloop/project/ai-instructions',
102
104
  title: 'Project AI instructions',
103
- summary: metadata.aiInstructions,
105
+ summary: aiInstructions,
104
106
  tags: ['instructions', 'project'],
105
107
  });
106
108
  }
@@ -28,8 +28,9 @@ async function writeJsonAtomic(targetPath, data) {
28
28
  try {
29
29
  await rename(tmpPath, targetPath);
30
30
  }
31
- catch {
31
+ catch (error) {
32
32
  // Retry once with a fresh temp file to reduce transient rename failures.
33
+ getLogger().debug(`[AuditFile] Atomic rename failed, retrying: ${error instanceof Error ? error.message : String(error)}`);
33
34
  const retryTmpPath = path.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}-retry`);
34
35
  await writeFile(retryTmpPath, payload);
35
36
  await rename(retryTmpPath, targetPath);
@@ -1,3 +1,4 @@
1
+ import { getLogger } from './logger.js';
1
2
  const auditTrail = [];
2
3
  const auditContext = {};
3
4
  const DEFAULT_BUFFER_LIMITS = {
@@ -19,7 +20,8 @@ function estimateEventSize(event) {
19
20
  try {
20
21
  return Buffer.byteLength(JSON.stringify(event), 'utf-8');
21
22
  }
22
- catch {
23
+ catch (error) {
24
+ getLogger().debug(`[AuditTrail] Failed to estimate event size: ${error instanceof Error ? error.message : String(error)}`);
23
25
  return 0;
24
26
  }
25
27
  }
@@ -1,3 +1,4 @@
1
+ import { asRecord } from '../utils/serialize.js';
1
2
  import { getAuditTrail } from './audit-trail.js';
2
3
  function safeString(value) {
3
4
  if (typeof value !== 'string')
@@ -23,13 +24,13 @@ export function extractAuthorizationDecisionsFromAuditTrail(auditTrail) {
23
24
  continue;
24
25
  if (event.action !== 'authorization.decision')
25
26
  continue;
26
- const details = event.details;
27
- if (!details || typeof details !== 'object')
27
+ if (!event.details || typeof event.details !== 'object')
28
28
  continue;
29
- const callId = safeString(details.callId);
30
- const toolName = safeString(details.toolName);
31
- const phase = safeString(details.phase) ?? safeString(event.phase);
32
- const outcome = safeString(details.outcome);
29
+ const d = asRecord(event.details);
30
+ const callId = safeString(d.callId);
31
+ const toolName = safeString(d.toolName);
32
+ const phase = safeString(d.phase) ?? safeString(event.phase);
33
+ const outcome = safeString(d.outcome);
33
34
  if (!callId || !toolName || !phase || !outcome)
34
35
  continue;
35
36
  decisions.push({
@@ -37,12 +38,12 @@ export function extractAuthorizationDecisionsFromAuditTrail(auditTrail) {
37
38
  toolName,
38
39
  phase: phase,
39
40
  outcome: outcome,
40
- source: (safeString(details.source) ?? 'unknown'),
41
- reason: safeString(details.reason),
42
- ttlMs: safeNumber(details.ttlMs),
43
- persist: safeString(details.persist),
44
- riskLevel: safeString(details.riskLevel),
45
- sideEffects: safeStringArray(details.sideEffects),
41
+ source: (safeString(d.source) ?? 'unknown'),
42
+ reason: safeString(d.reason),
43
+ ttlMs: safeNumber(d.ttlMs),
44
+ persist: safeString(d.persist),
45
+ riskLevel: safeString(d.riskLevel),
46
+ sideEffects: safeStringArray(d.sideEffects),
46
47
  timestamp: safeString(event.timestamp) ?? new Date().toISOString(),
47
48
  });
48
49
  }
@@ -1,4 +1,5 @@
1
1
  import { text } from '../../locales/index.js';
2
+ import { isRecord } from '../utils/serialize.js';
2
3
  import { getAuditTrail } from './audit-trail.js';
3
4
  import { REDACTED_ERROR_TOKEN } from './error-envelope.js';
4
5
  function mapLlmCodeToMessage(code) {
@@ -193,7 +194,7 @@ export function mapErrorForAudit(input) {
193
194
  };
194
195
  }
195
196
  function buildLangfuseHttpFailed(details) {
196
- const status = typeof details?.status === 'number' ? details.status : undefined;
197
+ const status = isRecord(details) && typeof details.status === 'number' ? details.status : undefined;
197
198
  if (!status)
198
199
  return undefined;
199
200
  if (status === 401 || status === 403) {
@@ -307,8 +307,9 @@ export class Logger {
307
307
  // Only remove from queue if write was successful
308
308
  this.logQueue.splice(0, contentToFlush.length);
309
309
  }
310
- catch {
310
+ catch (error) {
311
311
  // Keep logs in queue for next retry
312
+ this.debug(`[Logger] Failed to flush logs to file: ${error instanceof Error ? error.message : String(error)}`);
312
313
  }
313
314
  finally {
314
315
  this.isFlushing = false;
@@ -177,6 +177,30 @@ export class Monitor {
177
177
  /**
178
178
  * Generate metrics report
179
179
  */
180
+ getStructuredReport() {
181
+ const durations = [...this.applyBackMetrics.durations].sort((a, b) => a - b);
182
+ return {
183
+ checkpoint: {
184
+ createAttempts: this.checkpointMetrics.createAttempts,
185
+ createFailures: this.checkpointMetrics.createFailures,
186
+ createFailureRate: this.getCheckpointCreateFailureRate(),
187
+ cleanupAttempts: this.checkpointMetrics.cleanupAttempts,
188
+ cleanupFailures: this.checkpointMetrics.cleanupFailures,
189
+ },
190
+ applyBack: {
191
+ attempts: this.applyBackMetrics.attempts,
192
+ failures: this.applyBackMetrics.failures,
193
+ avgDurationMs: this.getApplyBackAvgDuration(),
194
+ p50DurationMs: durations.length > 0 ? durations[Math.floor(durations.length * 0.5)] : 0,
195
+ p95DurationMs: durations.length > 0 ? durations[Math.floor(durations.length * 0.95)] : 0,
196
+ },
197
+ errors: this.errorHistory.toArray().map((e) => ({
198
+ type: e.type,
199
+ message: e.message,
200
+ timestamp: e.timestamp.toISOString(),
201
+ })),
202
+ };
203
+ }
180
204
  getMetricsReport() {
181
205
  let report = `\n${text.monitor.metricsTitle}\n`;
182
206
  report += `\n${text.monitor.checkpointCreation}\n`;
@@ -7,6 +7,7 @@ export function buildRunOutcomeReport(result) {
7
7
  safeHint: result.safeHint,
8
8
  remediationSteps: result.remediationSteps,
9
9
  attempts: result.attempts,
10
+ durationMs: result.durationMs,
10
11
  failurePhase: result.failurePhase,
11
12
  errorCode: result.errorCode,
12
13
  changedFiles: result.changedFiles,
@@ -1,3 +1,4 @@
1
+ import { asRecord } from '../utils/serialize.js';
1
2
  import { getAuditTrail } from './audit-trail.js';
2
3
  function safeFiniteNumber(value) {
3
4
  if (typeof value !== 'number' || !Number.isFinite(value))
@@ -12,11 +13,11 @@ export function extractTokenUsageFromAuditTrail(auditTrail) {
12
13
  continue;
13
14
  if (event.action !== 'llm.usage')
14
15
  continue;
15
- const details = event.details;
16
- if (!details || typeof details !== 'object')
16
+ if (!event.details || typeof event.details !== 'object')
17
17
  continue;
18
- const promptTokens = safeFiniteNumber(details.promptTokens);
19
- const completionTokens = safeFiniteNumber(details.completionTokens);
18
+ const d = asRecord(event.details);
19
+ const promptTokens = safeFiniteNumber(d.promptTokens);
20
+ const completionTokens = safeFiniteNumber(d.completionTokens);
20
21
  if (typeof promptTokens === 'number')
21
22
  inputTokens += promptTokens;
22
23
  if (typeof completionTokens === 'number')