salmon-loop 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/cli/authorization/provider.js +2 -10
  2. package/dist/cli/commands/config.js +2 -2
  3. package/dist/cli/commands/mode.js +2 -2
  4. package/dist/cli/commands/run/handler.js +3 -1
  5. package/dist/cli/commands/run/loop-params.js +1 -0
  6. package/dist/cli/commands/run/runtime-options.js +3 -1
  7. package/dist/cli/config.js +0 -8
  8. package/dist/cli/locales/en.js +2 -2
  9. package/dist/cli/reporters/standard.js +10 -0
  10. package/dist/core/adapters/fs/file-adapter.js +3 -1
  11. package/dist/core/adapters/git/git-adapter.js +6 -3
  12. package/dist/core/adapters/git/git-runner.js +5 -2
  13. package/dist/core/adapters/git/lock-manager.js +7 -4
  14. package/dist/core/checkpoint-domain/manifest-store.js +21 -13
  15. package/dist/core/checkpoint-domain/service.js +3 -1
  16. package/dist/core/config/limits.js +1 -1
  17. package/dist/core/config/model-pricing.js +61 -0
  18. package/dist/core/context/ast/skeleton-extractor.js +225 -0
  19. package/dist/core/context/ast/source-outline.js +24 -1
  20. package/dist/core/context/budget/dynamic-adjuster.js +20 -5
  21. package/dist/core/context/builder.js +7 -3
  22. package/dist/core/context/cache/store-factory.js +3 -1
  23. package/dist/core/context/dependencies.js +2 -1
  24. package/dist/core/context/effectiveness/persistence.js +50 -0
  25. package/dist/core/context/effectiveness/tracker.js +24 -0
  26. package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
  27. package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
  28. package/dist/core/context/gatherers/ast-gatherer.js +30 -28
  29. package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
  30. package/dist/core/context/gatherers/knowledge-gatherer.js +18 -2
  31. package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
  32. package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
  33. package/dist/core/context/service.js +4 -2
  34. package/dist/core/context/steps/context-gather.js +14 -3
  35. package/dist/core/context/steps/context-targets.js +1 -0
  36. package/dist/core/context/targeting/target-resolver.js +29 -11
  37. package/dist/core/context/token/cache.js +5 -2
  38. package/dist/core/context/truncation/strategies/json.js +5 -2
  39. package/dist/core/context/truncation/type-detector.js +3 -1
  40. package/dist/core/extensions/paths.js +2 -2
  41. package/dist/core/facades/cli-authorization-provider.js +1 -0
  42. package/dist/core/feedback/parsers.js +290 -1
  43. package/dist/core/grizzco/dsl/llm-strategy.js +1 -1
  44. package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
  45. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -3
  46. package/dist/core/grizzco/engine/transaction/attempt-failure.js +44 -20
  47. package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
  48. package/dist/core/grizzco/execution/RejectionManager.js +7 -5
  49. package/dist/core/grizzco/runtime/apply-back-runtime.js +3 -1
  50. package/dist/core/grizzco/services/implementations/default/GitConfigService.js +2 -1
  51. package/dist/core/grizzco/steps/autopilot.js +21 -32
  52. package/dist/core/grizzco/steps/explore.js +5 -2
  53. package/dist/core/grizzco/steps/generateReview.js +3 -1
  54. package/dist/core/grizzco/steps/research.js +3 -1
  55. package/dist/core/grizzco/steps/verify.js +7 -1
  56. package/dist/core/grizzco/validation/AstValidationService.js +3 -1
  57. package/dist/core/history/input-history.js +3 -1
  58. package/dist/core/intent/chat-intent.js +3 -1
  59. package/dist/core/llm/ai-sdk/message-mapper.js +13 -8
  60. package/dist/core/llm/ai-sdk/request-params.js +1 -3
  61. package/dist/core/llm/ai-sdk/retry-classifier.js +12 -4
  62. package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
  63. package/dist/core/llm/errors.js +5 -4
  64. package/dist/core/llm/retry-utils.js +8 -2
  65. package/dist/core/llm/stream-utils.js +5 -3
  66. package/dist/core/llm/sub-agent-factory.js +3 -0
  67. package/dist/core/mcp/bridge/resource-context-provider.js +3 -1
  68. package/dist/core/mcp/catalog/discovery.js +3 -1
  69. package/dist/core/mcp/client/connection-manager.js +4 -2
  70. package/dist/core/mcp/client/transport-factory.js +7 -3
  71. package/dist/core/observability/audit-file.js +2 -1
  72. package/dist/core/observability/audit-trail.js +3 -1
  73. package/dist/core/observability/logger.js +2 -1
  74. package/dist/core/observability/monitor.js +24 -0
  75. package/dist/core/observability/run-outcome-reporter.js +1 -0
  76. package/dist/core/permission-gate/default-gate.js +5 -8
  77. package/dist/core/plan/storage.js +7 -4
  78. package/dist/core/plugin/loader.js +3 -1
  79. package/dist/core/prompts/registry.js +1 -1
  80. package/dist/core/prompts/runtime.js +3 -1
  81. package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
  82. package/dist/core/protocols/a2a/sdk/executor.js +3 -1
  83. package/dist/core/protocols/a2a/sdk/server.js +3 -1
  84. package/dist/core/protocols/acp/acp-command-runner.js +7 -6
  85. package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
  86. package/dist/core/protocols/acp/formal-agent.js +3 -2
  87. package/dist/core/protocols/acp/permission-provider.js +3 -2
  88. package/dist/core/reflection/engine.js +114 -14
  89. package/dist/core/runtime/batch-runner.js +81 -0
  90. package/dist/core/runtime/initialize.js +2 -1
  91. package/dist/core/runtime/loop-finalize.js +3 -0
  92. package/dist/core/runtime/loop-session-runner.js +5 -0
  93. package/dist/core/runtime/loop.js +4 -0
  94. package/dist/core/runtime/paths.js +9 -6
  95. package/dist/core/runtime/spawn-interactive.js +5 -4
  96. package/dist/core/security/redaction.js +3 -2
  97. package/dist/core/session/compression.js +3 -1
  98. package/dist/core/session/manager.js +2 -1
  99. package/dist/core/session/pruning-strategy.js +2 -1
  100. package/dist/core/session/token-tracker.js +11 -4
  101. package/dist/core/skills/permissions.js +2 -2
  102. package/dist/core/strata/checkpoint/manager.js +16 -10
  103. package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
  104. package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
  105. package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
  106. package/dist/core/strata/interaction/file-system-provider.js +2 -1
  107. package/dist/core/strata/layers/file-state-resolver.js +9 -7
  108. package/dist/core/strata/layers/immutable-git-layer.js +3 -1
  109. package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
  110. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
  111. package/dist/core/strata/layers/worktree.js +2 -1
  112. package/dist/core/strata/runtime/environment.js +2 -1
  113. package/dist/core/strata/runtime/synchronizer.js +18 -17
  114. package/dist/core/structured-output/json-extract.js +3 -1
  115. package/dist/core/sub-agent/artifacts/store.js +2 -1
  116. package/dist/core/sub-agent/core/manager.js +24 -1
  117. package/dist/core/sub-agent/registry-defaults.js +2 -2
  118. package/dist/core/sub-agent/summary.js +96 -0
  119. package/dist/core/sub-agent/tools/task-spawn.js +7 -4
  120. package/dist/core/target-runtime/profile.js +3 -1
  121. package/dist/core/tools/audit.js +3 -2
  122. package/dist/core/tools/budget.js +3 -1
  123. package/dist/core/tools/builtin/ast.js +144 -0
  124. package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
  125. package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
  126. package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
  127. package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
  128. package/dist/core/tools/builtin/fs.js +76 -1
  129. package/dist/core/tools/builtin/git.js +242 -0
  130. package/dist/core/tools/builtin/glob.js +79 -0
  131. package/dist/core/tools/builtin/index.js +12 -4
  132. package/dist/core/tools/builtin/knowledge.js +146 -4
  133. package/dist/core/tools/builtin/proposal.js +3 -1
  134. package/dist/core/tools/builtin/verify.js +35 -3
  135. package/dist/core/tools/permissions/permission-rules.js +3 -1
  136. package/dist/core/tools/router.js +88 -5
  137. package/dist/core/tools/session.js +10 -5
  138. package/dist/core/types/batch.js +2 -0
  139. package/dist/core/utils/sanitizer.js +5 -2
  140. package/dist/core/utils/serialize.js +5 -2
  141. package/dist/core/verification/detect-runner.js +86 -0
  142. package/dist/core/verification/runner.js +76 -0
  143. package/dist/core/version.js +3 -1
  144. package/dist/languages/python/index.js +154 -0
  145. package/dist/locales/en.js +6 -0
  146. package/package.json +2 -1
@@ -4,12 +4,13 @@ import { agentTeamSpec } from '../../sub-agent/tools/team.js';
4
4
  import { defineTool } from '../types.js';
5
5
  import { artifactReadSpec, executeArtifactRead } from './artifact.js';
6
6
  import { astGrepSpec, executeAstGrep } from './ast-grep.js';
7
- import { astDefsRefsSpec, executeAstDefsRefs } from './ast.js';
7
+ import { astDefsRefsSpec, executeAstDefsRefs, codeFindReferencesSpec, executeCodeFindReferences, } from './ast.js';
8
8
  import { benchmarkReportSpec, executeBenchmarkReport, executeGitApplyCheck, executeGitDiffCheck, executeSweBenchGetReport, executeSweBenchLoadInstance, executeSweBenchSubmitPredictions, executeSweBenchWritePrediction, gitApplyCheckSpec, gitDiffCheckSpec, sweBenchGetReportSpec, sweBenchLoadInstanceSpec, sweBenchSubmitPredictionsSpec, sweBenchWritePredictionSpec, } from './benchmark.js';
9
9
  import { codeSearchExecutor } from './code-search/executor.js';
10
10
  import { CodeSearchSpec } from './code-search/spec.js';
11
- import { codeReadSpec, executeFsCreateDirectory, executeFsList, executeFsListDirectory, executeFsListFiles, executeFsReadFile, executeFsDeleteFile, executeFsWriteFile, fsCreateDirectorySpec, fsDeleteFileSpec, fsListSpec, fsListDirectorySpec, fsListFilesSpec, fsReadFileSpec, fsWriteFileSpec, } from './fs.js';
12
- import { gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus } from './git.js';
11
+ import { codeReadSpec, executeFsCreateDirectory, executeFsEditFile, executeFsList, executeFsListDirectory, executeFsListFiles, executeFsReadFile, executeFsDeleteFile, executeFsWriteFile, fsCreateDirectorySpec, fsDeleteFileSpec, fsEditFileSpec, fsListSpec, fsListDirectorySpec, fsListFilesSpec, fsReadFileSpec, fsWriteFileSpec, } from './fs.js';
12
+ import { gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus, gitBlameSpec, executeGitBlame, gitLogSpec, executeGitLog, gitShowSpec, executeGitShow, } from './git.js';
13
+ import { globFindSpec, executeGlobFind } from './glob.js';
13
14
  import { askUserSpec } from './interaction.js';
14
15
  import { updateKnowledgeSpec, executeUpdateKnowledge } from './knowledge.js';
15
16
  import { planInitSpec, planReadSpec, planUpdateSpec } from './plan.js';
@@ -34,12 +35,18 @@ export function registerAllBuiltins(registry) {
34
35
  // Code search & AST
35
36
  registry.register(defineTool(CodeSearchSpec, codeSearchExecutor));
36
37
  registry.register(defineTool(astDefsRefsSpec, executeAstDefsRefs));
38
+ registry.register(defineTool(codeFindReferencesSpec, executeCodeFindReferences));
37
39
  registry.register(defineTool(astGrepSpec, executeAstGrep));
38
40
  // Git
39
41
  registry.register(defineTool(gitCatSpec, executeGitCat));
40
42
  registry.register(defineTool(gitStatusSpec, executeGitStatus));
43
+ registry.register(defineTool(gitBlameSpec, executeGitBlame));
44
+ registry.register(defineTool(gitLogSpec, executeGitLog));
45
+ registry.register(defineTool(gitShowSpec, executeGitShow));
41
46
  registry.register(defineTool(gitDiffCheckSpec, executeGitDiffCheck));
42
47
  registry.register(defineTool(gitApplyCheckSpec, executeGitApplyCheck));
48
+ // Glob
49
+ registry.register(defineTool(globFindSpec, executeGlobFind));
43
50
  // Benchmark / SWE-bench
44
51
  registry.register(defineTool(benchmarkReportSpec, executeBenchmarkReport));
45
52
  registry.register(defineTool(sweBenchLoadInstanceSpec, executeSweBenchLoadInstance));
@@ -53,6 +60,7 @@ export function registerAllBuiltins(registry) {
53
60
  registry.register(defineTool(fsListDirectorySpec, executeFsListDirectory));
54
61
  registry.register(defineTool(fsListFilesSpec, executeFsListFiles));
55
62
  registry.register(defineTool(fsWriteFileSpec, executeFsWriteFile));
63
+ registry.register(defineTool(fsEditFileSpec, executeFsEditFile));
56
64
  registry.register(defineTool(fsCreateDirectorySpec, executeFsCreateDirectory));
57
65
  registry.register(defineTool(fsDeleteFileSpec, executeFsDeleteFile));
58
66
  // Execution
@@ -64,5 +72,5 @@ export function registerAllBuiltins(registry) {
64
72
  registry.register(planUpdateSpec);
65
73
  registry.register(askUserSpec);
66
74
  }
67
- export { CodeSearchSpec, codeSearchExecutor, astDefsRefsSpec as codeAstSpec, executeAstDefsRefs as executeCodeAst, gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus, codeReadSpec, fsListSpec, executeFsList, fsReadFileSpec as fsReadSpec, executeFsReadFile as executeFsRead, updateKnowledgeSpec, executeUpdateKnowledge, astGrepSpec as codeSearchAstSpec, executeAstGrep as executeCodeSearchAst, verifyRunSpec as testRunSpec, executeVerifyRun as executeTestRun, gitDiffCheckSpec, executeGitDiffCheck, gitApplyCheckSpec, executeGitApplyCheck, benchmarkReportSpec, executeBenchmarkReport, sweBenchLoadInstanceSpec, executeSweBenchLoadInstance, sweBenchWritePredictionSpec, executeSweBenchWritePrediction, sweBenchSubmitPredictionsSpec, executeSweBenchSubmitPredictions, sweBenchGetReportSpec, executeSweBenchGetReport, workspaceInfoSpec, executeWorkspaceInfo, };
75
+ export { CodeSearchSpec, codeSearchExecutor, astDefsRefsSpec as codeAstSpec, executeAstDefsRefs as executeCodeAst, codeFindReferencesSpec, executeCodeFindReferences, gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus, gitBlameSpec, executeGitBlame, gitLogSpec, executeGitLog, gitShowSpec, executeGitShow, globFindSpec, executeGlobFind, codeReadSpec, fsListSpec, executeFsList, fsReadFileSpec as fsReadSpec, executeFsReadFile as executeFsRead, updateKnowledgeSpec, executeUpdateKnowledge, astGrepSpec as codeSearchAstSpec, executeAstGrep as executeCodeSearchAst, verifyRunSpec as testRunSpec, executeVerifyRun as executeTestRun, gitDiffCheckSpec, executeGitDiffCheck, gitApplyCheckSpec, executeGitApplyCheck, benchmarkReportSpec, executeBenchmarkReport, sweBenchLoadInstanceSpec, executeSweBenchLoadInstance, sweBenchWritePredictionSpec, executeSweBenchWritePrediction, sweBenchSubmitPredictionsSpec, executeSweBenchSubmitPredictions, sweBenchGetReportSpec, executeSweBenchGetReport, workspaceInfoSpec, executeWorkspaceInfo, };
68
76
  //# sourceMappingURL=index.js.map
@@ -1,10 +1,86 @@
1
1
  import { z } from 'zod';
2
- import { writeFile, mkdir } from '../../adapters/fs/node-fs.js';
2
+ import { readdir, readFile, writeFile, mkdir } from '../../adapters/fs/node-fs.js';
3
3
  import { getDefaultIndexPath } from '../../config/paths.js';
4
+ import { getLogger, tryGetLogger } from '../../observability/logger.js';
4
5
  import { Phase } from '../../types/runtime.js';
5
6
  import { safeJoin } from '../../utils/path.js';
6
7
  let lastEventTimestampMs = 0;
7
8
  let eventSequence = 0;
9
+ // ── Knowledge quality gates ──────────────────────────────────────────────────
10
+ const MIN_RULE_LENGTH = 10;
11
+ const MAX_RULE_LENGTH = 500;
12
+ function isValidContent(text) {
13
+ const trimmed = text.trim();
14
+ return (trimmed.length >= MIN_RULE_LENGTH && trimmed.length <= MAX_RULE_LENGTH && /[\w]/.test(trimmed));
15
+ }
16
+ /** Simple Levenshtein distance for short strings. */
17
+ function levenshtein(a, b) {
18
+ if (a === b)
19
+ return 0;
20
+ if (a.length === 0)
21
+ return b.length;
22
+ if (b.length === 0)
23
+ return a.length;
24
+ const matrix = [];
25
+ for (let i = 0; i <= b.length; i++)
26
+ matrix[i] = [i];
27
+ for (let j = 0; j <= a.length; j++)
28
+ matrix[0][j] = j;
29
+ for (let i = 1; i <= b.length; i++) {
30
+ for (let j = 1; j <= a.length; j++) {
31
+ const cost = b[i - 1] === a[j - 1] ? 0 : 1;
32
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
33
+ }
34
+ }
35
+ return matrix[b.length][a.length];
36
+ }
37
+ /** Check if a rule is too similar to any existing rule. */
38
+ function isDuplicateRule(newRule, existingRules) {
39
+ const normalized = newRule.trim().toLowerCase();
40
+ for (const existing of existingRules) {
41
+ const existingNorm = existing.trim().toLowerCase();
42
+ if (normalized === existingNorm)
43
+ return true;
44
+ if (levenshtein(normalized, existingNorm) < 5)
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+ /** Load existing knowledge to check for duplicates. */
50
+ async function loadExistingKnowledge(knowledgeDir) {
51
+ const rules = [];
52
+ const decisions = [];
53
+ const deprecatedRules = [];
54
+ try {
55
+ const files = await readdir(knowledgeDir);
56
+ const jsonFiles = files.filter((f) => f.endsWith('.json')).sort();
57
+ for (const file of jsonFiles) {
58
+ try {
59
+ const content = await readFile(safeJoin(knowledgeDir, file), 'utf-8');
60
+ const data = JSON.parse(content);
61
+ if (Array.isArray(data.project_rules))
62
+ rules.push(...data.project_rules);
63
+ if (Array.isArray(data.deprecated_rules))
64
+ deprecatedRules.push(...data.deprecated_rules);
65
+ if (Array.isArray(data.architectural_decisions)) {
66
+ for (const d of data.architectural_decisions) {
67
+ if (typeof d.decision === 'string')
68
+ decisions.push(d.decision);
69
+ }
70
+ }
71
+ }
72
+ catch (error) {
73
+ /* skip corrupted */
74
+ getLogger().debug(`[Knowledge] Failed to read knowledge file ${file}: ${error instanceof Error ? error.message : String(error)}`);
75
+ }
76
+ }
77
+ }
78
+ catch (error) {
79
+ /* dir missing */
80
+ getLogger().debug(`[Knowledge] Failed to read knowledge directory: ${error instanceof Error ? error.message : String(error)}`);
81
+ }
82
+ return { rules, decisions, deprecatedRules };
83
+ }
8
84
  function nextEventFilePrefix() {
9
85
  const nowMs = Date.now();
10
86
  if (nowMs === lastEventTimestampMs) {
@@ -36,6 +112,14 @@ const updateKnowledgeInputSchema = z.discriminatedUnion('category', [
36
112
  category: z.literal('user_preferences'),
37
113
  preferences: z.string().describe('Updated description of user personal preferences'),
38
114
  }),
115
+ z.object({
116
+ category: z.literal('lessons_learned'),
117
+ lessons: z.array(z.string()).describe('Lessons learned from execution outcomes'),
118
+ source: z
119
+ .enum(['success', 'failure'])
120
+ .optional()
121
+ .describe('Whether lessons came from success or failure'),
122
+ }),
39
123
  ]);
40
124
  export const updateKnowledgeSpec = {
41
125
  name: 'update_knowledge',
@@ -56,8 +140,60 @@ export async function executeUpdateKnowledge(input, ctx) {
56
140
  const { repoRoot } = ctx;
57
141
  const indexPath = getDefaultIndexPath(repoRoot);
58
142
  const knowledgeDir = safeJoin(indexPath, 'knowledge');
59
- // Use timestamp + per-process sequence to ensure unique append-only filenames
60
- // even when multiple events occur within the same millisecond.
143
+ await mkdir(knowledgeDir, { recursive: true });
144
+ const existing = await loadExistingKnowledge(knowledgeDir);
145
+ // ── Quality gates ────────────────────────────────────────────────────────
146
+ if (input.category === 'project_rules') {
147
+ // Filter out rules that are invalid or duplicate
148
+ const validRules = [];
149
+ let skipped = 0;
150
+ for (const rule of input.rules) {
151
+ if (!isValidContent(rule)) {
152
+ skipped++;
153
+ continue;
154
+ }
155
+ if (isDuplicateRule(rule, existing.rules)) {
156
+ skipped++;
157
+ continue;
158
+ }
159
+ if (isDuplicateRule(rule, existing.deprecatedRules)) {
160
+ skipped++;
161
+ continue;
162
+ }
163
+ validRules.push(rule);
164
+ }
165
+ if (skipped > 0) {
166
+ tryGetLogger()?.debug(`[Knowledge] Filtered ${skipped} invalid/duplicate rules`);
167
+ }
168
+ // If all rules were filtered, skip the write entirely
169
+ if (validRules.length === 0 &&
170
+ (!input.deprecated_rules || input.deprecated_rules.length === 0)) {
171
+ return { success: true, message: 'All rules were duplicates or invalid, nothing to record' };
172
+ }
173
+ // Rewrite input with filtered rules
174
+ input.rules = validRules;
175
+ }
176
+ if (input.category === 'architectural_decisions') {
177
+ if (!isValidContent(input.decision)) {
178
+ return { success: true, message: 'Decision too short or invalid, nothing to record' };
179
+ }
180
+ if (isDuplicateRule(input.decision, existing.decisions)) {
181
+ return { success: true, message: 'Decision already recorded, skipping duplicate' };
182
+ }
183
+ }
184
+ if (input.category === 'user_preferences') {
185
+ if (!isValidContent(input.preferences)) {
186
+ return { success: true, message: 'Preferences too short or invalid, nothing to record' };
187
+ }
188
+ }
189
+ if (input.category === 'lessons_learned') {
190
+ const validLessons = input.lessons.filter((l) => isValidContent(l));
191
+ if (validLessons.length === 0) {
192
+ return { success: true, message: 'All lessons were invalid, nothing to record' };
193
+ }
194
+ input.lessons = validLessons;
195
+ }
196
+ // ── Write ────────────────────────────────────────────────────────────────
61
197
  const fileName = `${nextEventFilePrefix()}-${input.category}.json`;
62
198
  const filePath = safeJoin(knowledgeDir, fileName);
63
199
  let dataToSave = {};
@@ -82,9 +218,15 @@ export async function executeUpdateKnowledge(input, ctx) {
82
218
  case 'user_preferences':
83
219
  dataToSave = { user_preferences: input.preferences };
84
220
  break;
221
+ case 'lessons_learned':
222
+ dataToSave = {
223
+ lessons_learned: input.lessons,
224
+ source: input.source ?? 'unknown',
225
+ date: new Date().toISOString().split('T')[0],
226
+ };
227
+ break;
85
228
  }
86
229
  try {
87
- await mkdir(knowledgeDir, { recursive: true });
88
230
  await writeFile(filePath, JSON.stringify(dataToSave, null, 2));
89
231
  return {
90
232
  success: true,
@@ -9,6 +9,7 @@ import { Executor } from '../../grizzco/execution/Executor.js';
9
9
  import { WorkerFactory } from '../../grizzco/execution/WorkerFactory.js';
10
10
  import { MockLockService } from '../../grizzco/services/implementations/mock/MockLockService.js';
11
11
  import { registry } from '../../grizzco/services/registry.js';
12
+ import { getLogger } from '../../observability/logger.js';
12
13
  import { normalizeDiff, validateDiff, convertDiffToShadowOperations } from '../../patch/diff.js';
13
14
  import { getRejectionsDir } from '../../runtime/paths.js';
14
15
  import { FileStateResolver } from '../../strata/layers/file-state-resolver.js';
@@ -67,7 +68,8 @@ export const proposalApplySpec = {
67
68
  changedFilesTruncated: meta.changedFiles.length > changedFiles.length,
68
69
  });
69
70
  }
70
- catch {
71
+ catch (error) {
72
+ getLogger().warn(`[Proposal] Failed to validate diff for authorization preview: ${error instanceof Error ? error.message : String(error)}`);
71
73
  return JSON.stringify({ handle, preview: 'invalid_diff' });
72
74
  }
73
75
  },
@@ -1,7 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { text } from '../../../locales/index.js';
3
+ import { parseRunnerOutput, parseStructuredSummary } from '../../feedback/parsers.js';
3
4
  import { Phase } from '../../types/runtime.js';
4
- import { runVerify, classifyError } from '../../verification/runner.js';
5
+ import { detectRunner, injectJsonFlags, } from '../../verification/detect-runner.js';
6
+ import { runVerify, classifyError, isRetryable as checkRetryable, parseTestSummary, } from '../../verification/runner.js';
5
7
  import { processResource, repoResource } from '../parallel/resource-helpers.js';
6
8
  export const verifyRunSpec = {
7
9
  name: 'test.run',
@@ -14,6 +16,10 @@ export const verifyRunSpec = {
14
16
  computeResources: (_input, ctx) => [repoResource(ctx), processResource(ctx)],
15
17
  inputSchema: z.object({
16
18
  command: z.string().describe('The shell command to run for verification'),
19
+ runner: z
20
+ .enum(['jest', 'vitest', 'pytest', 'tsc', 'eslint', 'bun', 'go'])
21
+ .optional()
22
+ .describe('Test runner type. Auto-detected from command if omitted.'),
17
23
  }),
18
24
  outputSchema: z.object({
19
25
  ok: z.boolean(),
@@ -21,6 +27,24 @@ export const verifyRunSpec = {
21
27
  exitCode: z.number().nullable(),
22
28
  errorType: z.string().optional(),
23
29
  isRetryable: z.boolean().optional(),
30
+ diagnostics: z
31
+ .array(z.object({
32
+ file: z.string(),
33
+ line: z.number().optional(),
34
+ column: z.number().optional(),
35
+ severity: z.enum(['error', 'warning']),
36
+ message: z.string(),
37
+ source: z.string(),
38
+ }))
39
+ .optional(),
40
+ summary: z
41
+ .object({
42
+ total: z.number(),
43
+ passed: z.number(),
44
+ failed: z.number(),
45
+ skipped: z.number(),
46
+ })
47
+ .optional(),
24
48
  }),
25
49
  allowedPhases: [Phase.VERIFY],
26
50
  };
@@ -29,13 +53,21 @@ export const verifyRunSpec = {
29
53
  */
30
54
  export async function executeVerifyRun(input, ctx) {
31
55
  const { command } = input;
56
+ const runner = input.runner ?? detectRunner(command);
57
+ const effectiveCommand = injectJsonFlags(command, runner);
32
58
  const activePath = ctx.worktreeRoot || ctx.repoRoot;
33
- const result = await runVerify(activePath, command, ctx.env, ctx.signal);
59
+ const result = await runVerify(activePath, effectiveCommand, ctx.env, ctx.signal);
34
60
  const errorType = !result.ok ? classifyError(result.output) : undefined;
61
+ // Structured parsing when we know the runner; generic text heuristics otherwise
62
+ const diagnostics = !result.ok ? parseRunnerOutput(result.output, runner) : [];
63
+ // Prefer structured JSON summary; fall back to regex-based text parser
64
+ const summary = parseStructuredSummary(result.output, runner) ?? parseTestSummary(result.output);
35
65
  return {
36
66
  ...result,
37
67
  errorType,
38
- isRetryable: !result.ok ? true : false, // In SalmonLoop, most verification failures are retryable by the LLM
68
+ isRetryable: errorType ? checkRetryable(errorType) : false,
69
+ diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
70
+ summary,
39
71
  };
40
72
  }
41
73
  //# sourceMappingURL=verify.js.map
@@ -1,4 +1,5 @@
1
1
  import { text } from '../../../locales/index.js';
2
+ import { getLogger } from '../../observability/logger.js';
2
3
  import { normalizeDiff, validateDiff } from '../../patch/diff.js';
3
4
  import { ArtifactStore } from '../../sub-agent/artifacts/store.js';
4
5
  import { normalizeRepoRelativePath } from '../../utils/path.js';
@@ -363,7 +364,8 @@ async function loadProposalChangedFiles(handle) {
363
364
  const meta = validateDiff(normalized);
364
365
  return meta.changedFiles ?? [];
365
366
  }
366
- catch {
367
+ catch (error) {
368
+ getLogger().warn(`[PermissionRules] Failed to load proposal changed files: ${error instanceof Error ? error.message : String(error)}`);
367
369
  return null;
368
370
  }
369
371
  }
@@ -1,5 +1,8 @@
1
1
  import * as crypto from 'crypto';
2
+ import path from 'path';
2
3
  import { z } from 'zod';
4
+ import { readFile } from '../adapters/fs/node-fs.js';
5
+ import { AstParser } from '../ast/parser.js';
3
6
  import { LIMITS } from '../config/limits.js';
4
7
  import { getLogger } from '../observability/logger.js';
5
8
  import { isRecord } from '../utils/serialize.js';
@@ -110,7 +113,7 @@ export class ToolRouter {
110
113
  ctx: normalizedEnvelope.ctx,
111
114
  });
112
115
  if (permissionDecision.kind === 'deny') {
113
- const result = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason, {
116
+ const result = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason ?? 'Permission denied', {
114
117
  authorization: {
115
118
  outcome: 'deny',
116
119
  reason: permissionDecision.reason,
@@ -192,6 +195,12 @@ export class ToolRouter {
192
195
  outputSummary: sanitized.summary,
193
196
  durationMs,
194
197
  };
198
+ // Per-edit syntax guard: check for syntax errors after file writes
199
+ const syntaxWarnings = await checkPostEditSyntax(spec, normalizedEnvelope.args, rawOutput, normalizedEnvelope.ctx);
200
+ if (syntaxWarnings.length > 0) {
201
+ result.warnings = syntaxWarnings;
202
+ result.meta = { ...result.meta, syntaxWarning: true };
203
+ }
195
204
  this.audit.onEnd(result);
196
205
  return result;
197
206
  }
@@ -260,7 +269,7 @@ export class ToolRouter {
260
269
  ctx: normalizedEnvelope.ctx,
261
270
  });
262
271
  if (permissionDecision.kind === 'deny') {
263
- const toolResult = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason, {
272
+ const toolResult = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason ?? 'Permission denied', {
264
273
  authorization: {
265
274
  outcome: 'deny',
266
275
  reason: permissionDecision.reason,
@@ -379,7 +388,8 @@ export class ToolRouter {
379
388
  return raw;
380
389
  return `${raw.slice(0, maxLength)}...`;
381
390
  }
382
- catch {
391
+ catch (error) {
392
+ getLogger().debug(`[ToolRouter] Failed to summarize args: ${error instanceof Error ? error.message : String(error)}`);
383
393
  return '[Unserializable]';
384
394
  }
385
395
  }
@@ -392,7 +402,8 @@ export class ToolRouter {
392
402
  // Truncation to 16 hex was insufficient collision resistance for security use.
393
403
  return crypto.createHash('sha256').update(raw).digest('hex');
394
404
  }
395
- catch {
405
+ catch (error) {
406
+ getLogger().debug(`[ToolRouter] Failed to hash args: ${error instanceof Error ? error.message : String(error)}`);
396
407
  return undefined;
397
408
  }
398
409
  }
@@ -495,9 +506,81 @@ export class ToolRouter {
495
506
  ]);
496
507
  return typeof result === 'string' && result.trim() ? result : fallback;
497
508
  }
498
- catch {
509
+ catch (error) {
510
+ getLogger().debug(`[ToolRouter] Failed to get authorization args summary: ${error instanceof Error ? error.message : String(error)}`);
499
511
  return fallback;
500
512
  }
501
513
  }
502
514
  }
515
+ /**
516
+ * Per-edit syntax guard: after a file write, parse the file with tree-sitter
517
+ * and return syntax error warnings. Non-blocking — returns empty array on
518
+ * any failure (missing grammar, parse error, etc.).
519
+ */
520
+ async function checkPostEditSyntax(spec, args, rawOutput, ctx) {
521
+ if (spec.name !== 'fs.write_file' && spec.name !== 'fs.edit_file')
522
+ return [];
523
+ if (!isRecord(rawOutput) || typeof rawOutput.path !== 'string')
524
+ return [];
525
+ const filePath = rawOutput.path;
526
+ let content;
527
+ if (spec.name === 'fs.write_file') {
528
+ if (!isRecord(args) || typeof args.content !== 'string')
529
+ return [];
530
+ content = args.content;
531
+ }
532
+ else {
533
+ // fs.edit_file — read post-edit content from disk
534
+ try {
535
+ const absolutePath = path.resolve(ctx.repoRoot, filePath);
536
+ content = await readFile(absolutePath, 'utf-8');
537
+ }
538
+ catch (error) {
539
+ getLogger().debug(`[ToolRouter] Failed to read file for post-edit syntax check: ${error instanceof Error ? error.message : String(error)}`);
540
+ return [];
541
+ }
542
+ }
543
+ // Detect language from extension
544
+ const ext = path.extname(filePath).toLowerCase().replace('.', '');
545
+ if (!ext)
546
+ return [];
547
+ // Only check for languages that have tree-sitter support
548
+ const plugin = ctx.languagePlugins?.getByExtension(`.${ext}`);
549
+ if (!plugin)
550
+ return [];
551
+ try {
552
+ const tree = await AstParser.parse(content, plugin.meta.id);
553
+ if (!tree?.rootNode)
554
+ return [];
555
+ const errors = collectSyntaxErrors(tree.rootNode);
556
+ if (errors.length === 0)
557
+ return [];
558
+ return [
559
+ `Syntax warning in ${filePath}: ${errors.length} error(s) detected — ` +
560
+ errors
561
+ .slice(0, 3)
562
+ .map((e) => `line ${e.line}: ${e.text}`)
563
+ .join('; '),
564
+ ];
565
+ }
566
+ catch (error) {
567
+ // Tree-sitter parse failed (no grammar, etc.) — silently skip
568
+ getLogger().debug(`[ToolRouter] Post-edit syntax check parse failed: ${error instanceof Error ? error.message : String(error)}`);
569
+ return [];
570
+ }
571
+ }
572
+ function collectSyntaxErrors(node, errors = [], depth = 0) {
573
+ if (depth > 50)
574
+ return errors; // prevent stack overflow
575
+ if (node.type === 'ERROR' || node.isMissing) {
576
+ errors.push({
577
+ line: (node.startPosition?.row ?? 0) + 1,
578
+ text: node.text?.slice(0, 80) ?? node.type,
579
+ });
580
+ }
581
+ for (const child of node.children ?? []) {
582
+ collectSyntaxErrors(child, errors, depth + 1);
583
+ }
584
+ return errors;
585
+ }
503
586
  //# sourceMappingURL=router.js.map
@@ -46,8 +46,9 @@ function safeParseJson(argsText) {
46
46
  try {
47
47
  value = JSON.parse(nested);
48
48
  }
49
- catch {
49
+ catch (error) {
50
50
  // Ignore: fall back to the first parse result to preserve observability.
51
+ getLogger().debug(`[ToolSession] Double-decoded JSON parse fallback: ${error instanceof Error ? error.message : String(error)}`);
51
52
  }
52
53
  }
53
54
  }
@@ -71,7 +72,8 @@ function formatToolResultForModel(result) {
71
72
  try {
72
73
  return JSON.stringify(payload);
73
74
  }
74
- catch {
75
+ catch (error) {
76
+ getLogger().debug(`[ToolSession] Failed to serialize tool result: ${error instanceof Error ? error.message : String(error)}`);
75
77
  return JSON.stringify({
76
78
  id: result.id,
77
79
  toolName: result.toolName,
@@ -88,7 +90,8 @@ function safeStringifyForAudit(value) {
88
90
  try {
89
91
  return redactJsonString(JSON.stringify(redactValue(value)));
90
92
  }
91
- catch {
93
+ catch (error) {
94
+ getLogger().debug(`[ToolSession] Failed to stringify value for audit: ${error instanceof Error ? error.message : String(error)}`);
92
95
  return '[Unserializable]';
93
96
  }
94
97
  }
@@ -215,7 +218,8 @@ function serializeToolResultOutputForArtifact(output) {
215
218
  fileExt: 'json',
216
219
  };
217
220
  }
218
- catch {
221
+ catch (error) {
222
+ getLogger().debug(`[ToolSession] Failed to serialize tool output for artifact: ${error instanceof Error ? error.message : String(error)}`);
219
223
  return undefined;
220
224
  }
221
225
  }
@@ -789,7 +793,8 @@ function coercePlanUpdatePatch(args) {
789
793
  coercedPatchSource: 'stringified',
790
794
  };
791
795
  }
792
- catch {
796
+ catch (error) {
797
+ getLogger().debug(`[ToolSession] Failed to parse plan.update patch JSON: ${error instanceof Error ? error.message : String(error)}`);
793
798
  return { args, error: formatPlanUpdatePatchTypeError('string') };
794
799
  }
795
800
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=batch.js.map
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../observability/logger.js';
1
2
  /**
2
3
  * Sanitizes any error input (object, string, or mixed) to prevent leakage
3
4
  * of sensitive technical data like Zod dumps or stack traces.
@@ -10,7 +11,8 @@ export function sanitizeErrorMessage(err) {
10
11
  try {
11
12
  msg = err instanceof Error ? err.message : typeof err === 'string' ? err : JSON.stringify(err);
12
13
  }
13
- catch {
14
+ catch (error) {
15
+ getLogger().debug(`[Sanitizer] Failed to convert error to string: ${error instanceof Error ? error.message : String(error)}`);
14
16
  msg = String(err);
15
17
  }
16
18
  // 2. Strict Whitelist Detection
@@ -87,7 +89,8 @@ export function sanitizeObject(obj, maxDepth = MAX_DEPTH, depth = 0) {
87
89
  try {
88
90
  result[key] = sanitizeObject(value, maxDepth, depth + 1);
89
91
  }
90
- catch {
92
+ catch (error) {
93
+ getLogger().debug(`[Sanitizer] Circular reference detected during object sanitization: ${error instanceof Error ? error.message : String(error)}`);
91
94
  result[key] = '[CIRCULAR]';
92
95
  }
93
96
  }
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../observability/logger.js';
1
2
  /**
2
3
  * Safely serialize a value to JSON string.
3
4
  * Returns '[Unserializable]' if JSON.stringify throws and String() also fails.
@@ -10,11 +11,13 @@ export function safeStringify(value, options) {
10
11
  }
11
12
  return raw;
12
13
  }
13
- catch {
14
+ catch (error) {
15
+ getLogger().debug(`[Serialize] JSON.stringify failed, falling back to String(): ${error instanceof Error ? error.message : String(error)}`);
14
16
  try {
15
17
  return String(value);
16
18
  }
17
- catch {
19
+ catch (innerError) {
20
+ getLogger().debug(`[Serialize] String() conversion also failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`);
18
21
  return '[Unserializable]';
19
22
  }
20
23
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Test runner detection and JSON flag injection.
3
+ *
4
+ * Detects which test runner a command invokes and, when the runner
5
+ * supports structured JSON output, rewrites the command to emit JSON
6
+ * so downstream parsers can consume machine-readable data instead of
7
+ * regex-matching human-friendly text.
8
+ */
9
+ // ── Detection ────────────────────────────────────────────────────────────────
10
+ /** Heuristic detection from the raw command string. */
11
+ export function detectRunner(command) {
12
+ const cmd = command.toLowerCase();
13
+ // Order matters: more specific patterns first
14
+ if (/\bvitest\b/.test(cmd))
15
+ return 'vitest';
16
+ if (/\bjest\b/.test(cmd))
17
+ return 'jest';
18
+ if (/\bpytest\b/.test(cmd) || /\bpy\.test\b/.test(cmd))
19
+ return 'pytest';
20
+ if (/\btsc\b/.test(cmd))
21
+ return 'tsc';
22
+ if (/\beslint\b/.test(cmd))
23
+ return 'eslint';
24
+ if (/\bbun\s+test\b/.test(cmd))
25
+ return 'bun';
26
+ if (/\bgo\s+test\b/.test(cmd))
27
+ return 'go';
28
+ // npm/pnpm/yarn script proxies — try to infer from script name
29
+ if (/\bnpm\s+run\s+/.test(cmd) || /\bpnpm\s+/.test(cmd) || /\byarn\s+/.test(cmd)) {
30
+ if (/test:unit|test:e2e|test:integration|test:full/.test(cmd))
31
+ return 'unknown';
32
+ if (/\btest\b/.test(cmd))
33
+ return 'unknown'; // could be anything
34
+ }
35
+ return 'unknown';
36
+ }
37
+ // ── JSON flag injection ──────────────────────────────────────────────────────
38
+ /** Returns true when the runner supports structured JSON output. */
39
+ export function supportsJsonOutput(runner) {
40
+ return (runner === 'jest' ||
41
+ runner === 'vitest' ||
42
+ runner === 'eslint' ||
43
+ runner === 'bun' ||
44
+ runner === 'go');
45
+ }
46
+ /**
47
+ * Rewrite the command to emit structured output.
48
+ *
49
+ * Only modifies commands for runners that support JSON.
50
+ * Returns the original command unchanged when the runner
51
+ * has no JSON mode (pytest, tsc) or is unknown.
52
+ */
53
+ export function injectJsonFlags(command, runner) {
54
+ switch (runner) {
55
+ case 'jest':
56
+ // jest --json --outputFile=/dev/null would suppress file write;
57
+ // but --json alone prints to stdout which is what we want.
58
+ // Avoid duplicating --json if already present.
59
+ if (command.includes('--json'))
60
+ return command;
61
+ return `${command} --json`;
62
+ case 'vitest':
63
+ // vitest --reporter=json --run (--run prevents watch mode)
64
+ if (command.includes('--reporter=json') || command.includes('--reporter json'))
65
+ return command;
66
+ return `${command} --reporter=json --run`;
67
+ case 'eslint':
68
+ // eslint --format json
69
+ if (command.includes('--format json') || command.includes('--format=json'))
70
+ return command;
71
+ return `${command} --format json`;
72
+ case 'bun':
73
+ // bun test --json (outputs NDJSON to stdout)
74
+ if (command.includes('--json'))
75
+ return command;
76
+ return `${command} --json`;
77
+ case 'go':
78
+ // go test -json
79
+ if (command.includes('-json'))
80
+ return command;
81
+ return `${command} -json`;
82
+ default:
83
+ return command;
84
+ }
85
+ }
86
+ //# sourceMappingURL=detect-runner.js.map