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
@@ -3,8 +3,13 @@ import { z } from 'zod';
3
3
  import { text } from '../../../locales/index.js';
4
4
  import { readFile } from '../../adapters/fs/node-fs.js';
5
5
  import { AstParser } from '../../ast/parser.js';
6
+ import { extractImportSpecifiers } from '../../context/ast/import-extractor.js';
7
+ import { resolveImportCandidates } from '../../context/ast/module-resolver.js';
8
+ import { getLogger } from '../../observability/logger.js';
6
9
  import { tryGetPluginRegistry } from '../../plugin/registry.js';
10
+ import { spawnCommand } from '../../runtime/process-runner.js';
7
11
  import { Phase } from '../../types/runtime.js';
12
+ import { normalizePath } from '../../utils/path.js';
8
13
  import { pathPrefixResource } from '../parallel/resource-helpers.js';
9
14
  export const astDefsRefsSpec = {
10
15
  name: 'code.ast',
@@ -59,4 +64,143 @@ export async function executeAstDefsRefs(input, ctx) {
59
64
  // Tree deletion is handled by AstParser's cache cleanup logic or explicit delete if needed
60
65
  }
61
66
  }
67
+ // ── code.find_references ──────────────────────────────────────────────
68
+ const MAX_SCAN_FILES = 30;
69
+ const RG_TIMEOUT_MS = 10_000;
70
+ export const codeFindReferencesSpec = {
71
+ name: 'code.find_references',
72
+ source: 'builtin',
73
+ intent: 'SEARCH',
74
+ description: text.tools.codeFindReferencesDescription,
75
+ riskLevel: 'low',
76
+ sideEffects: ['fs_read'],
77
+ concurrency: 'parallel_ok',
78
+ computeResources: (input, ctx) => [pathPrefixResource(ctx, input.file)],
79
+ inputSchema: z.object({
80
+ file: z.string().describe('Relative path to the file where the symbol is defined'),
81
+ symbol: z.string().describe('The symbol name to find references for'),
82
+ }),
83
+ outputSchema: z.object({
84
+ definition: z
85
+ .object({
86
+ file: z.string(),
87
+ name: z.string(),
88
+ location: z.any(),
89
+ })
90
+ .nullable(),
91
+ references: z.array(z.object({
92
+ file: z.string(),
93
+ name: z.string(),
94
+ location: z.any(),
95
+ })),
96
+ filesScanned: z.number(),
97
+ }),
98
+ allowedPhases: [Phase.CONTEXT, Phase.EXPLORE, Phase.PLAN, Phase.AUTOPILOT],
99
+ };
100
+ export async function executeCodeFindReferences(input, ctx) {
101
+ const repoRoot = ctx.worktreeRoot || ctx.repoRoot;
102
+ const registry = ctx.languagePlugins ?? tryGetPluginRegistry();
103
+ // 1. Parse the target file to find the definition
104
+ const targetPath = join(repoRoot, input.file);
105
+ const targetCode = await readFile(targetPath, 'utf-8');
106
+ const targetLang = registry?.getByExtension(input.file)?.meta.id;
107
+ let definition = null;
108
+ if (targetLang) {
109
+ const tree = await AstParser.parse(targetCode, targetLang);
110
+ const defs = await AstParser.identifyDefinitions(tree, targetLang);
111
+ const match = defs.find((d) => d.name === input.symbol);
112
+ if (match) {
113
+ definition = { file: input.file, name: match.name, location: match.location };
114
+ }
115
+ }
116
+ // 2. Collect candidate files via ripgrep (fast pre-filter)
117
+ const candidates = await rgFindCandidates(repoRoot, input.symbol, input.file);
118
+ // 3. Also collect import neighbors of the target file
119
+ const importNeighbors = await resolveImportNeighbors(input.file, targetCode, repoRoot);
120
+ for (const neighbor of importNeighbors) {
121
+ if (!candidates.includes(neighbor) && neighbor !== input.file) {
122
+ candidates.push(neighbor);
123
+ }
124
+ }
125
+ // 4. Cap the number of files to scan
126
+ const toScan = candidates.slice(0, MAX_SCAN_FILES);
127
+ // 5. Parse each candidate and find references
128
+ const references = [];
129
+ for (const file of toScan) {
130
+ try {
131
+ const lang = registry?.getByExtension(file)?.meta.id;
132
+ if (!lang)
133
+ continue;
134
+ const fullPath = join(repoRoot, file);
135
+ const code = await readFile(fullPath, 'utf-8');
136
+ const tree = await AstParser.parse(code, lang);
137
+ const refs = await AstParser.identifyReferences(tree, lang);
138
+ for (const ref of refs) {
139
+ if (ref.name === input.symbol) {
140
+ references.push({ file, name: ref.name, location: ref.location });
141
+ }
142
+ }
143
+ }
144
+ catch (error) {
145
+ // Skip files that can't be parsed
146
+ getLogger().debug(`[CodeAst] Failed to parse file ${file}: ${error instanceof Error ? error.message : String(error)}`);
147
+ }
148
+ }
149
+ return {
150
+ definition,
151
+ references,
152
+ filesScanned: toScan.length,
153
+ };
154
+ }
155
+ /**
156
+ * Use ripgrep to find files that mention the symbol (fast pre-filter).
157
+ * Returns repo-relative paths, excluding the target file itself.
158
+ */
159
+ async function rgFindCandidates(repoRoot, symbol, excludeFile) {
160
+ let stdout = '';
161
+ try {
162
+ const result = await spawnCommand({
163
+ command: 'rg',
164
+ args: ['--files-with-matches', '--fixed-strings', '--max-count', '1', symbol, '.'],
165
+ cwd: repoRoot,
166
+ env: process.env,
167
+ timeoutMs: RG_TIMEOUT_MS,
168
+ onStdoutChunk: (chunk) => {
169
+ stdout += Buffer.from(chunk).toString();
170
+ },
171
+ onStderrChunk: () => { },
172
+ });
173
+ if (result.error || result.timedOut || (result.code !== 0 && result.code !== 1)) {
174
+ return [];
175
+ }
176
+ const normalizedExclude = normalizePath(excludeFile).replace(/^(\.\/|\/)+/, '');
177
+ return stdout
178
+ .split('\n')
179
+ .map((line) => normalizePath(line.trim()).replace(/^(\.\/|\/)+/, ''))
180
+ .filter((f) => f && f !== normalizedExclude);
181
+ }
182
+ catch (error) {
183
+ getLogger().debug(`[CodeAst] rg candidate search failed: ${error instanceof Error ? error.message : String(error)}`);
184
+ return [];
185
+ }
186
+ }
187
+ /**
188
+ * Resolve import neighbors of a file — files that the target imports.
189
+ */
190
+ async function resolveImportNeighbors(targetFile, targetCode, _repoRoot) {
191
+ const specifiers = extractImportSpecifiers(targetCode);
192
+ const neighbors = [];
193
+ for (const spec of specifiers) {
194
+ if (!spec.startsWith('.'))
195
+ continue;
196
+ const candidates = resolveImportCandidates({ currentFile: targetFile, specifier: spec });
197
+ for (const candidate of candidates) {
198
+ const normalized = normalizePath(candidate).replace(/^(\.\/|\/)+/, '');
199
+ if (normalized && !neighbors.includes(normalized)) {
200
+ neighbors.push(normalized);
201
+ }
202
+ }
203
+ }
204
+ return neighbors;
205
+ }
62
206
  //# sourceMappingURL=ast.js.map
@@ -1,4 +1,5 @@
1
1
  import { LIMITS } from '../../../../config/limits.js';
2
+ import { getLogger } from '../../../../observability/logger.js';
2
3
  import { parsePlainMatches } from '../parse/plain-grep.js';
3
4
  export const psBackend = {
4
5
  id: 'powershell',
@@ -12,7 +13,8 @@ export const psBackend = {
12
13
  });
13
14
  return res.exitCode === 0;
14
15
  }
15
- catch {
16
+ catch (error) {
17
+ getLogger().debug(`[CodeSearch] PowerShell compatibility check failed: ${error instanceof Error ? error.message : String(error)}`);
16
18
  return false;
17
19
  }
18
20
  },
@@ -1,5 +1,6 @@
1
1
  import { resolve } from 'path';
2
2
  import { LIMITS } from '../../../../config/limits.js';
3
+ import { getLogger } from '../../../../observability/logger.js';
3
4
  import { parseRgJson } from '../parse/rg-json.js';
4
5
  export const rgBackend = {
5
6
  id: 'rg',
@@ -9,7 +10,8 @@ export const rgBackend = {
9
10
  const res = await ctx.runner.execFile('rg', ['--version'], { timeoutMs: 1500 });
10
11
  return res.exitCode === 0;
11
12
  }
12
- catch {
13
+ catch (error) {
14
+ getLogger().debug(`[CodeSearch] rg compatibility check failed: ${error instanceof Error ? error.message : String(error)}`);
13
15
  return false;
14
16
  }
15
17
  },
@@ -1,6 +1,7 @@
1
1
  import { LIMITS } from '../../../config/limits.js';
2
2
  import { getLogger } from '../../../observability/logger.js';
3
3
  import { spawnCommand } from '../../../runtime/process-runner.js';
4
+ import { isRecord } from '../../../utils/serialize.js';
4
5
  import { runWithFallback } from '../../capability/executor.js';
5
6
  import { psBackend } from './backends/powershell.js';
6
7
  import { rgBackend } from './backends/rg.js';
@@ -24,53 +25,55 @@ export async function codeSearchExecutor(input, ctx) {
24
25
  attemptId: ctx.attemptId,
25
26
  dryRun: ctx.dryRun,
26
27
  // Allow tests (and callers) to override platform; default to host platform.
27
- platform: ctx.platform ?? process.platform,
28
- runner: ctx.runner ?? {
29
- execFile: async (file, args, opts) => {
30
- const maxStdoutBytes = opts?.maxStdoutBytes ?? Number.POSITIVE_INFINITY;
31
- let stdout = '';
32
- let stderr = '';
33
- let stdoutBytes = 0;
34
- const result = await spawnCommand({
35
- command: file,
36
- args,
37
- cwd: opts?.cwd ?? ctx.repoRoot,
38
- timeoutMs: opts?.timeoutMs,
39
- signal: ctx.signal,
40
- env: { ...process.env, ...ctx.env, ...opts?.env },
41
- onStdoutChunk: (chunk) => {
42
- if (stdoutBytes >= maxStdoutBytes)
43
- return;
44
- const buffer = Buffer.from(chunk);
45
- const remaining = maxStdoutBytes - stdoutBytes;
46
- if (buffer.length <= remaining) {
47
- stdout += buffer.toString();
48
- stdoutBytes += buffer.length;
49
- return;
50
- }
51
- stdout += buffer.subarray(0, remaining).toString();
52
- stdoutBytes += remaining;
53
- },
54
- onStderrChunk: (chunk) => {
55
- stderr += Buffer.from(chunk).toString();
56
- },
57
- });
58
- if (result.error) {
28
+ platform: isRecord(ctx) && typeof ctx.platform === 'string' ? ctx.platform : process.platform,
29
+ runner: isRecord(ctx) && typeof ctx.runner === 'object' && ctx.runner !== null
30
+ ? ctx.runner
31
+ : {
32
+ execFile: async (file, args, opts) => {
33
+ const maxStdoutBytes = opts?.maxStdoutBytes ?? Number.POSITIVE_INFINITY;
34
+ let stdout = '';
35
+ let stderr = '';
36
+ let stdoutBytes = 0;
37
+ const result = await spawnCommand({
38
+ command: file,
39
+ args,
40
+ cwd: opts?.cwd ?? ctx.repoRoot,
41
+ timeoutMs: opts?.timeoutMs,
42
+ signal: ctx.signal,
43
+ env: { ...process.env, ...ctx.env, ...opts?.env },
44
+ onStdoutChunk: (chunk) => {
45
+ if (stdoutBytes >= maxStdoutBytes)
46
+ return;
47
+ const buffer = Buffer.from(chunk);
48
+ const remaining = maxStdoutBytes - stdoutBytes;
49
+ if (buffer.length <= remaining) {
50
+ stdout += buffer.toString();
51
+ stdoutBytes += buffer.length;
52
+ return;
53
+ }
54
+ stdout += buffer.subarray(0, remaining).toString();
55
+ stdoutBytes += remaining;
56
+ },
57
+ onStderrChunk: (chunk) => {
58
+ stderr += Buffer.from(chunk).toString();
59
+ },
60
+ });
61
+ if (result.error) {
62
+ return {
63
+ stdout,
64
+ stderr: stderr || result.error.message,
65
+ exitCode: 1,
66
+ timedOut: false,
67
+ };
68
+ }
59
69
  return {
60
70
  stdout,
61
- stderr: stderr || result.error.message,
62
- exitCode: 1,
63
- timedOut: false,
71
+ stderr,
72
+ exitCode: result.code ?? 1,
73
+ timedOut: result.timedOut,
64
74
  };
65
- }
66
- return {
67
- stdout,
68
- stderr,
69
- exitCode: result.code ?? 1,
70
- timedOut: result.timedOut,
71
- };
75
+ },
72
76
  },
73
- },
74
77
  limits: {
75
78
  timeoutMs: LIMITS.defaultToolTimeoutMs,
76
79
  maxOutputBytes: LIMITS.maxToolOutputBytes,
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../../../../observability/logger.js';
1
2
  /**
2
3
  * A versatile parser for non-JSON-native search tools.
3
4
  * Supports PowerShell JSON objects and traditional line-based formats.
@@ -29,8 +30,9 @@ function parsePsJson(stdout, maxMatches) {
29
30
  }
30
31
  }
31
32
  }
32
- catch {
33
+ catch (error) {
33
34
  // If JSON parsing fails, fallback to empty
35
+ getLogger().debug(`[CodeSearch] Failed to parse PowerShell JSON output: ${error instanceof Error ? error.message : String(error)}`);
34
36
  }
35
37
  return { matches, truncated };
36
38
  }
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../../../../observability/logger.js';
1
2
  /**
2
3
  * Parses the newline-delimited JSON output from ripgrep (--json).
3
4
  */
@@ -22,8 +23,9 @@ export function parseRgJson(stdout, opts) {
22
23
  });
23
24
  }
24
25
  }
25
- catch {
26
+ catch (error) {
26
27
  // Ignore malformed JSON lines
28
+ getLogger().debug(`[CodeSearch] Failed to parse rg JSON line: ${error instanceof Error ? error.message : String(error)}`);
27
29
  }
28
30
  }
29
31
  return { matches, truncated };
@@ -5,8 +5,10 @@ import { z } from 'zod';
5
5
  import { text } from '../../../locales/index.js';
6
6
  import { AtomicFileWriter } from '../../adapters/fs/atomic-file-writer.js';
7
7
  import { mkdir, readFile, readdir, stat } from '../../adapters/fs/node-fs.js';
8
+ import { getLogger } from '../../observability/logger.js';
8
9
  import { Phase } from '../../types/runtime.js';
9
10
  import { normalizeRepoRelativePath } from '../../utils/path.js';
11
+ import { isRecord } from '../../utils/serialize.js';
10
12
  import { pathPrefixResource } from '../parallel/resource-helpers.js';
11
13
  const FsListEntryType = z.enum(['file', 'dir', 'symlink', 'other']);
12
14
  const fsListInputSchema = z.preprocess((raw) => {
@@ -218,7 +220,8 @@ function shouldIncludeListedEntry(dir, entryName, includeHidden) {
218
220
  assertNotReservedRepoPrefix(childPath);
219
221
  return true;
220
222
  }
221
- catch {
223
+ catch (error) {
224
+ getLogger().debug(`[Fs] Reserved path check failed for "${childPath}": ${error instanceof Error ? error.message : String(error)}`);
222
225
  return false;
223
226
  }
224
227
  }
@@ -338,12 +341,13 @@ export const fsWriteFileSpec = {
338
341
  bytesWritten: z.number().int().nonnegative(),
339
342
  }),
340
343
  summarizeArgsForAuthorization: async (args) => {
341
- const encoding = args?.encoding || 'utf-8';
342
- const content = String(args?.content ?? '');
344
+ const a = isRecord(args) ? args : {};
345
+ const encoding = typeof a.encoding === 'string' ? a.encoding : 'utf-8';
346
+ const content = String(a.content ?? '');
343
347
  const bytes = Buffer.byteLength(content, 'utf8');
344
348
  const sha256 = createHash('sha256').update(content, 'utf8').digest('hex');
345
349
  return JSON.stringify({
346
- file: args?.file,
350
+ file: typeof a.file === 'string' ? a.file : undefined,
347
351
  encoding,
348
352
  bytes,
349
353
  sha256,
@@ -364,6 +368,79 @@ export async function executeFsWriteFile(input, ctx) {
364
368
  bytesWritten: contentBytes.length,
365
369
  };
366
370
  }
371
+ // ── fs.edit_file ──────────────────────────────────────────────────────
372
+ const fsEditFileInputSchema = z.preprocess((raw) => {
373
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
374
+ return raw;
375
+ const input = raw;
376
+ if (typeof input.file === 'string')
377
+ return input;
378
+ const alias = input.path ?? input.file_path ?? input.filePath;
379
+ if (typeof alias !== 'string')
380
+ return input;
381
+ return {
382
+ ...input,
383
+ file: alias,
384
+ };
385
+ }, z.object({
386
+ file: z.string().describe('Relative path to the file from the repository root'),
387
+ old_string: z.string().min(1).describe('The exact text to find in the file'),
388
+ new_string: z.string().describe('The replacement text'),
389
+ replace_all: z
390
+ .boolean()
391
+ .optional()
392
+ .describe('Replace all occurrences instead of just the first one'),
393
+ }));
394
+ export const fsEditFileSpec = {
395
+ name: 'fs.edit_file',
396
+ source: 'builtin',
397
+ intent: 'WRITE',
398
+ description: text.tools.fsEditFileDescription,
399
+ riskLevel: 'high',
400
+ sideEffects: ['fs_write'],
401
+ concurrency: 'serial_only',
402
+ computeResources: (input, ctx) => [pathPrefixResource(ctx, input.file)],
403
+ allowedPhases: [Phase.SLASH, Phase.AUTOPILOT],
404
+ inputSchema: fsEditFileInputSchema,
405
+ outputSchema: z.object({
406
+ ok: z.boolean(),
407
+ path: z.string(),
408
+ replacements: z.number().int().nonnegative(),
409
+ }),
410
+ summarizeArgsForAuthorization: async (args) => {
411
+ const a = isRecord(args) ? args : {};
412
+ return JSON.stringify({
413
+ file: typeof a.file === 'string' ? a.file : undefined,
414
+ oldString: String(a.old_string ?? '').slice(0, 80),
415
+ newString: String(a.new_string ?? '').slice(0, 80),
416
+ replaceAll: Boolean(a.replace_all),
417
+ });
418
+ },
419
+ };
420
+ export async function executeFsEditFile(input, ctx) {
421
+ if (ctx.dryRun) {
422
+ return { ok: true, path: input.file, replacements: 0 };
423
+ }
424
+ const { absolutePath } = resolveRepoRelativePath(ctx.repoRoot, input.file);
425
+ const content = await readFile(absolutePath, 'utf-8');
426
+ const count = input.old_string ? content.split(input.old_string).length - 1 : 0;
427
+ if (count === 0) {
428
+ throw new Error(`old_string not found in file "${input.file}". Ensure the string matches exactly, including whitespace and indentation.`);
429
+ }
430
+ if (count > 1 && !input.replace_all) {
431
+ throw new Error(`old_string found ${count} times in "${input.file}", expected exactly 1. Use replace_all: true to replace all occurrences, or provide more surrounding context to uniquely identify the location.`);
432
+ }
433
+ const updated = input.replace_all
434
+ ? content.replaceAll(input.old_string, input.new_string)
435
+ : content.replace(input.old_string, input.new_string);
436
+ const writer = new AtomicFileWriter();
437
+ await writer.writeAtomic(absolutePath, Buffer.from(updated, 'utf8'));
438
+ return {
439
+ ok: true,
440
+ path: input.file,
441
+ replacements: count,
442
+ };
443
+ }
367
444
  const fsCreateDirectoryInputSchema = z.preprocess((raw) => {
368
445
  if (typeof raw === 'string')
369
446
  return { path: raw };
@@ -390,7 +467,10 @@ export const fsCreateDirectorySpec = {
390
467
  ok: z.boolean(),
391
468
  path: z.string(),
392
469
  }),
393
- summarizeArgsForAuthorization: async (args) => JSON.stringify({ path: args?.path, recursive: args?.recursive }),
470
+ summarizeArgsForAuthorization: async (args) => {
471
+ const a = isRecord(args) ? args : {};
472
+ return JSON.stringify({ path: a.path, recursive: a.recursive });
473
+ },
394
474
  };
395
475
  export async function executeFsCreateDirectory(input, ctx) {
396
476
  if (ctx.dryRun) {
@@ -427,7 +507,10 @@ export const fsDeleteFileSpec = {
427
507
  path: z.string(),
428
508
  deleted: z.boolean(),
429
509
  }),
430
- summarizeArgsForAuthorization: async (args) => JSON.stringify({ file: args?.file, missingOk: args?.missingOk }),
510
+ summarizeArgsForAuthorization: async (args) => {
511
+ const a = isRecord(args) ? args : {};
512
+ return JSON.stringify({ file: a.file, missingOk: a.missingOk });
513
+ },
431
514
  };
432
515
  export async function executeFsDeleteFile(input, ctx) {
433
516
  if (ctx.dryRun) {
@@ -439,7 +522,7 @@ export async function executeFsDeleteFile(input, ctx) {
439
522
  await stat(absolutePath);
440
523
  }
441
524
  catch (e) {
442
- const code = e && typeof e === 'object' && 'code' in e ? e.code : undefined;
525
+ const code = isRecord(e) && typeof e.code === 'string' ? e.code : undefined;
443
526
  if (code === 'ENOENT')
444
527
  exists = false;
445
528
  else