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
@@ -6,6 +6,7 @@ export class KnowledgeGatherer {
6
6
  static KNOWLEDGE_SUBDIR = 'knowledge';
7
7
  static SNAPSHOT_FILE = 'snapshot.json';
8
8
  static COMPACTION_THRESHOLD = 20; // Compact after 20 events
9
+ static MAX_DECISIONS = 50; // Keep only the most recent N decisions
9
10
  fileAdapter = new FileAdapter();
10
11
  async gather(req) {
11
12
  const { repoPath } = req;
@@ -15,6 +16,7 @@ export class KnowledgeGatherer {
15
16
  project_rules: undefined,
16
17
  architectural_decisions: [],
17
18
  user_preferences: undefined,
19
+ lessons_learned: [],
18
20
  };
19
21
  const allDeprecated = new Set();
20
22
  try {
@@ -50,28 +52,44 @@ export class KnowledgeGatherer {
50
52
  data.deprecated_rules.forEach((r) => allDeprecated.add(r));
51
53
  }
52
54
  if (data.architectural_decisions) {
55
+ if (!Array.isArray(aggregated.architectural_decisions)) {
56
+ aggregated.architectural_decisions = [];
57
+ }
53
58
  aggregated.architectural_decisions.push(...data.architectural_decisions);
54
59
  }
55
60
  if (data.user_preferences) {
56
61
  aggregated.user_preferences = data.user_preferences;
57
62
  }
63
+ if (data.lessons_learned && Array.isArray(data.lessons_learned)) {
64
+ if (!Array.isArray(aggregated.lessons_learned)) {
65
+ aggregated.lessons_learned = [];
66
+ }
67
+ aggregated.lessons_learned.push(...data.lessons_learned);
68
+ }
58
69
  }
59
- catch {
70
+ catch (error) {
60
71
  // Skip corrupted files
72
+ getLogger().debug(`[KnowledgeGatherer] failed to load event file ${file}: ${error instanceof Error ? error.message : String(error)}`);
61
73
  }
62
74
  }
63
75
  // Filter out deprecated rules from aggregated project_rules
64
76
  if (aggregated.project_rules) {
65
77
  aggregated.project_rules = aggregated.project_rules.filter((r) => !allDeprecated.has(r));
66
78
  }
79
+ // Prune stale architectural decisions (keep only the most recent N)
80
+ if (aggregated.architectural_decisions &&
81
+ aggregated.architectural_decisions.length > KnowledgeGatherer.MAX_DECISIONS) {
82
+ aggregated.architectural_decisions = aggregated.architectural_decisions.slice(-KnowledgeGatherer.MAX_DECISIONS);
83
+ }
67
84
  // 3. Optional Compaction
68
85
  if (eventFiles.length >= KnowledgeGatherer.COMPACTION_THRESHOLD) {
69
86
  // Run compaction in background (non-blocking)
70
87
  this.compact(knowledgeDir, aggregated, eventFiles).catch((e) => getLogger().debug(`[KnowledgeGatherer] Compaction failed: ${e}`));
71
88
  }
72
89
  }
73
- catch {
90
+ catch (error) {
74
91
  // Directory missing or other read errors, return empty aggregated state
92
+ getLogger().debug(`[KnowledgeGatherer] knowledge directory read failed: ${error instanceof Error ? error.message : String(error)}`);
75
93
  }
76
94
  return {
77
95
  project_rules: aggregated.project_rules,
@@ -79,6 +97,7 @@ export class KnowledgeGatherer {
79
97
  ? aggregated.architectural_decisions
80
98
  : undefined,
81
99
  user_preferences: aggregated.user_preferences,
100
+ lessons_learned: aggregated.lessons_learned?.length ? aggregated.lessons_learned : undefined,
82
101
  };
83
102
  }
84
103
  async compact(knowledgeDir, aggregated, filesToMerge) {
@@ -1,4 +1,5 @@
1
1
  import { FileAdapter } from '../../adapters/fs/file-adapter.js';
2
+ import { getLogger } from '../../observability/logger.js';
2
3
  import { safeJoin } from '../../utils/path.js';
3
4
  export class MetadataGatherer {
4
5
  fileAdapter = new FileAdapter();
@@ -10,16 +11,18 @@ export class MetadataGatherer {
10
11
  const pkgRaw = await this.fileAdapter.readFile(safeJoin(repoPath, 'package.json'), 'utf-8');
11
12
  metadata.packageJson = JSON.parse(pkgRaw);
12
13
  }
13
- catch {
14
- // Ignored
14
+ catch (error) {
15
+ // Ignored - best-effort metadata
16
+ getLogger().debug(`[MetadataGatherer] package.json read failed: ${error instanceof Error ? error.message : String(error)}`);
15
17
  }
16
18
  // 2. README.md (first 1000 chars)
17
19
  try {
18
20
  const readmeRaw = await this.fileAdapter.readFile(safeJoin(repoPath, 'README.md'), 'utf-8');
19
21
  metadata.readmeHeader = readmeRaw.slice(0, 1000);
20
22
  }
21
- catch {
22
- // Ignored
23
+ catch (error) {
24
+ // Ignored - best-effort metadata
25
+ getLogger().debug(`[MetadataGatherer] README.md read failed: ${error instanceof Error ? error.message : String(error)}`);
23
26
  }
24
27
  // 3. AI Instructions (GEMINI.md, CLAUDE.md, ARCH.md)
25
28
  const aiFiles = ['GEMINI.md', 'CLAUDE.md', 'ARCH.md', '.gemini/ARCH.md'];
@@ -28,8 +31,9 @@ export class MetadataGatherer {
28
31
  const content = await this.fileAdapter.readFile(safeJoin(repoPath, file), 'utf-8');
29
32
  metadata.aiInstructions = (metadata.aiInstructions || '') + `\n--- ${file} ---\n${content}`;
30
33
  }
31
- catch {
32
- // Ignored
34
+ catch (error) {
35
+ // Ignored - best-effort metadata
36
+ getLogger().debug(`[MetadataGatherer] AI instruction file ${file} not found: ${error instanceof Error ? error.message : String(error)}`);
33
37
  }
34
38
  }
35
39
  // 4. List common config files
@@ -50,8 +54,9 @@ export class MetadataGatherer {
50
54
  await this.fileAdapter.readFile(safeJoin(repoPath, config), 'utf-8');
51
55
  metadata.configFiles.push(config);
52
56
  }
53
- catch {
57
+ catch (error) {
54
58
  // Ignored: config not found
59
+ getLogger().debug(`[MetadataGatherer] config file ${config} not found: ${error instanceof Error ? error.message : String(error)}`);
55
60
  }
56
61
  }
57
62
  return metadata;
@@ -30,7 +30,8 @@ export class RipgrepGatherer {
30
30
  try {
31
31
  entries = await fileAdapter.readdirWithTypes(absoluteCurrent);
32
32
  }
33
- catch {
33
+ catch (error) {
34
+ getLogger().debug(`[RipgrepGatherer] readdir failed for ${absoluteCurrent}: ${error instanceof Error ? error.message : String(error)}`);
34
35
  continue;
35
36
  }
36
37
  entries.sort((a, b) => a.name.localeCompare(b.name));
@@ -77,7 +78,8 @@ export class RipgrepGatherer {
77
78
  return results;
78
79
  }
79
80
  }
80
- catch {
81
+ catch (error) {
82
+ getLogger().debug(`[RipgrepGatherer] file read failed for ${absoluteFile}: ${error instanceof Error ? error.message : String(error)}`);
81
83
  continue;
82
84
  }
83
85
  }
@@ -149,8 +151,9 @@ export class RipgrepGatherer {
149
151
  });
150
152
  }
151
153
  }
152
- catch {
154
+ catch (error) {
153
155
  // Ignore malformed JSON.
156
+ getLogger().debug(`[RipgrepGatherer] JSON parse failed for ripgrep output line: ${error instanceof Error ? error.message : String(error)}`);
154
157
  }
155
158
  }
156
159
  return results;
@@ -119,6 +119,7 @@ export class ContextService {
119
119
  expectedTargetSetSignature;
120
120
  if (recordedTargetSetSignature !== expectedTargetSetSignature) {
121
121
  await this.cacheStore.delete(cacheKey);
122
+ this.deleteUpdater(cacheKey);
122
123
  this.cacheMetrics.misses += 1;
123
124
  return {
124
125
  missReason: 'target_signature_mismatch',
@@ -128,6 +129,7 @@ export class ContextService {
128
129
  const nextSignature = await this.computeTrackedFilesSignature(repoPath, entry.trackedFiles);
129
130
  if (nextSignature !== entry.signature) {
130
131
  await this.cacheStore.delete(cacheKey);
132
+ this.deleteUpdater(cacheKey);
131
133
  this.cacheMetrics.misses += 1;
132
134
  return { missReason: 'signature_mismatch', targetSetSignature: expectedTargetSetSignature };
133
135
  }
@@ -165,7 +167,8 @@ export class ContextService {
165
167
  const stat = await this.fileAdapter.stat(absoluteFile);
166
168
  parts.push(this.formatStatSignature(relativeFile, stat));
167
169
  }
168
- catch {
170
+ catch (error) {
171
+ getLogger().debug(`[ContextService] stat failed for ${relativeFile}: ${error instanceof Error ? error.message : String(error)}`);
169
172
  parts.push(`${relativeFile}:missing`);
170
173
  }
171
174
  }
@@ -184,7 +187,8 @@ export class ContextService {
184
187
  const stat = await this.fileAdapter.stat(gitPath);
185
188
  parts.push(this.formatStatSignature(rel, stat));
186
189
  }
187
- catch {
190
+ catch (error) {
191
+ getLogger().debug(`[ContextService] stat failed for ${rel}: ${error instanceof Error ? error.message : String(error)}`);
188
192
  parts.push(`${rel}:missing`);
189
193
  }
190
194
  }
@@ -210,6 +214,7 @@ export class ContextService {
210
214
  if (!last || Date.now() - last <= this.cacheTtlMs)
211
215
  return false;
212
216
  await this.cacheStore.delete(cacheKey);
217
+ this.deleteUpdater(cacheKey);
213
218
  this.cacheMetrics.evictions += 1;
214
219
  return true;
215
220
  }
@@ -243,6 +248,7 @@ export class ContextService {
243
248
  }
244
249
  if (victimKey) {
245
250
  await this.cacheStore.delete(victimKey);
251
+ this.deleteUpdater(victimKey);
246
252
  this.cacheMetrics.evictions += 1;
247
253
  }
248
254
  }
@@ -253,6 +259,7 @@ export class ContextService {
253
259
  const chunk = victims.slice(i, i + 10);
254
260
  await Promise.all(chunk.map(async ([key]) => {
255
261
  await this.cacheStore.delete(key);
262
+ this.deleteUpdater(key);
256
263
  this.cacheMetrics.evictions += 1;
257
264
  }));
258
265
  }
@@ -285,6 +292,9 @@ export class ContextService {
285
292
  }
286
293
  return updater;
287
294
  }
295
+ deleteUpdater(key) {
296
+ this.updaters.delete(key);
297
+ }
288
298
  logDiff(key, diff) {
289
299
  if (!diff.addedFiles.length && !diff.modifiedFiles.length && !diff.removedFiles.length) {
290
300
  return;
@@ -1,9 +1,12 @@
1
1
  import { FileAdapter } from '../../adapters/fs/file-adapter.js';
2
2
  import { LIMITS } from '../../config/limits.js';
3
+ import { getLogger } from '../../observability/logger.js';
3
4
  import { ensureInSandbox, normalizePath, safeJoin } from '../../utils/path.js';
4
- import { outlineSource } from '../ast/source-outline.js';
5
+ import { detectLang } from '../ast/skeleton-extractor.js';
6
+ import { outlineSourceAsync } from '../ast/source-outline.js';
5
7
  import { CONTEXT_AUDIT_ACTION, CONTEXT_AUDIT_PHASE } from '../audit-constants.js';
6
8
  import { recordContextAuditEvent } from '../audit.js';
9
+ import { getEffectivenessTracker } from '../effectiveness/tracker.js';
7
10
  import { extractKeywords } from '../keywords.js';
8
11
  import { assertNotAborted } from '../service-helpers.js';
9
12
  const fileAdapter = new FileAdapter();
@@ -18,7 +21,8 @@ async function readMatchedFileContent(req, file) {
18
21
  return null;
19
22
  return await fileAdapter.readFile(fullPath, 'utf-8');
20
23
  }
21
- catch {
24
+ catch (error) {
25
+ getLogger().debug(`[ContextGather] readMatchedFileContent failed for ${file}: ${error instanceof Error ? error.message : String(error)}`);
22
26
  return null;
23
27
  }
24
28
  }
@@ -59,7 +63,7 @@ export function buildContextGatherStep(deps) {
59
63
  kind: 'dependency',
60
64
  mode: content ? 'full' : 'outline',
61
65
  content: content ?? `ripgrep match at line ${snippet.line}: ${snippet.content}`,
62
- outline: content ? outlineSource(content) : undefined,
66
+ outline: content ? await outlineSourceAsync(content, detectLang(file)) : undefined,
63
67
  });
64
68
  }
65
69
  recordContextAuditEvent(CONTEXT_AUDIT_ACTION.gatherCompleted, {
@@ -75,6 +79,13 @@ export function buildContextGatherStep(deps) {
75
79
  hasKnowledgeBase: Boolean(knowledgeBase),
76
80
  hasRuntimeArtifacts: Boolean(runtimeArtifacts),
77
81
  }, { source: 'context', severity: 'low', scope: 'session', phase: CONTEXT_AUDIT_PHASE.gather });
82
+ // Record context usage for effectiveness tracking
83
+ const tracker = getEffectivenessTracker();
84
+ for (const file of astRes.relatedFiles) {
85
+ const tokens = file.content ? Math.ceil(file.content.length / 4) : 0;
86
+ const relevanceScore = file.mode === 'full' ? 80 : 40;
87
+ tracker.recordUsage(file.path, false, tokens, relevanceScore);
88
+ }
78
89
  return {
79
90
  req,
80
91
  diffScope,
@@ -20,6 +20,7 @@ export function buildContextTargetsStep(deps) {
20
20
  definitionMap: ast.definitionMap,
21
21
  symbolMap: ast.symbolMap,
22
22
  churnByFile: gitHistory?.churnByFile,
23
+ contextFiles: req.contextFiles,
23
24
  });
24
25
  assertNotAborted(req.signal);
25
26
  recordContextAuditEvent(CONTEXT_AUDIT_ACTION.targetsResolved, {
@@ -7,6 +7,8 @@ function reasonRank(reason) {
7
7
  switch (reason) {
8
8
  case 'explicit_path':
9
9
  return 100;
10
+ case 'context_file':
11
+ return 95;
10
12
  case 'symbol_definition':
11
13
  return 90;
12
14
  case 'diff_included':
@@ -140,6 +142,19 @@ function buildExplicitTargets(req) {
140
142
  evidence: 'instruction_path',
141
143
  })));
142
144
  }
145
+ function buildContextFileTargets(contextFiles) {
146
+ if (!contextFiles || contextFiles.length === 0)
147
+ return [];
148
+ return dedupeTargets(contextFiles
149
+ .map((f) => normalizePath(f).replace(/^(\.\/|\/)+/, ''))
150
+ .filter(Boolean)
151
+ .map((path) => ({
152
+ path,
153
+ reason: 'context_file',
154
+ confidence: 'high',
155
+ evidence: 'context_files_option',
156
+ })));
157
+ }
143
158
  function buildDiffTargets(includedFiles) {
144
159
  if (!includedFiles || includedFiles.length === 0)
145
160
  return [];
@@ -382,7 +397,7 @@ export class TargetResolver {
382
397
  };
383
398
  }
384
399
  async resolve(params) {
385
- const { req, includedFiles, importRelatedFiles, rgHitFiles, definitionMap, symbolMap, diffusionDepth, maxDiffusionTargets, churnByFile, } = params;
400
+ const { req, includedFiles, importRelatedFiles, rgHitFiles, definitionMap, symbolMap, diffusionDepth, maxDiffusionTargets, churnByFile, contextFiles, } = params;
386
401
  const runner = new MicroTaskRunner({
387
402
  debugLabel: 'context-targeting',
388
403
  maxRounds: 5,
@@ -390,7 +405,8 @@ export class TargetResolver {
390
405
  if (key === 'explicitTargets') {
391
406
  const primary = buildPrimaryTarget(ctx.primaryFile);
392
407
  const explicit = buildExplicitTargets(req);
393
- return dedupeTargets([...primary, ...explicit]);
408
+ const contextFileTargets = buildContextFileTargets(contextFiles);
409
+ return dedupeTargets([...primary, ...explicit, ...contextFileTargets]);
394
410
  }
395
411
  if (key === 'diffTargets') {
396
412
  const primary = buildPrimaryTarget(ctx.primaryFile);
@@ -433,34 +449,36 @@ export class TargetResolver {
433
449
  return [];
434
450
  },
435
451
  strategy: (engine) => {
452
+ const hasExplicitSignal = (data, key) => (data?.[key] || []).some((t) => t.reason === 'explicit_path' || t.reason === 'context_file');
453
+ const hasSymbolTargets = (data) => (data?.symbolTargets || []).some((t) => t.reason === 'symbol_definition');
454
+ const hasDiffTargets = (data) => (data?.diffTargets || []).some((t) => t.reason === 'diff_included');
436
455
  return engine
437
456
  .phase('Dependencies')
438
457
  .requireData(['explicitTargets', 'symbolTargets', 'diffTargets', 'defaultTargets'])
439
458
  .phase('Selection')
440
- .when((c) => (c.data?.explicitTargets || []).some((t) => t.reason === 'explicit_path'), (p) => {
459
+ .when((c) => hasExplicitSignal(c.data, 'explicitTargets'), (p) => {
441
460
  p.addAction('SET_TARGETS', {
442
461
  strategy: 'explicit',
443
462
  targets: engine.ctx.data.explicitTargets,
444
463
  });
445
464
  })
446
- .when((c) => !(c.data?.explicitTargets || []).some((t) => t.reason === 'explicit_path') &&
447
- (c.data?.symbolTargets || []).some((t) => t.reason === 'symbol_definition'), (p) => {
465
+ .when((c) => !hasExplicitSignal(c.data, 'explicitTargets') && hasSymbolTargets(c.data), (p) => {
448
466
  p.addAction('SET_TARGETS', {
449
467
  strategy: 'symbol',
450
468
  targets: engine.ctx.data.symbolTargets,
451
469
  });
452
470
  })
453
- .when((c) => !(c.data?.explicitTargets || []).some((t) => t.reason === 'explicit_path') &&
454
- !(c.data?.symbolTargets || []).some((t) => t.reason === 'symbol_definition') &&
455
- (c.data?.diffTargets || []).some((t) => t.reason === 'diff_included'), (p) => {
471
+ .when((c) => !hasExplicitSignal(c.data, 'explicitTargets') &&
472
+ !hasSymbolTargets(c.data) &&
473
+ hasDiffTargets(c.data), (p) => {
456
474
  p.addAction('SET_TARGETS', {
457
475
  strategy: 'diff',
458
476
  targets: engine.ctx.data.diffTargets,
459
477
  });
460
478
  })
461
- .unless((c) => (c.data?.explicitTargets || []).some((t) => t.reason === 'explicit_path') ||
462
- (c.data?.symbolTargets || []).some((t) => t.reason === 'symbol_definition') ||
463
- (c.data?.diffTargets || []).some((t) => t.reason === 'diff_included'), (p) => {
479
+ .unless((c) => hasExplicitSignal(c.data, 'explicitTargets') ||
480
+ hasSymbolTargets(c.data) ||
481
+ hasDiffTargets(c.data), (p) => {
464
482
  p.addAction('SET_TARGETS', {
465
483
  strategy: 'default',
466
484
  targets: engine.ctx.data.defaultTargets,
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { createHash } from 'crypto';
8
8
  import { FileAdapter } from '../../adapters/fs/file-adapter.js';
9
+ import { getLogger } from '../../observability/logger.js';
9
10
  /**
10
11
  * Two-level token cache for performance optimization.
11
12
  *
@@ -82,8 +83,9 @@ export class TokenCache {
82
83
  this.fileCache.set(filePath, entry);
83
84
  return entry;
84
85
  }
85
- catch {
86
+ catch (error) {
86
87
  // File doesn't exist or can't be accessed
88
+ getLogger().debug(`[TokenCache] file cache get failed for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
87
89
  this.fileCache.delete(filePath);
88
90
  this.misses++;
89
91
  return null;
@@ -108,8 +110,9 @@ export class TokenCache {
108
110
  contentHash: this.hashContent(content),
109
111
  });
110
112
  }
111
- catch {
113
+ catch (error) {
112
114
  // Ignore if file can't be accessed
115
+ getLogger().debug(`[TokenCache] file cache set failed for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
113
116
  }
114
117
  }
115
118
  // ==================== Invalidation ====================
@@ -24,13 +24,13 @@ class TiktokenEncoding {
24
24
  this.initialized = true;
25
25
  }
26
26
  encode(text) {
27
- this.ensureInitialized();
28
- return Array.from(this.encoder.encode(text));
27
+ const encoder = this.getEncoder();
28
+ return Array.from(encoder.encode(text));
29
29
  }
30
30
  decode(tokens) {
31
- this.ensureInitialized();
31
+ const encoder = this.getEncoder();
32
32
  const uint32Tokens = new Uint32Array(tokens);
33
- const decoded = this.encoder.decode(uint32Tokens);
33
+ const decoded = encoder.decode(uint32Tokens);
34
34
  return new TextDecoder().decode(decoded);
35
35
  }
36
36
  count(text) {
@@ -43,10 +43,11 @@ class TiktokenEncoding {
43
43
  }
44
44
  this.initialized = false;
45
45
  }
46
- ensureInitialized() {
47
- if (!this.initialized || !this.encoder) {
46
+ getEncoder() {
47
+ if (!this.encoder) {
48
48
  throw new Error(`Encoding ${this.name} not initialized. Call initialize() first.`);
49
49
  }
50
+ return this.encoder;
50
51
  }
51
52
  }
52
53
  /**
@@ -4,6 +4,7 @@
4
4
  * Preserves JSON structure while truncating large arrays/objects.
5
5
  * Keeps root structure and key names visible.
6
6
  */
7
+ import { getLogger } from '../../../observability/logger.js';
7
8
  import { DEFAULT_TRUNCATION_CONFIG } from '../types.js';
8
9
  /**
9
10
  * JSON truncation strategy.
@@ -21,7 +22,8 @@ export class JsonStrategy {
21
22
  JSON.parse(output);
22
23
  return true;
23
24
  }
24
- catch {
25
+ catch (error) {
26
+ getLogger().debug(`[JsonStrategy] canHandle parse check failed: ${error instanceof Error ? error.message : String(error)}`);
25
27
  return false;
26
28
  }
27
29
  }
@@ -40,8 +42,9 @@ export class JsonStrategy {
40
42
  try {
41
43
  parsed = JSON.parse(output);
42
44
  }
43
- catch {
45
+ catch (error) {
44
46
  // Not valid JSON, fall back to simple truncation
47
+ getLogger().debug(`[JsonStrategy] JSON parse failed during truncation: ${error instanceof Error ? error.message : String(error)}`);
45
48
  return this.simpleTruncate(output, budget);
46
49
  }
47
50
  // Truncate while preserving structure
@@ -4,6 +4,7 @@
4
4
  * Analyzes output content to determine the most appropriate
5
5
  * truncation strategy.
6
6
  */
7
+ import { getLogger } from '../../observability/logger.js';
7
8
  /**
8
9
  * Detection patterns for each output type.
9
10
  */
@@ -84,8 +85,9 @@ export function detectOutputType(output) {
84
85
  try {
85
86
  JSON.parse(output);
86
87
  }
87
- catch {
88
+ catch (error) {
88
89
  // Not valid JSON, downgrade to generic
90
+ getLogger().debug(`[TypeDetector] JSON validation failed: ${error instanceof Error ? error.message : String(error)}`);
89
91
  bestType = 'generic';
90
92
  bestScore = 0;
91
93
  }
@@ -3,9 +3,9 @@ import { buildResolvedMcpServersV2 } from '../mcp/config/index.js';
3
3
  import { getLogger } from '../observability/logger.js';
4
4
  import { loadConfig } from './load.js';
5
5
  import { mergeScopedEntries } from './merge.js';
6
- import { expandHome, getRepoMcpConfigPath, getRepoSkillConfigPath, getRepoToolConfigPath, getUserMcpConfigPath, getUserSkillConfigPath, getUserToolConfigPath, isWithinRoot, resolveRepoRelative, resolveUserRelative, } from './paths.js';
6
+ import { expandHome, getRepoAgentsConfigPath, getRepoMcpConfigPath, getRepoSkillConfigPath, getRepoToolConfigPath, getUserAgentsConfigPath, getUserMcpConfigPath, getUserSkillConfigPath, getUserToolConfigPath, isWithinRoot, resolveRepoRelative, resolveUserRelative, } from './paths.js';
7
7
  import { redactExtensions } from './redact.js';
8
- import { McpConfigSchema, SkillsConfigSchema, ToolsConfigSchema } from './schemas.js';
8
+ import { AgentsConfigSchema, McpConfigSchema, SkillsConfigSchema, ToolsConfigSchema, } from './schemas.js';
9
9
  function defaultEnabled(scope) {
10
10
  return scope === 'repo';
11
11
  }
@@ -29,6 +29,47 @@ function buildResolvedPlugins(entries, repoRoot) {
29
29
  };
30
30
  });
31
31
  }
32
+ function buildResolvedAgentProfiles(user, repo) {
33
+ const seen = new Map();
34
+ // User profiles first (lower priority)
35
+ if (user) {
36
+ for (const agent of user.agents) {
37
+ if (agent.enabled === false)
38
+ continue;
39
+ seen.set(agent.id, toResolvedProfile(agent, 'user'));
40
+ }
41
+ }
42
+ // Repo profiles override user profiles (higher priority)
43
+ if (repo) {
44
+ for (const agent of repo.agents) {
45
+ if (agent.enabled === false) {
46
+ seen.delete(agent.id);
47
+ continue;
48
+ }
49
+ seen.set(agent.id, toResolvedProfile(agent, 'repo'));
50
+ }
51
+ }
52
+ return Array.from(seen.values());
53
+ }
54
+ function toResolvedProfile(raw, scope) {
55
+ return {
56
+ id: raw.id,
57
+ name: raw.name,
58
+ role: raw.role,
59
+ description: raw.description,
60
+ allowedTools: raw.allowedTools ?? ['code.search', 'fs.read'],
61
+ readOnly: raw.readOnly ?? false,
62
+ stratagem: raw.stratagem ?? 'investigator',
63
+ toolInheritance: raw.toolInheritance,
64
+ permissionMode: raw.permissionMode,
65
+ systemPrompt: raw.systemPrompt,
66
+ maxTokens: raw.maxTokens,
67
+ maxAttempts: raw.maxAttempts,
68
+ timeoutMs: raw.timeoutMs,
69
+ model: raw.model,
70
+ scope,
71
+ };
72
+ }
32
73
  function buildResolvedSkills(user, repo, repoRoot) {
33
74
  const repoDiscovery = repo?.discovery;
34
75
  const userDiscovery = user?.discovery;
@@ -73,13 +114,15 @@ function buildResolvedSkills(user, repo, repoRoot) {
73
114
  }
74
115
  export async function resolveExtensions(options) {
75
116
  const { repoRoot } = options;
76
- const [userMcp, repoMcp, userTools, repoTools, userSkills, repoSkills] = await Promise.all([
117
+ const [userMcp, repoMcp, userTools, repoTools, userSkills, repoSkills, userAgents, repoAgents] = await Promise.all([
77
118
  loadConfig(getUserMcpConfigPath(), McpConfigSchema),
78
119
  loadConfig(getRepoMcpConfigPath(repoRoot), McpConfigSchema),
79
120
  loadConfig(getUserToolConfigPath(), ToolsConfigSchema),
80
121
  loadConfig(getRepoToolConfigPath(repoRoot), ToolsConfigSchema),
81
122
  loadConfig(getUserSkillConfigPath(), SkillsConfigSchema),
82
123
  loadConfig(getRepoSkillConfigPath(repoRoot), SkillsConfigSchema),
124
+ loadConfig(getUserAgentsConfigPath(), AgentsConfigSchema),
125
+ loadConfig(getRepoAgentsConfigPath(repoRoot), AgentsConfigSchema),
83
126
  ]);
84
127
  const mergedServers = mergeScopedEntries(userMcp?.config.servers, repoMcp?.config.servers);
85
128
  const mergedPlugins = mergeScopedEntries(userTools?.config.plugins, repoTools?.config.plugins);
@@ -87,11 +130,13 @@ export async function resolveExtensions(options) {
87
130
  mcpServers: buildResolvedMcpServersV2(mergedServers, repoRoot),
88
131
  toolPlugins: buildResolvedPlugins(mergedPlugins, repoRoot),
89
132
  skillDiscovery: buildResolvedSkills(userSkills?.config, repoSkills?.config, repoRoot),
133
+ agentProfiles: buildResolvedAgentProfiles(userAgents?.config, repoAgents?.config),
90
134
  };
91
135
  const rawEffective = {
92
136
  mcp: repoMcp?.config ?? userMcp?.config ?? null,
93
137
  tools: repoTools?.config ?? userTools?.config ?? null,
94
138
  skills: repoSkills?.config ?? userSkills?.config ?? null,
139
+ agents: repoAgents?.config ?? userAgents?.config ?? null,
95
140
  };
96
141
  return {
97
142
  resolved,
@@ -1,4 +1,5 @@
1
1
  import { syncFs as fs } from '../adapters/fs/node-fs.js';
2
+ import { errorMessage } from '../utils/error.js';
2
3
  export class ExtensionConfigError extends Error {
3
4
  path;
4
5
  constructor(path, message) {
@@ -18,7 +19,7 @@ export async function tryLoadJsonFile(path) {
18
19
  : undefined) === 'ENOENT') {
19
20
  return { exists: false };
20
21
  }
21
- throw new ExtensionConfigError(path, (error instanceof Error ? error.message : String(error)) || 'Unable to read file');
22
+ throw new ExtensionConfigError(path, errorMessage(error) || 'Unable to read file');
22
23
  }
23
24
  }
24
25
  export async function loadConfig(path, schema) {
@@ -30,7 +31,7 @@ export async function loadConfig(path, schema) {
30
31
  return { path, config };
31
32
  }
32
33
  catch (error) {
33
- throw new ExtensionConfigError(path, (error instanceof Error ? error.message : String(error)) || 'Schema validation failed');
34
+ throw new ExtensionConfigError(path, errorMessage(error) || 'Schema validation failed');
34
35
  }
35
36
  }
36
37
  //# sourceMappingURL=load.js.map
@@ -11,7 +11,10 @@ export function mergeScopedEntries(user, repo) {
11
11
  if (previous) {
12
12
  merged.set(key, {
13
13
  key,
14
- entry: { ...previous.entry, ...entry },
14
+ entry: {
15
+ ...previous.entry,
16
+ ...entry,
17
+ },
15
18
  scope: 'repo',
16
19
  });
17
20
  }
@@ -38,6 +41,7 @@ export function mergeResolvedExtensions(base, overlay) {
38
41
  : base.skillDiscovery.scope,
39
42
  paths: [...base.skillDiscovery.paths, ...overlay.skillDiscovery.paths],
40
43
  },
44
+ agentProfiles: [...base.agentProfiles, ...overlay.agentProfiles],
41
45
  };
42
46
  }
43
47
  //# sourceMappingURL=merge.js.map
@@ -39,6 +39,12 @@ export function getUserToolConfigPath() {
39
39
  export function getUserSkillConfigPath() {
40
40
  return path.join(USER_CONFIG_DIR, 'skills-user.json');
41
41
  }
42
+ export function getRepoAgentsConfigPath(repoRoot) {
43
+ return path.join(repoRoot, REPO_CONFIG_DIR, 'agents.json');
44
+ }
45
+ export function getUserAgentsConfigPath() {
46
+ return path.join(USER_CONFIG_DIR, 'agents-user.json');
47
+ }
42
48
  /**
43
49
  * Check whether a candidate path resides within (or equals) a given root directory.
44
50
  *
@@ -60,11 +66,11 @@ export function isWithinRoot(candidate, root) {
60
66
  const realRoot = realpathSync(resolvedRoot);
61
67
  return realCandidate === realRoot || realCandidate.startsWith(realRoot + path.sep);
62
68
  }
63
- catch {
69
+ catch (error) {
64
70
  // Candidate or root does not exist yet — fall back to lexical check.
65
71
  // This allows pre-declaring paths that will be created later, while
66
72
  // still catching obvious traversal sequences like `../../etc`.
67
- tryGetLogger()?.debug(`isWithinRoot: path not on disk, using lexical check for "${candidate}" against root "${root}"`);
73
+ tryGetLogger()?.debug(`[Paths] isWithinRoot: path not on disk, using lexical check for "${candidate}" against root "${root}": ${error instanceof Error ? error.message : String(error)}`);
68
74
  return (resolvedCandidate === resolvedRoot || resolvedCandidate.startsWith(resolvedRoot + path.sep));
69
75
  }
70
76
  }