salmon-loop 0.2.3 → 0.2.16

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 (234) hide show
  1. package/dist/cli/argv/headless-detection.js +27 -0
  2. package/dist/cli/chat-flow.js +11 -0
  3. package/dist/cli/chat.js +161 -24
  4. package/dist/cli/commands/chat.js +30 -24
  5. package/dist/cli/commands/context.js +15 -3
  6. package/dist/cli/commands/flow-mode.js +63 -0
  7. package/dist/cli/commands/help-format.js +12 -0
  8. package/dist/cli/commands/registry.js +6 -7
  9. package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
  10. package/dist/cli/commands/run/config-resolution.js +30 -24
  11. package/dist/cli/commands/run/early-errors.js +23 -0
  12. package/dist/cli/commands/run/handler.js +131 -44
  13. package/dist/cli/commands/run/headless-error-writer.js +8 -0
  14. package/dist/cli/commands/run/loop-params.js +3 -0
  15. package/dist/cli/commands/run/mode.js +2 -5
  16. package/dist/cli/commands/run/parse-options.js +18 -2
  17. package/dist/cli/commands/run/persist-session.js +10 -1
  18. package/dist/cli/commands/run/preflight.js +10 -0
  19. package/dist/cli/commands/run/reporter-factory.js +4 -0
  20. package/dist/cli/commands/run/runtime-llm.js +38 -11
  21. package/dist/cli/commands/run/runtime-options.js +2 -2
  22. package/dist/cli/commands/run/validate-options.js +0 -5
  23. package/dist/cli/commands/run/verbose.js +2 -7
  24. package/dist/cli/commands/serve.js +117 -90
  25. package/dist/cli/commands/tool-names.js +78 -78
  26. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
  27. package/dist/cli/headless/json-protocol.js +37 -0
  28. package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
  29. package/dist/cli/headless/protocol-metadata.js +22 -0
  30. package/dist/cli/headless/stream-json-protocol.js +34 -1
  31. package/dist/cli/index.js +6 -4
  32. package/dist/cli/locales/en.js +32 -6
  33. package/dist/cli/program-bootstrap.js +14 -4
  34. package/dist/cli/program-commands.js +9 -1
  35. package/dist/cli/program-options.js +1 -0
  36. package/dist/cli/reporters/anthropic-stream.js +7 -1
  37. package/dist/cli/reporters/json.js +4 -0
  38. package/dist/cli/reporters/stream-json.js +17 -2
  39. package/dist/cli/run-cli.js +5 -3
  40. package/dist/cli/slash/runtime.js +30 -15
  41. package/dist/cli/ui/components/CommandInput.js +7 -3
  42. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  43. package/dist/cli/utils/command-option-source.js +13 -0
  44. package/dist/cli/utils/output-format.js +6 -0
  45. package/dist/cli/utils/resolve-cli-config.js +98 -0
  46. package/dist/cli/utils/verbose-level.js +8 -0
  47. package/dist/cli/utils/verify-resolver.js +8 -4
  48. package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
  49. package/dist/core/adapters/fs/file-adapter.js +6 -0
  50. package/dist/core/adapters/fs/filesystem.js +2 -1
  51. package/dist/core/adapters/git/git-adapter.js +78 -1
  52. package/dist/core/benchmark/patch-artifact.js +124 -0
  53. package/dist/core/benchmark/swe-bench.js +25 -0
  54. package/dist/core/config/load.js +39 -18
  55. package/dist/core/config/merge.js +27 -0
  56. package/dist/core/config/paths.js +24 -5
  57. package/dist/core/config/resolve-llm.js +12 -0
  58. package/dist/core/config/resolve.js +7 -5
  59. package/dist/core/config/resolvers/server.js +0 -6
  60. package/dist/core/config/validate.js +94 -21
  61. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  62. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  63. package/dist/core/context/keywords.js +18 -4
  64. package/dist/core/context/service-deps.js +2 -2
  65. package/dist/core/context/service.js +8 -0
  66. package/dist/core/context/steps/context-gather.js +38 -0
  67. package/dist/core/context/summarization/summarizer.js +55 -12
  68. package/dist/core/context/targeting/target-resolver.js +4 -4
  69. package/dist/core/extensions/index.js +23 -5
  70. package/dist/core/extensions/paths.js +31 -0
  71. package/dist/core/extensions/schemas.js +8 -5
  72. package/dist/core/facades/cli-chat.js +6 -2
  73. package/dist/core/facades/cli-command-chat.js +2 -1
  74. package/dist/core/facades/cli-command-tool-names.js +2 -0
  75. package/dist/core/facades/cli-context.js +1 -0
  76. package/dist/core/facades/cli-observability.js +1 -1
  77. package/dist/core/facades/cli-run-handler.js +4 -2
  78. package/dist/core/facades/cli-run-persist-session.js +1 -0
  79. package/dist/core/facades/cli-serve.js +2 -4
  80. package/dist/core/facades/cli-utils-worktree.js +1 -1
  81. package/dist/core/failure/diagnostics.js +53 -1
  82. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  83. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  84. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  85. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  86. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  87. package/dist/core/grizzco/engine/transaction/transaction-runner.js +173 -7
  88. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  89. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  90. package/dist/core/grizzco/steps/answer.js +13 -14
  91. package/dist/core/grizzco/steps/autopilot.js +396 -0
  92. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  93. package/dist/core/grizzco/steps/explore.js +37 -21
  94. package/dist/core/grizzco/steps/generateReview.js +2 -5
  95. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  96. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  97. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  98. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  99. package/dist/core/grizzco/steps/patch.js +105 -146
  100. package/dist/core/grizzco/steps/plan.js +101 -25
  101. package/dist/core/grizzco/steps/preflight.js +5 -3
  102. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  103. package/dist/core/grizzco/steps/research.js +39 -36
  104. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  105. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  106. package/dist/core/grizzco/steps/verify.js +13 -21
  107. package/dist/core/intent/chat-intent.js +0 -4
  108. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  109. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  110. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  111. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  112. package/dist/core/llm/ai-sdk/request-params.js +74 -1
  113. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  114. package/dist/core/llm/ai-sdk.js +112 -27
  115. package/dist/core/llm/capabilities.js +12 -0
  116. package/dist/core/llm/contracts/repair.js +36 -30
  117. package/dist/core/llm/errors.js +83 -2
  118. package/dist/core/llm/message-composition.js +7 -22
  119. package/dist/core/llm/phase-router.js +29 -10
  120. package/dist/core/llm/redact.js +28 -3
  121. package/dist/core/llm/registry.js +2 -0
  122. package/dist/core/llm/request-augmentation.js +55 -0
  123. package/dist/core/llm/request-envelope.js +334 -0
  124. package/dist/core/llm/shared-request-assembly.js +35 -0
  125. package/dist/core/llm/stream-utils.js +13 -4
  126. package/dist/core/llm/utils.js +18 -29
  127. package/dist/core/memory/relevant-retrieval.js +144 -0
  128. package/dist/core/observability/logger.js +11 -2
  129. package/dist/core/patch/diff.js +1 -0
  130. package/dist/core/prompts/registry.js +39 -2
  131. package/dist/core/prompts/runtime.js +50 -12
  132. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  133. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  134. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  135. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  136. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  137. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  138. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  139. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  140. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  141. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  142. package/dist/core/protocols/a2a/agent-card.js +3 -2
  143. package/dist/core/protocols/a2a/sdk/executor.js +8 -6
  144. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  145. package/dist/core/protocols/acp/formal-agent.js +221 -55
  146. package/dist/core/protocols/acp/handlers.js +5 -1
  147. package/dist/core/protocols/acp/permission-provider.js +21 -1
  148. package/dist/core/protocols/shared/execution-request.js +24 -0
  149. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  150. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  151. package/dist/core/public-capabilities/projections.js +29 -0
  152. package/dist/core/public-capabilities/registry.js +26 -0
  153. package/dist/core/public-capabilities/types.js +2 -0
  154. package/dist/core/runtime/agent-server-runtime.js +47 -43
  155. package/dist/core/runtime/execution-profile.js +67 -0
  156. package/dist/core/session/artifact-state.js +160 -0
  157. package/dist/core/session/compaction/index.js +183 -0
  158. package/dist/core/session/compaction/microcompact.js +78 -0
  159. package/dist/core/session/compaction/tracking.js +48 -0
  160. package/dist/core/session/compaction/types.js +11 -0
  161. package/dist/core/session/compression.js +12 -4
  162. package/dist/core/session/manager.js +247 -10
  163. package/dist/core/session/pruning-strategy.js +55 -9
  164. package/dist/core/session/replacement-preview-provider.js +24 -0
  165. package/dist/core/session/replacement-state.js +131 -0
  166. package/dist/core/session/resume-repair/pipeline.js +79 -0
  167. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  168. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  169. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  170. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  171. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  172. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  173. package/dist/core/session/resume-repair/types.js +2 -0
  174. package/dist/core/session/summary-sync.js +164 -13
  175. package/dist/core/session/token-tracker.js +6 -0
  176. package/dist/core/skills/audit.js +34 -0
  177. package/dist/core/skills/bridge.js +84 -7
  178. package/dist/core/skills/discovery.js +94 -0
  179. package/dist/core/skills/feature-flags.js +52 -0
  180. package/dist/core/skills/index.js +1 -1
  181. package/dist/core/skills/loader.js +195 -20
  182. package/dist/core/skills/parser.js +296 -24
  183. package/dist/core/skills/permissions.js +117 -0
  184. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  185. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  186. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  187. package/dist/core/strata/layers/worktree.js +70 -13
  188. package/dist/core/strata/runtime/synchronizer.js +29 -2
  189. package/dist/core/streaming/stream-assembler.js +75 -31
  190. package/dist/core/sub-agent/context-snapshot.js +156 -0
  191. package/dist/core/sub-agent/core/loop.js +1 -1
  192. package/dist/core/sub-agent/core/manager.js +119 -20
  193. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  194. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  195. package/dist/core/sub-agent/registry-defaults.js +4 -0
  196. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  197. package/dist/core/sub-agent/types.js +134 -5
  198. package/dist/core/tools/audit.js +13 -4
  199. package/dist/core/tools/builtin/ast-grep.js +1 -1
  200. package/dist/core/tools/builtin/ast.js +1 -1
  201. package/dist/core/tools/builtin/benchmark.js +360 -0
  202. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  203. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  204. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  205. package/dist/core/tools/builtin/fs.js +256 -23
  206. package/dist/core/tools/builtin/git.js +2 -2
  207. package/dist/core/tools/builtin/index.js +51 -2
  208. package/dist/core/tools/builtin/interaction.js +8 -1
  209. package/dist/core/tools/builtin/plan.js +37 -15
  210. package/dist/core/tools/builtin/shell.js +1 -1
  211. package/dist/core/tools/loader.js +39 -16
  212. package/dist/core/tools/mapper.js +17 -3
  213. package/dist/core/tools/parallel/scheduler.js +35 -4
  214. package/dist/core/tools/permissions/permission-rules.js +5 -10
  215. package/dist/core/tools/policy.js +6 -1
  216. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  217. package/dist/core/tools/router.js +24 -6
  218. package/dist/core/tools/session.js +458 -48
  219. package/dist/core/tools/tool-visibility.js +62 -0
  220. package/dist/core/tools/types.js +9 -1
  221. package/dist/core/types/execution.js +4 -0
  222. package/dist/core/types/flow-mode.js +8 -0
  223. package/dist/core/utils/path.js +52 -0
  224. package/dist/core/verification/runner.js +4 -1
  225. package/dist/interfaces/cli/task-runner.js +4 -3
  226. package/dist/languages/typescript/index.js +4 -1
  227. package/dist/locales/en.js +87 -2
  228. package/dist/utils/eol.js +1 -1
  229. package/package.json +15 -8
  230. package/scripts/fix-es-abstract-compat.js +77 -0
  231. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  232. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  233. package/dist/core/runtime/sidecar-paths.js +0 -47
  234. package/dist/core/runtime/sidecar-route-catalog.js +0 -103
@@ -1,34 +1,55 @@
1
1
  import { readFile } from '../adapters/fs/node-fs.js';
2
2
  import { ConfigError } from './errors.js';
3
3
  import { parseConfigText } from './file-format.js';
4
- import { getDefaultRepoConfigPaths, resolveConfigPath } from './paths.js';
4
+ import { getDefaultRepoConfigPaths, getDefaultUserConfigPaths, resolveConfigPath, } from './paths.js';
5
5
  import { validateConfigFileV1 } from './validate.js';
6
- export async function tryLoadConfigFile(opts) {
7
- if (!opts.enabled)
8
- return null;
9
- const candidatePaths = opts.configPath
10
- ? [resolveConfigPath(opts.repoRoot, opts.configPath)]
11
- : getDefaultRepoConfigPaths(opts.repoRoot);
12
- for (let i = 0; i < candidatePaths.length; i++) {
13
- const absPath = candidatePaths[i];
6
+ async function loadFromCandidates(candidatePaths, required) {
7
+ const results = await Promise.all(candidatePaths.map(async (absPath) => {
14
8
  try {
15
9
  const raw = await readFile(absPath, 'utf8');
16
10
  const parsed = parseConfigText(raw, absPath);
17
11
  const config = validateConfigFileV1(parsed);
18
- return { path: absPath, config };
12
+ return { ok: true, absPath, loaded: { path: absPath, config } };
19
13
  }
20
14
  catch (e) {
21
- if ((e && typeof e === 'object' && 'code' in e ? e.code : undefined) ===
22
- 'ENOENT') {
23
- const isLast = i === candidatePaths.length - 1;
24
- if (opts.required && isLast) {
25
- throw new ConfigError('CONFIG_FILE_NOT_FOUND', { path: absPath });
26
- }
27
- continue;
15
+ return { ok: false, absPath, error: e };
16
+ }
17
+ }));
18
+ for (const [i, result] of results.entries()) {
19
+ if (result.ok) {
20
+ return result.loaded;
21
+ }
22
+ const code = result.error && typeof result.error === 'object' && 'code' in result.error
23
+ ? result.error.code
24
+ : undefined;
25
+ if (code === 'ENOENT') {
26
+ const isLast = i === results.length - 1;
27
+ if (required && isLast) {
28
+ throw new ConfigError('CONFIG_FILE_NOT_FOUND', { path: result.absPath });
28
29
  }
29
- throw e;
30
+ continue;
30
31
  }
32
+ throw result.error;
31
33
  }
32
34
  return null;
33
35
  }
36
+ export async function tryLoadConfigFile(opts) {
37
+ if (!opts.enabled)
38
+ return null;
39
+ const candidatePaths = opts.configPath
40
+ ? [resolveConfigPath(opts.repoRoot, opts.configPath)]
41
+ : getDefaultRepoConfigPaths(opts.repoRoot);
42
+ return loadFromCandidates(candidatePaths, opts.required);
43
+ }
44
+ export async function loadConfigStack(opts) {
45
+ if (!opts.enabled)
46
+ return {};
47
+ if (opts.configPath) {
48
+ const loaded = await tryLoadConfigFile(opts);
49
+ return loaded ? { repo: loaded } : {};
50
+ }
51
+ const repo = await loadFromCandidates(getDefaultRepoConfigPaths(opts.repoRoot), false);
52
+ const user = await loadFromCandidates(getDefaultUserConfigPaths(), false);
53
+ return { repo: repo ?? undefined, user: user ?? undefined };
54
+ }
34
55
  //# sourceMappingURL=load.js.map
@@ -0,0 +1,27 @@
1
+ function isPlainObject(value) {
2
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3
+ }
4
+ function mergeValues(userValue, repoValue) {
5
+ if (repoValue === undefined)
6
+ return userValue;
7
+ if (userValue === undefined)
8
+ return repoValue;
9
+ if (isPlainObject(userValue) && isPlainObject(repoValue)) {
10
+ const merged = { ...userValue };
11
+ for (const [key, value] of Object.entries(repoValue)) {
12
+ merged[key] = mergeValues(userValue[key], value);
13
+ }
14
+ return merged;
15
+ }
16
+ return repoValue;
17
+ }
18
+ export function mergeConfigFiles(userConfig, repoConfig) {
19
+ if (!userConfig && !repoConfig)
20
+ return undefined;
21
+ if (!userConfig)
22
+ return repoConfig;
23
+ if (!repoConfig)
24
+ return userConfig;
25
+ return mergeValues(userConfig, repoConfig);
26
+ }
27
+ //# sourceMappingURL=merge.js.map
@@ -1,4 +1,11 @@
1
- import { join, resolve } from 'path';
1
+ import { homedir } from 'os';
2
+ import { defaultPathAdapter } from '../adapters/path/path-adapter.js';
3
+ function resolveUserConfigHome() {
4
+ const override = (process.env.SALMONLOOP_USER_CONFIG_HOME || '').trim();
5
+ if (override)
6
+ return defaultPathAdapter.resolve(override);
7
+ return homedir();
8
+ }
2
9
  /**
3
10
  * Repo-local configuration lives under ".salmonloop/" and is expected to be gitignored.
4
11
  * Runtime state is stored under ".salmonloop/runtime/" (audit, rejections, tmp, locks).
@@ -7,14 +14,26 @@ export function getDefaultRepoConfigPath(repoRoot) {
7
14
  return getDefaultRepoConfigPaths(repoRoot)[0];
8
15
  }
9
16
  export function getDefaultRepoConfigPaths(repoRoot) {
10
- const base = join(resolve(repoRoot), '.salmonloop', 'config');
11
- return [join(base, 'config.yaml'), join(base, 'config.yml'), join(base, 'config.json')];
17
+ const base = defaultPathAdapter.join(defaultPathAdapter.resolve(repoRoot), '.salmonloop', 'config');
18
+ return [
19
+ defaultPathAdapter.join(base, 'config.yaml'),
20
+ defaultPathAdapter.join(base, 'config.yml'),
21
+ defaultPathAdapter.join(base, 'config.json'),
22
+ ];
23
+ }
24
+ export function getDefaultUserConfigPaths() {
25
+ const base = defaultPathAdapter.join(resolveUserConfigHome(), '.salmonloop', 'config');
26
+ return [
27
+ defaultPathAdapter.join(base, 'config.yaml'),
28
+ defaultPathAdapter.join(base, 'config.yml'),
29
+ defaultPathAdapter.join(base, 'config.json'),
30
+ ];
12
31
  }
13
32
  export function resolveConfigPath(repoRoot, configPath) {
14
33
  // Relative paths are resolved against the target repo root (not the CLI's cwd).
15
- return resolve(repoRoot, configPath);
34
+ return defaultPathAdapter.resolve(repoRoot, configPath);
16
35
  }
17
36
  export function getDefaultIndexPath(repoRoot) {
18
- return join(resolve(repoRoot), '.salmonloop', 'index');
37
+ return defaultPathAdapter.join(defaultPathAdapter.resolve(repoRoot), '.salmonloop', 'index');
19
38
  }
20
39
  //# sourceMappingURL=paths.js.map
@@ -1,6 +1,14 @@
1
1
  import { resolveBaseUrl } from '../llm/base-url.js';
2
2
  import { ConfigError } from './errors.js';
3
3
  import { firstProviderRef, resolveApiKey, resolveModelId } from './resolve-env.js';
4
+ function mergeCapabilities(providerCapabilities, modelCapabilities) {
5
+ if (!providerCapabilities && !modelCapabilities)
6
+ return undefined;
7
+ return {
8
+ ...providerCapabilities,
9
+ ...modelCapabilities,
10
+ };
11
+ }
4
12
  export function resolveLlmFromConfig(raw) {
5
13
  const llm = raw?.llm;
6
14
  const providers = llm?.providers || {};
@@ -49,6 +57,7 @@ export function resolveLlmFromConfig(raw) {
49
57
  const apiKeyResolution = resolveApiKey(provider.api?.apiKey);
50
58
  const baseUrl = resolveBaseUrl(provider.api?.baseUrl);
51
59
  const selectedModelId = resolveModelId(activeProfile.id);
60
+ const activeCapabilities = mergeCapabilities(provider.capabilities, activeProfile.capabilities);
52
61
  const routing = llm?.routing;
53
62
  const phaseToProviderModel = routing?.phaseToModel && typeof routing.phaseToModel === 'object'
54
63
  ? Object.fromEntries(Object.entries(routing.phaseToModel)
@@ -76,6 +85,7 @@ export function resolveLlmFromConfig(raw) {
76
85
  });
77
86
  }
78
87
  const phaseKey = resolveApiKey(phaseProvider.api?.apiKey);
88
+ const capabilities = mergeCapabilities(phaseProvider.capabilities, profile.capabilities);
79
89
  return [
80
90
  phase,
81
91
  {
@@ -93,6 +103,7 @@ export function resolveLlmFromConfig(raw) {
93
103
  id: resolveModelId(profile.id),
94
104
  slot: profileSlot,
95
105
  },
106
+ capabilities,
96
107
  },
97
108
  ];
98
109
  })
@@ -124,6 +135,7 @@ export function resolveLlmFromConfig(raw) {
124
135
  selectedModelId,
125
136
  selectedModelSlot: activeModelSlot,
126
137
  },
138
+ capabilities: activeCapabilities,
127
139
  routing: resolvedRouting,
128
140
  };
129
141
  }
@@ -1,5 +1,6 @@
1
1
  import { resolveLlmOutputPolicy } from '../llm/output-policy.js';
2
- import { tryLoadConfigFile } from './load.js';
2
+ import { loadConfigStack } from './load.js';
3
+ import { mergeConfigFiles } from './merge.js';
3
4
  import { getDefaultRepoConfigPath } from './paths.js';
4
5
  import { resolveLlmFromConfig } from './resolve-llm.js';
5
6
  import { resolveAstValidationStrictness } from './resolvers/ast-validation.js';
@@ -15,20 +16,21 @@ export async function resolveConfig(opts) {
15
16
  const enabled = opts.enableConfigFile !== false;
16
17
  const path = opts.configFilePath;
17
18
  const required = Boolean(opts.configFilePath);
18
- const loaded = await tryLoadConfigFile({
19
+ const loaded = await loadConfigStack({
19
20
  repoRoot: opts.repoRoot,
20
21
  configPath: path,
21
22
  enabled,
22
23
  required,
23
24
  });
24
- const raw = loaded?.config;
25
+ const raw = mergeConfigFiles(loaded.user?.config, loaded.repo?.config);
25
26
  const uiLogMode = resolveUiLogMode(raw);
26
27
  const permissionMode = resolvePermissionMode(raw);
28
+ const sourcePath = loaded.repo?.path || loaded.user?.path || path || getDefaultRepoConfigPath(opts.repoRoot);
27
29
  return {
28
30
  source: {
29
31
  enabled,
30
- path: loaded?.path || path || getDefaultRepoConfigPath(opts.repoRoot),
31
- used: Boolean(loaded),
32
+ path: sourcePath,
33
+ used: Boolean(loaded.repo || loaded.user),
32
34
  },
33
35
  raw,
34
36
  permissionMode,
@@ -10,12 +10,6 @@ export function resolveServerConfig(raw) {
10
10
  tokens: serverRaw.a2a.tokens,
11
11
  };
12
12
  }
13
- if (serverRaw.sidecar) {
14
- server.sidecar = {
15
- socket: serverRaw.sidecar.socket,
16
- allowConditional: serverRaw.sidecar.allowConditional,
17
- };
18
- }
19
13
  if (serverRaw.acp) {
20
14
  server.acp = {
21
15
  sessionStore: {
@@ -26,6 +26,59 @@ function isValidMarkdownTheme(value) {
26
26
  function isValidMarkdownRenderMode(value) {
27
27
  return typeof value === 'string' && MARKDOWN_RENDER_MODES.includes(value);
28
28
  }
29
+ function validateLlmCapabilities(input, context) {
30
+ if (input === undefined)
31
+ return undefined;
32
+ if (!isRecord(input)) {
33
+ throw new ConfigError('CONFIG_INVALID_LLM_CAPABILITIES', {
34
+ ...context,
35
+ expected: 'object',
36
+ });
37
+ }
38
+ const allowedKeys = new Set(['toolCalling', 'responseFormatJsonObject', 'streaming']);
39
+ for (const key of Object.keys(input)) {
40
+ if (!allowedKeys.has(key)) {
41
+ throw new ConfigError('CONFIG_INVALID_LLM_CAPABILITY', {
42
+ ...context,
43
+ capability: key,
44
+ expected: 'toolCalling|responseFormatJsonObject|streaming',
45
+ });
46
+ }
47
+ }
48
+ const cfg = {};
49
+ for (const key of ['toolCalling', 'responseFormatJsonObject', 'streaming']) {
50
+ if (input[key] !== undefined && !isBoolean(input[key])) {
51
+ throw new ConfigError('CONFIG_INVALID_LLM_CAPABILITY', {
52
+ ...context,
53
+ capability: key,
54
+ expected: 'boolean',
55
+ });
56
+ }
57
+ if (input[key] !== undefined) {
58
+ cfg[key] = input[key];
59
+ }
60
+ }
61
+ return cfg;
62
+ }
63
+ function normalizeLlmModelParams(input, context) {
64
+ if (!isRecord(input))
65
+ return undefined;
66
+ for (const key of [
67
+ 'capabilities',
68
+ 'toolCalling',
69
+ 'responseFormatJsonObject',
70
+ 'streaming',
71
+ ]) {
72
+ if (input[key] !== undefined) {
73
+ throw new ConfigError('CONFIG_INVALID_LLM_CAPABILITY_LOCATION', {
74
+ ...context,
75
+ capability: key,
76
+ expected: 'model.capabilities',
77
+ });
78
+ }
79
+ }
80
+ return input;
81
+ }
29
82
  export function validateConfigFileV1(input) {
30
83
  if (!isRecord(input)) {
31
84
  throw new ConfigError('CONFIG_INVALID_ROOT', { expected: 'object' });
@@ -164,6 +217,11 @@ export function validateConfigFileV1(input) {
164
217
  if (!isRecord(serverRaw)) {
165
218
  throw new ConfigError('CONFIG_INVALID_SERVER', { expected: 'object' });
166
219
  }
220
+ for (const key of Object.keys(serverRaw)) {
221
+ if (key !== 'a2a' && key !== 'acp') {
222
+ throw new ConfigError('CONFIG_INVALID_SERVER_UNKNOWN_KEY', { key });
223
+ }
224
+ }
167
225
  const server = {};
168
226
  if (serverRaw.a2a !== undefined) {
169
227
  if (!isRecord(serverRaw.a2a)) {
@@ -187,24 +245,6 @@ export function validateConfigFileV1(input) {
187
245
  tokens: a2aRaw.tokens,
188
246
  };
189
247
  }
190
- if (serverRaw.sidecar !== undefined) {
191
- if (!isRecord(serverRaw.sidecar)) {
192
- throw new ConfigError('CONFIG_INVALID_SERVER_SIDECAR', { expected: 'object' });
193
- }
194
- const sidecarRaw = serverRaw.sidecar;
195
- if (sidecarRaw.socket !== undefined && !isString(sidecarRaw.socket)) {
196
- throw new ConfigError('CONFIG_INVALID_SERVER_SIDECAR_SOCKET', { expected: 'string' });
197
- }
198
- if (sidecarRaw.allowConditional !== undefined && !isBoolean(sidecarRaw.allowConditional)) {
199
- throw new ConfigError('CONFIG_INVALID_SERVER_SIDECAR_ALLOW_CONDITIONAL', {
200
- expected: 'boolean',
201
- });
202
- }
203
- server.sidecar = {
204
- socket: sidecarRaw.socket,
205
- allowConditional: sidecarRaw.allowConditional,
206
- };
207
- }
208
248
  if (serverRaw.acp !== undefined) {
209
249
  if (!isRecord(serverRaw.acp)) {
210
250
  throw new ConfigError('CONFIG_INVALID_SERVER_ACP', { expected: 'object' });
@@ -471,6 +511,27 @@ export function validateConfigFileV1(input) {
471
511
  }
472
512
  if (input.llm.activeModel !== undefined)
473
513
  cfg.llm.activeModel = input.llm.activeModel;
514
+ const llmAny = input.llm;
515
+ if (llmAny.simpleModel !== undefined && !isString(llmAny.simpleModel)) {
516
+ throw new ConfigError('CONFIG_INVALID_LLM_SIMPLE_MODEL', { expected: 'string' });
517
+ }
518
+ if (llmAny.simpleModel !== undefined)
519
+ cfg.llm.simpleModel = llmAny.simpleModel;
520
+ if (llmAny.mediumModel !== undefined && !isString(llmAny.mediumModel)) {
521
+ throw new ConfigError('CONFIG_INVALID_LLM_MEDIUM_MODEL', { expected: 'string' });
522
+ }
523
+ if (llmAny.mediumModel !== undefined)
524
+ cfg.llm.mediumModel = llmAny.mediumModel;
525
+ if (llmAny.complexModel !== undefined && !isString(llmAny.complexModel)) {
526
+ throw new ConfigError('CONFIG_INVALID_LLM_COMPLEX_MODEL', { expected: 'string' });
527
+ }
528
+ if (llmAny.complexModel !== undefined)
529
+ cfg.llm.complexModel = llmAny.complexModel;
530
+ if (llmAny.reasoningModel !== undefined && !isString(llmAny.reasoningModel)) {
531
+ throw new ConfigError('CONFIG_INVALID_LLM_REASONING_MODEL', { expected: 'string' });
532
+ }
533
+ if (llmAny.reasoningModel !== undefined)
534
+ cfg.llm.reasoningModel = llmAny.reasoningModel;
474
535
  if (input.llm.providers !== undefined) {
475
536
  if (!isRecord(input.llm.providers)) {
476
537
  throw new ConfigError('CONFIG_INVALID_LLM_PROVIDERS', { expected: 'object' });
@@ -540,6 +601,12 @@ export function validateConfigFileV1(input) {
540
601
  hint: 'use llm.models with provider references',
541
602
  });
542
603
  }
604
+ const providerCapabilities = validateLlmCapabilities(rawProvider.capabilities, {
605
+ provider: id,
606
+ });
607
+ if (providerCapabilities) {
608
+ p.capabilities = providerCapabilities;
609
+ }
543
610
  cfg.llm.providers[id] = p;
544
611
  }
545
612
  }
@@ -569,12 +636,18 @@ export function validateConfigFileV1(input) {
569
636
  expected: 'non_empty_string',
570
637
  });
571
638
  }
639
+ const params = normalizeLlmModelParams(rawModel.params, {
640
+ model: slot,
641
+ location: 'params',
642
+ });
643
+ const capabilities = validateLlmCapabilities(rawModel.capabilities, {
644
+ model: slot,
645
+ });
572
646
  cfg.llm.models[slot] = {
573
647
  provider: provider,
574
648
  id: rawModel.id,
575
- params: isRecord(rawModel.params)
576
- ? rawModel.params
577
- : undefined,
649
+ params,
650
+ capabilities,
578
651
  };
579
652
  }
580
653
  }
@@ -38,6 +38,7 @@ export class MetadataGatherer {
38
38
  'tsconfig.json',
39
39
  'eslint.config.js',
40
40
  '.prettierrc',
41
+ '.oxfmtrc.json',
41
42
  'vitest.config.ts',
42
43
  'jest.config.js',
43
44
  'bun.lock',
@@ -1,8 +1,89 @@
1
+ import { FileAdapter } from '../../adapters/fs/file-adapter.js';
1
2
  import { LIMITS } from '../../config/limits.js';
2
3
  import { getLogger } from '../../observability/logger.js';
3
4
  import { spawnCommand } from '../../runtime/process-runner.js';
4
- import { normalizePath } from '../../utils/path.js';
5
+ import { ensureInSandbox, normalizePath, safeJoin, safeRelative } from '../../utils/path.js';
6
+ const FALLBACK_EXCLUDED_DIRS = new Set(['.git', 'node_modules']);
7
+ const FALLBACK_MAX_FILES = 2000;
8
+ const FALLBACK_MAX_FILE_BYTES = Math.max(LIMITS.largeFileThresholdBytes, 64 * 1024);
9
+ const fileAdapter = new FileAdapter();
10
+ function isHiddenPathSegment(name) {
11
+ return name.startsWith('.');
12
+ }
13
+ function isBinaryLike(content) {
14
+ return content.includes('\0');
15
+ }
5
16
  export class RipgrepGatherer {
17
+ async searchFileSystem(query, cwd, signal) {
18
+ const needle = query.toLowerCase();
19
+ if (!needle)
20
+ return [];
21
+ const results = [];
22
+ let scannedFiles = 0;
23
+ const pending = ['.'];
24
+ while (pending.length > 0 && scannedFiles < FALLBACK_MAX_FILES) {
25
+ if (signal?.aborted)
26
+ throw new Error('Operation cancelled by user');
27
+ const current = pending.shift();
28
+ const absoluteCurrent = ensureInSandbox(cwd, safeJoin(cwd, current));
29
+ let entries;
30
+ try {
31
+ entries = await fileAdapter.readdirWithTypes(absoluteCurrent);
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ entries.sort((a, b) => a.name.localeCompare(b.name));
37
+ for (const entry of entries) {
38
+ if (signal?.aborted)
39
+ throw new Error('Operation cancelled by user');
40
+ if (isHiddenPathSegment(entry.name))
41
+ continue;
42
+ if (entry.isSymbolicLink())
43
+ continue;
44
+ const relativePath = normalizePath(safeJoin(current, entry.name)).replace(/^(\.\/|\/)+/, '');
45
+ if (!relativePath)
46
+ continue;
47
+ if (entry.isDirectory()) {
48
+ if (FALLBACK_EXCLUDED_DIRS.has(entry.name))
49
+ continue;
50
+ pending.push(relativePath);
51
+ continue;
52
+ }
53
+ if (!entry.isFile())
54
+ continue;
55
+ scannedFiles += 1;
56
+ if (scannedFiles > FALLBACK_MAX_FILES)
57
+ break;
58
+ const absoluteFile = ensureInSandbox(cwd, safeJoin(cwd, relativePath));
59
+ try {
60
+ const stat = await fileAdapter.stat(absoluteFile);
61
+ if (!stat.isFile() || stat.size > FALLBACK_MAX_FILE_BYTES)
62
+ continue;
63
+ const content = await fileAdapter.readFile(absoluteFile, 'utf-8');
64
+ if (isBinaryLike(content))
65
+ continue;
66
+ const lines = content.split(/\r?\n/);
67
+ for (let index = 0; index < lines.length; index += 1) {
68
+ const line = lines[index] ?? '';
69
+ if (!line.toLowerCase().includes(needle))
70
+ continue;
71
+ results.push({
72
+ file: normalizePath(safeRelative(cwd, absoluteFile)),
73
+ line: index + 1,
74
+ content: line,
75
+ });
76
+ if (results.length >= LIMITS.defaultSearchMatches)
77
+ return results;
78
+ }
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ }
84
+ }
85
+ return results;
86
+ }
6
87
  async runRipgrep(query, cwd, signal) {
7
88
  getLogger().trace(` [RG] Searching for: "${query}" in ${cwd}`);
8
89
  if (signal?.aborted) {
@@ -38,7 +119,8 @@ export class RipgrepGatherer {
38
119
  getLogger().trace(` [RG] Process closed with code ${result.code}. Output length: ${output.length}`);
39
120
  if (result.error) {
40
121
  if (result.error.code === 'ENOENT') {
41
- getLogger().error('Error: ripgrep (rg) not found in PATH. Context gathering may be incomplete.');
122
+ getLogger().error('Error: ripgrep (rg) not found in PATH. Falling back to bounded filesystem search.');
123
+ return await this.searchFileSystem(query, cwd, signal);
42
124
  }
43
125
  else {
44
126
  getLogger().error(`Error running ripgrep: ${result.error.message}`);
@@ -85,7 +85,7 @@ function extractPathLikeTokens(input) {
85
85
  }
86
86
  function extractBacktickedTokens(input) {
87
87
  const matches = [];
88
- const re = /`([^`]{1,64})`/g;
88
+ const re = /(?<!`)`([^`\n]{1,64})`(?!`)/g;
89
89
  let m;
90
90
  while ((m = re.exec(input)) !== null) {
91
91
  const val = m[1]?.trim();
@@ -106,7 +106,7 @@ function extractErrorLikeTokens(input) {
106
106
  }
107
107
  function extractIdentifierTokens(input) {
108
108
  const matches = [];
109
- const re = /\b[A-Za-z_][A-Za-z0-9_]{2,}\b/g;
109
+ const re = /\b(?:[A-Z][A-Z0-9_]{1,}\d+[A-Z0-9_]*|[A-Za-z_][A-Za-z0-9_]{2,})\b/g;
110
110
  let m;
111
111
  while ((m = re.exec(input)) !== null) {
112
112
  if (m[0])
@@ -114,6 +114,17 @@ function extractIdentifierTokens(input) {
114
114
  }
115
115
  return matches;
116
116
  }
117
+ function extractQuotedPhrases(input) {
118
+ const matches = [];
119
+ const re = /["“”]([^"“”\n]{8,96})["“”]/g;
120
+ let m;
121
+ while ((m = re.exec(input)) !== null) {
122
+ const val = m[1]?.trim();
123
+ if (val)
124
+ matches.push(val);
125
+ }
126
+ return matches;
127
+ }
117
128
  function isCjk(char) {
118
129
  return /\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Hangul}/u.test(char);
119
130
  }
@@ -143,9 +154,11 @@ export function extractKeywords(instruction) {
143
154
  const backticked = extractBacktickedTokens(raw);
144
155
  // Priority 3: Error-like tokens (strong signal)
145
156
  const errorLike = extractErrorLikeTokens(raw);
146
- // Priority 4: Identifiers (code-related)
157
+ // Priority 4: Quoted diagnostics and messages (strong signal in bug reports)
158
+ const quotedPhrases = extractQuotedPhrases(raw);
159
+ // Priority 5: Identifiers (code-related)
147
160
  const identifiers = extractIdentifierTokens(raw);
148
- // Priority 5: Word tokens
161
+ // Priority 6: Word tokens
149
162
  const wordTokens = raw
150
163
  .toLowerCase()
151
164
  .split(/[^\p{L}\p{N}_-]+/u)
@@ -155,6 +168,7 @@ export function extractKeywords(instruction) {
155
168
  ...pathLike,
156
169
  ...backticked,
157
170
  ...errorLike,
171
+ ...quotedPhrases,
158
172
  ...identifiers,
159
173
  ...wordTokens,
160
174
  ]);
@@ -1,5 +1,5 @@
1
1
  import { DefaultPromptAssembler } from './assembly/default-prompt-assembler.js';
2
- import { PromptCachingManager } from './cache/prompt-caching.js';
2
+ import { getPromptCachingManager } from './cache/prompt-caching.js';
3
3
  import { ArchitectureGatherer } from './gatherers/architecture-gatherer.js';
4
4
  import { ArtifactGatherer } from './gatherers/artifact-gatherer.js';
5
5
  import { AstGatherer } from './gatherers/ast-gatherer.js';
@@ -26,7 +26,7 @@ export function defaultContextServiceDeps() {
26
26
  ghostDependencyGatherer: new GhostDependencyGatherer(ripgrepGatherer),
27
27
  targetResolver: new TargetResolver(),
28
28
  assembler: new DefaultPromptAssembler(),
29
- promptCachingManager: new PromptCachingManager(),
29
+ promptCachingManager: getPromptCachingManager(),
30
30
  };
31
31
  }
32
32
  //# sourceMappingURL=service-deps.js.map
@@ -147,6 +147,14 @@ export class ContextService {
147
147
  if (target.path)
148
148
  deduped.add(target.path);
149
149
  }
150
+ for (const file of result.context.relatedFiles ?? []) {
151
+ if (file.path)
152
+ deduped.add(file.path);
153
+ }
154
+ for (const snippet of result.context.rgSnippets ?? []) {
155
+ if (snippet.file)
156
+ deduped.add(snippet.file);
157
+ }
150
158
  return [...deduped].sort().slice(0, ContextService.MAX_CACHE_TRACKED_FILES);
151
159
  }
152
160
  async computeTrackedFilesSignature(repoPath, files) {
@@ -1,7 +1,27 @@
1
+ import { FileAdapter } from '../../adapters/fs/file-adapter.js';
2
+ import { LIMITS } from '../../config/limits.js';
3
+ import { ensureInSandbox, normalizePath, safeJoin } from '../../utils/path.js';
4
+ import { outlineSource } from '../ast/source-outline.js';
1
5
  import { CONTEXT_AUDIT_ACTION, CONTEXT_AUDIT_PHASE } from '../audit-constants.js';
2
6
  import { recordContextAuditEvent } from '../audit.js';
3
7
  import { extractKeywords } from '../keywords.js';
4
8
  import { assertNotAborted } from '../service-helpers.js';
9
+ const fileAdapter = new FileAdapter();
10
+ async function readMatchedFileContent(req, file) {
11
+ if (req.snapshotHash && req.checkpointManager) {
12
+ return req.checkpointManager.readSnapshotFile(req.repoPath, req.snapshotHash, file);
13
+ }
14
+ try {
15
+ const fullPath = ensureInSandbox(req.repoPath, safeJoin(req.repoPath, file));
16
+ const stat = await fileAdapter.stat(fullPath);
17
+ if (!stat.isFile() || stat.size > LIMITS.largeFileThresholdBytes)
18
+ return null;
19
+ return await fileAdapter.readFile(fullPath, 'utf-8');
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
5
25
  export function buildContextGatherStep(deps) {
6
26
  return async ({ req, diffScope, primaryText }) => {
7
27
  assertNotAborted(req.signal);
@@ -24,6 +44,24 @@ export function buildContextGatherStep(deps) {
24
44
  if (ghostFiles.length > 0) {
25
45
  astRes.relatedFiles.push(...ghostFiles);
26
46
  }
47
+ const relatedSeen = new Set(astRes.relatedFiles.map((file) => file.path));
48
+ const primaryPath = req.primaryFile
49
+ ? normalizePath(req.primaryFile).replace(/^(\.\/|\/)+/, '')
50
+ : undefined;
51
+ for (const snippet of rgSnippets) {
52
+ const file = normalizePath(snippet.file).replace(/^(\.\/|\/)+/, '');
53
+ if (!file || file === primaryPath || relatedSeen.has(file))
54
+ continue;
55
+ relatedSeen.add(file);
56
+ const content = await readMatchedFileContent(req, file);
57
+ astRes.relatedFiles.push({
58
+ path: file,
59
+ kind: 'dependency',
60
+ mode: content ? 'full' : 'outline',
61
+ content: content ?? `ripgrep match at line ${snippet.line}: ${snippet.content}`,
62
+ outline: content ? outlineSource(content) : undefined,
63
+ });
64
+ }
27
65
  recordContextAuditEvent(CONTEXT_AUDIT_ACTION.gatherCompleted, {
28
66
  rgSnippets: rgSnippets.length,
29
67
  includedFiles: diffRes.includedFiles.length,