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
@@ -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,11 +9,13 @@ 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';
15
16
  import { ArtifactStore } from '../../sub-agent/artifacts/store.js';
16
17
  import { Phase } from '../../types/runtime.js';
18
+ import { isRecord } from '../../utils/serialize.js';
17
19
  function bootstrapRegistry() {
18
20
  if (!registry.has('remote_lock'))
19
21
  registry.register(new MockLockService());
@@ -48,7 +50,7 @@ export const proposalApplySpec = {
48
50
  // challenge-response authorization without violating the execution contract.
49
51
  allowedPhases: [Phase.VERIFY],
50
52
  summarizeArgsForAuthorization: async (args, _ctx) => {
51
- const handle = args?.handle;
53
+ const handle = isRecord(args) && typeof args.handle === 'string' ? args.handle : undefined;
52
54
  if (!handle)
53
55
  return undefined;
54
56
  const read = await ArtifactStore.readText(handle);
@@ -66,7 +68,8 @@ export const proposalApplySpec = {
66
68
  changedFilesTruncated: meta.changedFiles.length > changedFiles.length,
67
69
  });
68
70
  }
69
- catch {
71
+ catch (error) {
72
+ getLogger().warn(`[Proposal] Failed to validate diff for authorization preview: ${error instanceof Error ? error.message : String(error)}`);
70
73
  return JSON.stringify({ handle, preview: 'invalid_diff' });
71
74
  }
72
75
  },
@@ -99,7 +102,15 @@ export async function executeProposalApply(input, ctx) {
99
102
  for (const op of operations) {
100
103
  const fileState = stateMap.get(op.path);
101
104
  const fileInfo = fileState
102
- ? { ...fileState, hasConflict: fileState.status === FileStatus.CONFLICT }
105
+ ? {
106
+ path: fileState.path,
107
+ status: fileState.status,
108
+ isBinary: fileState.isBinary,
109
+ isSymlink: fileState.isSymlink,
110
+ isIgnored: fileState.isIgnored,
111
+ hasConflict: fileState.status === FileStatus.CONFLICT,
112
+ size: fileState.size,
113
+ }
103
114
  : {
104
115
  path: op.path,
105
116
  status: FileStatus.CLEAN,
@@ -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
@@ -75,10 +75,10 @@ export async function runWithFallback(backends, input, ctx, opts) {
75
75
  throw new Error(`All backends failed for capability. Tried: ${JSON.stringify(meta.tried)}`);
76
76
  }
77
77
  function createBackendError(backendId, fail, meta) {
78
- const error = new Error(`Backend ${backendId} failed: [${fail.code}] ${fail.message}`);
79
- error.backendId = backendId;
80
- error.failCode = fail.code;
81
- error.meta = meta;
82
- return error;
78
+ return Object.assign(new Error(`Backend ${backendId} failed: [${fail.code}] ${fail.message}`), {
79
+ backendId,
80
+ failCode: fail.code,
81
+ meta,
82
+ });
83
83
  }
84
84
  //# sourceMappingURL=executor.js.map
@@ -1,7 +1,5 @@
1
1
  import { redactValue } from '../llm/redact.js';
2
- function isRecord(value) {
3
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
4
- }
2
+ import { isRecord } from '../utils/serialize.js';
5
3
  function limitValue(value, params) {
6
4
  if (params.depth >= params.maxDepth)
7
5
  return '[Truncated]';
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { unwrapZodSchema } from '../utils/zod.js';
2
3
  function formatToolExamplesForDescription(spec) {
3
4
  if (!Array.isArray(spec.examples) || spec.examples.length === 0)
4
5
  return '';
@@ -13,37 +14,8 @@ function formatToolExamplesForDescription(spec) {
13
14
  function toolDescriptionForModel(spec) {
14
15
  return `${spec.description}${formatToolExamplesForDescription(spec)}`;
15
16
  }
16
- function unwrapForSchemaGeneration(schema) {
17
- let current = schema;
18
- for (let depth = 0; depth < 20; depth++) {
19
- const ZodEffects = z.ZodEffects;
20
- if (typeof ZodEffects === 'function' && current instanceof ZodEffects) {
21
- current = current._def.schema;
22
- continue;
23
- }
24
- if (current instanceof z.ZodPipe) {
25
- // z.preprocess in Zod v4 produces a ZodPipe(in=ZodTransform, out=<schema>).
26
- current = current._def.out;
27
- continue;
28
- }
29
- if (current instanceof z.ZodOptional) {
30
- current = current._def.innerType;
31
- continue;
32
- }
33
- if (current instanceof z.ZodNullable) {
34
- current = current._def.innerType;
35
- continue;
36
- }
37
- if (current instanceof z.ZodDefault) {
38
- current = current._def.innerType;
39
- continue;
40
- }
41
- break;
42
- }
43
- return current;
44
- }
45
17
  function zodToOpenApi3(schema) {
46
- const unwrapped = unwrapForSchemaGeneration(schema);
18
+ const unwrapped = unwrapZodSchema(schema);
47
19
  const description = unwrapped.description;
48
20
  if (unwrapped instanceof z.ZodObject) {
49
21
  const shape = unwrapped.shape;
@@ -64,34 +36,28 @@ function zodToOpenApi3(schema) {
64
36
  return out;
65
37
  }
66
38
  if (unwrapped instanceof z.ZodArray) {
67
- const items = zodToOpenApi3(unwrapped._def.type);
39
+ const items = zodToOpenApi3(unwrapped.element);
68
40
  const out = { type: 'array', items };
69
41
  if (description)
70
42
  out.description = description;
71
43
  return out;
72
44
  }
73
45
  if (unwrapped instanceof z.ZodEnum) {
74
- const options = unwrapped.options ?? unwrapped._def?.values;
75
- let values = [];
76
- if (Array.isArray(options)) {
77
- values = options.map(String);
78
- }
79
- else if (options && typeof options === 'object') {
80
- values = Object.values(options).map(String);
81
- }
46
+ const options = unwrapped.options;
47
+ const values = options.map(String);
82
48
  const out = values.length > 0 ? { type: 'string', enum: values } : { type: 'string' };
83
49
  if (description)
84
50
  out.description = description;
85
51
  return out;
86
52
  }
87
53
  if (unwrapped instanceof z.ZodLiteral) {
88
- const out = { const: unwrapped._def.value };
54
+ const out = { const: unwrapped.value };
89
55
  if (description)
90
56
  out.description = description;
91
57
  return out;
92
58
  }
93
59
  if (unwrapped instanceof z.ZodUnion) {
94
- const options = unwrapped._def.options;
60
+ const options = unwrapped.options;
95
61
  const out = { oneOf: options.map((o) => zodToOpenApi3(o)) };
96
62
  if (description)
97
63
  out.description = description;
@@ -110,7 +76,7 @@ function zodToOpenApi3(schema) {
110
76
  return out;
111
77
  }
112
78
  if (unwrapped instanceof z.ZodNumber) {
113
- const isInt = Boolean(unwrapped._def.checks?.some((c) => c.kind === 'int'));
79
+ const isInt = unwrapped.isInt;
114
80
  const out = { type: isInt ? 'integer' : 'number' };
115
81
  if (description)
116
82
  out.description = description;
@@ -1,5 +1,12 @@
1
1
  import path from 'path';
2
2
  import { syncFs as fs } from '../../adapters/fs/node-fs.js';
3
+ import { isRecord } from '../../utils/serialize.js';
4
+ function isPersistedPlanState(value) {
5
+ return (isRecord(value) &&
6
+ isRecord(value.plan) &&
7
+ isRecord(value.result) &&
8
+ typeof value.updatedAt === 'string');
9
+ }
3
10
  /**
4
11
  * Persistence layer for parallel execution plans.
5
12
  * Supports saving, loading, and listing plans from the .salmonloop/parallel directory.
@@ -41,10 +48,13 @@ export class PlanPersistence {
41
48
  const filePath = path.join(this.getPersistenceDir(repoRoot), `${planId}.json`);
42
49
  try {
43
50
  const content = await fs.readFile(filePath, 'utf8');
44
- return JSON.parse(content);
51
+ const parsed = JSON.parse(content);
52
+ if (!isPersistedPlanState(parsed))
53
+ return null;
54
+ return parsed;
45
55
  }
46
56
  catch (_error) {
47
- if (_error.code === 'ENOENT') {
57
+ if (isRecord(_error) && _error.code === 'ENOENT') {
48
58
  return null;
49
59
  }
50
60
  throw _error;
@@ -63,7 +73,9 @@ export class PlanPersistence {
63
73
  for (const file of jsonFiles) {
64
74
  try {
65
75
  const content = await fs.readFile(path.join(dir, file), 'utf8');
66
- states.push(JSON.parse(content));
76
+ const parsed = JSON.parse(content);
77
+ if (isPersistedPlanState(parsed))
78
+ states.push(parsed);
67
79
  }
68
80
  catch (_error) {
69
81
  // Skip malformed or unreadable files
@@ -73,7 +85,7 @@ export class PlanPersistence {
73
85
  return states;
74
86
  }
75
87
  catch (_error) {
76
- if (_error.code === 'ENOENT') {
88
+ if (isRecord(_error) && _error.code === 'ENOENT') {
77
89
  return [];
78
90
  }
79
91
  throw _error;
@@ -117,7 +129,7 @@ export class PlanPersistence {
117
129
  await fs.unlink(filePath);
118
130
  }
119
131
  catch (_error) {
120
- if (_error.code !== 'ENOENT') {
132
+ if (isRecord(_error) && _error.code !== 'ENOENT') {
121
133
  throw _error;
122
134
  }
123
135
  }
@@ -1,3 +1,5 @@
1
+ import { Phase } from '../../types/runtime.js';
2
+ import { isRecord } from '../../utils/serialize.js';
1
3
  import { isRecoverableToolInputErrorCode } from '../recoverable-tool-errors.js';
2
4
  import { IsolationManager } from './isolation.js';
3
5
  import { resolveArgsWithResults } from './resolve-args.js';
@@ -14,8 +16,7 @@ export class ParallelScheduler {
14
16
  tryResolveSpec(node) {
15
17
  if (node.spec)
16
18
  return node.spec;
17
- const router = this.router;
18
- const spec = typeof router.getSpec === 'function' ? router.getSpec(node.toolName) : undefined;
19
+ const spec = this.router.getSpec?.(node.toolName);
19
20
  if (!spec)
20
21
  return undefined;
21
22
  node.spec = spec;
@@ -28,6 +29,8 @@ export class ParallelScheduler {
28
29
  return parsed.success ? parsed.data : args;
29
30
  }
30
31
  shouldFallbackFromComputeResources(spec, args, error) {
32
+ if (!spec.inputSchema || typeof spec.inputSchema.safeParse !== 'function')
33
+ return false;
31
34
  const parsed = spec.inputSchema.safeParse(args);
32
35
  if (parsed.success)
33
36
  return false;
@@ -35,9 +38,7 @@ export class ParallelScheduler {
35
38
  if (issueCode === 'invalid_type' || issueCode === 'invalid_union' || issueCode === 'custom') {
36
39
  return true;
37
40
  }
38
- const errorCode = typeof error === 'object' && error !== null && 'code' in error
39
- ? error.code
40
- : undefined;
41
+ const errorCode = isRecord(error) && typeof error.code === 'string' ? error.code : undefined;
41
42
  return isRecoverableToolInputErrorCode(errorCode);
42
43
  }
43
44
  deriveDefaultResources(spec, ctx) {
@@ -139,7 +140,7 @@ export class ParallelScheduler {
139
140
  try {
140
141
  const spec = this.tryResolveSpec(node);
141
142
  if (!spec) {
142
- const phase = typeof baseCtx.phase === 'string' ? baseCtx.phase : undefined;
143
+ const phase = isRecord(baseCtx) && typeof baseCtx.phase === 'string' ? baseCtx.phase : undefined;
143
144
  const toolResult = {
144
145
  id: nodeId,
145
146
  toolName: node.toolName,
@@ -185,15 +186,14 @@ export class ParallelScheduler {
185
186
  const resolvedArgs = resolveArgsWithResults(node.args, nodeResults);
186
187
  const normalizedArgs = this.normalizeArgsForSpec(spec, resolvedArgs);
187
188
  // 1.5 Deferred authorization preflight (avoid holding locks while waiting for user)
188
- const preflight = typeof this.router.preflightDeferredAuthorization === 'function'
189
- ? await this.router.preflightDeferredAuthorization({
190
- id: nodeId,
191
- phase: baseCtx.phase || 'execute',
192
- toolName: node.toolName,
193
- args: normalizedArgs,
194
- ctx: baseCtx,
195
- })
196
- : null;
189
+ const phase = isRecord(baseCtx) && typeof baseCtx.phase === 'string' ? baseCtx.phase : Phase.EXPLORE;
190
+ const preflight = (await this.router.preflightDeferredAuthorization?.({
191
+ id: nodeId,
192
+ phase,
193
+ toolName: node.toolName,
194
+ args: normalizedArgs,
195
+ ctx: baseCtx,
196
+ })) ?? null;
197
197
  if (preflight?.kind === 'pending') {
198
198
  nodeStates.set(nodeId, 'BLOCKED_APPROVAL');
199
199
  const approval = {
@@ -250,7 +250,7 @@ export class ParallelScheduler {
250
250
  const runStart = Date.now();
251
251
  const result = await this.router.call({
252
252
  id: nodeId,
253
- phase: baseCtx.phase || 'execute',
253
+ phase,
254
254
  toolName: node.toolName,
255
255
  args: normalizedArgs,
256
256
  ctx: isolatedEnv
@@ -275,7 +275,9 @@ export class ParallelScheduler {
275
275
  toolName: node.toolName,
276
276
  riskLevel: spec.riskLevel,
277
277
  message: result.error.message || 'Approval required',
278
- confirmToken: result.error.confirmToken,
278
+ confirmToken: isRecord(result.error)
279
+ ? result.error.confirmToken
280
+ : undefined,
279
281
  };
280
282
  nodeResults[nodeId] = {
281
283
  status: 'BLOCKED_APPROVAL',
@@ -307,18 +309,18 @@ export class ParallelScheduler {
307
309
  catch (e) {
308
310
  nodeStates.set(nodeId, 'FAILED');
309
311
  const error = e instanceof Error
310
- ? { code: 'EXECUTION_ERROR', message: e.message, stack: e.stack }
311
- : { code: 'EXECUTION_ERROR', message: String(e) };
312
+ ? { code: 'EXECUTION_ERROR', message: e.message, retryable: false }
313
+ : { code: 'EXECUTION_ERROR', message: String(e), retryable: false };
312
314
  const toolResult = {
313
315
  id: nodeId,
314
316
  toolName: node.toolName,
315
317
  source: 'builtin',
316
318
  status: 'error',
317
- error: error,
319
+ error,
318
320
  };
319
321
  nodeResults[nodeId] = {
320
322
  status: 'FAILED',
321
- error: error,
323
+ error,
322
324
  toolResult,
323
325
  timing: { lockWaitMs: 0, runMs: 0 },
324
326
  };