salmon-loop 0.3.0 → 0.3.1

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 (92) hide show
  1. package/dist/cli/authorization/non-interactive.js +7 -21
  2. package/dist/cli/commands/chat.js +1 -1
  3. package/dist/cli/commands/parallel.js +46 -41
  4. package/dist/cli/commands/run/assistant-message.js +3 -0
  5. package/dist/cli/commands/run/handler.js +2 -1
  6. package/dist/cli/commands/serve.js +109 -153
  7. package/dist/cli/headless/json-protocol.js +1 -1
  8. package/dist/cli/headless/stream-json-protocol.js +3 -2
  9. package/dist/cli/slash/runtime.js +5 -1
  10. package/dist/core/adapters/fs/node-fs.js +1 -0
  11. package/dist/core/benchmark/patch-artifact.js +1 -1
  12. package/dist/core/context/service.js +5 -2
  13. package/dist/core/extensions/index.js +2 -35
  14. package/dist/core/extensions/redact.js +9 -3
  15. package/dist/core/extensions/schemas.js +2 -51
  16. package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
  17. package/dist/core/facades/cli-serve.js +0 -1
  18. package/dist/core/grizzco/dsl/strategies.js +1 -3
  19. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
  20. package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
  21. package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
  22. package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
  23. package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
  24. package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
  25. package/dist/core/grizzco/steps/apply.js +0 -7
  26. package/dist/core/grizzco/steps/autopilot.js +108 -6
  27. package/dist/core/grizzco/steps/preflight.js +10 -0
  28. package/dist/core/grizzco/steps/tool-runtime.js +1 -0
  29. package/dist/core/interaction/events/bus.js +14 -0
  30. package/dist/core/interaction/orchestration/facade.js +10 -0
  31. package/dist/core/mcp/bridge/index.js +4 -0
  32. package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
  33. package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
  34. package/dist/core/mcp/bridge/tool-bridge.js +303 -0
  35. package/dist/core/mcp/cache/resource-cache.js +41 -0
  36. package/dist/core/mcp/catalog/discovery.js +51 -0
  37. package/dist/core/mcp/catalog/notification-router.js +28 -0
  38. package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
  39. package/dist/core/mcp/catalog/resource-catalog.js +7 -0
  40. package/dist/core/mcp/catalog/tool-catalog.js +4 -0
  41. package/dist/core/mcp/client/connection-manager.js +239 -0
  42. package/dist/core/mcp/client/lifecycle.js +13 -0
  43. package/dist/core/mcp/client/transport-factory.js +168 -0
  44. package/dist/core/mcp/config/index.js +32 -0
  45. package/dist/core/mcp/config/schema-v2.js +129 -0
  46. package/dist/core/mcp/host/elicitation-provider.js +209 -0
  47. package/dist/core/mcp/host/roots-provider.js +70 -0
  48. package/dist/core/mcp/host/sampling-provider.js +170 -0
  49. package/dist/core/mcp/index.js +4 -0
  50. package/dist/core/mcp/observability/events.js +19 -0
  51. package/dist/core/mcp/policy/approval-policy.js +2 -0
  52. package/dist/core/mcp/policy/classifier.js +172 -0
  53. package/dist/core/mcp/policy/grants.js +356 -0
  54. package/dist/core/mcp/policy/uri-policy.js +60 -0
  55. package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
  56. package/dist/core/mcp/types.js +2 -0
  57. package/dist/core/protocols/a2a/agent-card.js +36 -11
  58. package/dist/core/protocols/a2a/sdk/executor.js +105 -36
  59. package/dist/core/protocols/a2a/sdk/server.js +1311 -3
  60. package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
  61. package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
  62. package/dist/core/protocols/acp/acp-types.js +17 -0
  63. package/dist/core/protocols/acp/formal-agent.js +271 -603
  64. package/dist/core/protocols/acp/handlers.js +3 -0
  65. package/dist/core/protocols/acp/permission-provider.js +11 -39
  66. package/dist/core/protocols/acp/stdio-server.js +20 -1
  67. package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
  68. package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
  69. package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
  70. package/dist/core/public-capabilities/projections.js +1 -0
  71. package/dist/core/runtime/agent-server-runtime.js +2 -3
  72. package/dist/core/runtime/spawn-command.js +8 -2
  73. package/dist/core/runtime/spawn-interactive.js +26 -0
  74. package/dist/core/session/manager.js +48 -25
  75. package/dist/core/tools/builtin/index.js +6 -1
  76. package/dist/core/tools/builtin/proposal.js +0 -7
  77. package/dist/core/tools/builtin/workspace.js +76 -0
  78. package/dist/core/tools/dispatcher.js +1 -0
  79. package/dist/core/tools/loader.js +92 -46
  80. package/dist/core/verification/runner.js +60 -31
  81. package/dist/core/workspace/capabilities.js +80 -0
  82. package/dist/locales/en.js +17 -3
  83. package/package.json +4 -2
  84. package/dist/core/protocols/a2a/mapper.js +0 -14
  85. package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
  86. package/dist/core/protocols/a2a/task-projection.js +0 -45
  87. package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
  88. package/dist/core/tools/mcp/client.js +0 -309
  89. package/dist/core/tools/mcp/loader.js +0 -110
  90. package/dist/core/tools/mcp/schema.js +0 -54
  91. package/dist/core/tools/mcp/streamable-http.js +0 -101
  92. package/dist/core/tools/mcp/types.js +0 -26
@@ -1,6 +1,6 @@
1
1
  import { execa } from 'execa';
2
2
  import { z } from 'zod';
3
- import { getLogger, McpClient } from '../../core/facades/cli-authorization-non-interactive.js';
3
+ import { getLogger, McpConnectionManager, } from '../../core/facades/cli-authorization-non-interactive.js';
4
4
  import { text } from '../locales/index.js';
5
5
  const DecisionSchema = z
6
6
  .object({
@@ -21,22 +21,6 @@ function findMcpServer(extensions, name) {
21
21
  return undefined;
22
22
  return extensions.mcpServers.find((s) => s.enabled && s.name === name);
23
23
  }
24
- function toMcpClientConfig(server) {
25
- if (server.transport === 'http') {
26
- return {
27
- name: server.name,
28
- url: server.url,
29
- headers: server.headers,
30
- };
31
- }
32
- return {
33
- name: server.name,
34
- command: server.command,
35
- args: server.args,
36
- env: server.env,
37
- cwd: server.cwd,
38
- };
39
- }
40
24
  function extractDecisionPayloadFromMcpResult(result) {
41
25
  if (!result || typeof result !== 'object')
42
26
  return result;
@@ -130,11 +114,13 @@ export async function requestNonInteractiveAuthorizationDecision(params) {
130
114
  const timeoutMs = params.config.nonInteractive?.mcp?.timeoutMs ?? 10_000;
131
115
  const controller = new AbortController();
132
116
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
133
- const client = new McpClient(toMcpClientConfig(server));
117
+ const manager = params.mcpConnectionManagerFactory?.(server) ?? new McpConnectionManager([server]);
134
118
  try {
135
- await client.start();
119
+ await manager.startAll();
136
120
  const result = await Promise.race([
137
- client.callTool(toolName, { request: params.request }),
121
+ manager.callTool(server.name, toolName, { request: params.request }, {
122
+ signal: controller.signal,
123
+ }),
138
124
  new Promise((_, reject) => {
139
125
  controller.signal.addEventListener('abort', () => reject(new Error('timeout')), {
140
126
  once: true,
@@ -156,7 +142,7 @@ export async function requestNonInteractiveAuthorizationDecision(params) {
156
142
  }
157
143
  finally {
158
144
  clearTimeout(timeout);
159
- await client.stop();
145
+ await manager.stopAll();
160
146
  }
161
147
  }
162
148
  const unknown = String(params.config.nonInteractive?.strategy ?? '');
@@ -46,7 +46,7 @@ export async function handleChatCommand(options, command) {
46
46
  const modeOptionSource = getOptionValueSourceWithGlobalFallback(command, 'mode');
47
47
  const checkpointStrategyOptionSource = getOptionValueSourceWithGlobalFallback(command, 'checkpointStrategy');
48
48
  const rawPermissionMode = (modeOptionSource === 'cli' ? allOptions.mode : undefined) ??
49
- resolvedConfig.permissionMode ??
49
+ normalizePermissionMode(resolvedConfig.raw?.mode) ??
50
50
  defaultFlowProfile.defaultPermissionMode ??
51
51
  'interactive';
52
52
  const permissionMode = normalizePermissionMode(rawPermissionMode);
@@ -196,50 +196,55 @@ export const parallelCommand = {
196
196
  authorizationMode: 'deferred',
197
197
  extensions: extensionResolution.resolved,
198
198
  });
199
- const scheduler = new ParallelScheduler(toolstack.router, new InMemoryLockManager());
200
- const phase = state.runtime?.phase || 'PATCH';
201
- const runtime = {
202
- repoRoot: workspace.workPath,
203
- worktreeRoot: workspace.workPath,
204
- persistenceRoot: workspace.baseRepoPath,
205
- attemptId: 0,
206
- dryRun: false,
207
- model: state.runtime?.model,
208
- phase,
209
- };
210
- const runSignal = new AbortController().signal;
211
- let result = await scheduler.run(state.plan, runtime, runSignal, {
212
- initialResults: state.result.nodeResults,
213
- resumeBlockedApprovals: true,
214
- });
215
- const canWaitForAuth = typeof toolstack.router.waitForAuthorization === 'function';
216
- let resumeAttempts = 0;
217
- while (result.blockedApprovals.length > 0 && canWaitForAuth && !runSignal.aborted) {
218
- resumeAttempts++;
219
- if (resumeAttempts > 10)
220
- break;
221
- await Promise.all(result.blockedApprovals.map(async (a) => {
222
- await toolstack.router.waitForAuthorization(a.nodeId, runSignal);
223
- }));
224
- result = await scheduler.run(state.plan, runtime, runSignal, {
225
- initialResults: result.nodeResults,
199
+ try {
200
+ const scheduler = new ParallelScheduler(toolstack.router, new InMemoryLockManager());
201
+ const phase = state.runtime?.phase || 'PATCH';
202
+ const runtime = {
203
+ repoRoot: workspace.workPath,
204
+ worktreeRoot: workspace.workPath,
205
+ persistenceRoot: workspace.baseRepoPath,
206
+ attemptId: 0,
207
+ dryRun: false,
208
+ model: state.runtime?.model,
209
+ phase,
210
+ };
211
+ const runSignal = new AbortController().signal;
212
+ let result = await scheduler.run(state.plan, runtime, runSignal, {
213
+ initialResults: state.result.nodeResults,
226
214
  resumeBlockedApprovals: true,
227
215
  });
216
+ const canWaitForAuth = typeof toolstack.router.waitForAuthorization === 'function';
217
+ let resumeAttempts = 0;
218
+ while (result.blockedApprovals.length > 0 && canWaitForAuth && !runSignal.aborted) {
219
+ resumeAttempts++;
220
+ if (resumeAttempts > 10)
221
+ break;
222
+ await Promise.all(result.blockedApprovals.map(async (a) => {
223
+ await toolstack.router.waitForAuthorization(a.nodeId, runSignal);
224
+ }));
225
+ result = await scheduler.run(state.plan, runtime, runSignal, {
226
+ initialResults: result.nodeResults,
227
+ resumeBlockedApprovals: true,
228
+ });
229
+ }
230
+ await PlanPersistence.save(workspace.baseRepoPath, state.plan, result, {
231
+ repoRoot: workspace.workPath,
232
+ worktreeRoot: workspace.workPath,
233
+ persistenceRoot: workspace.baseRepoPath,
234
+ phase,
235
+ model: state.runtime?.model,
236
+ });
237
+ emit({
238
+ type: 'log',
239
+ level: result.failed ? 'warn' : 'info',
240
+ message: text.cli.parallelResumed(planId, result.blockedApprovals.length, result.failed),
241
+ timestamp: new Date(),
242
+ });
243
+ return;
244
+ }
245
+ finally {
246
+ await toolstack.dispose?.();
228
247
  }
229
- await PlanPersistence.save(workspace.baseRepoPath, state.plan, result, {
230
- repoRoot: workspace.workPath,
231
- worktreeRoot: workspace.workPath,
232
- persistenceRoot: workspace.baseRepoPath,
233
- phase,
234
- model: state.runtime?.model,
235
- });
236
- emit({
237
- type: 'log',
238
- level: result.failed ? 'warn' : 'info',
239
- message: text.cli.parallelResumed(planId, result.blockedApprovals.length, result.failed),
240
- timestamp: new Date(),
241
- });
242
- return;
243
248
  }
244
249
  finally {
245
250
  await WorkspaceManager.teardown(workspace, emit);
@@ -2,6 +2,9 @@ import { text } from '../../locales/index.js';
2
2
  export function buildRunAssistantMessage(params) {
3
3
  if (!params.result.success)
4
4
  return text.cli.chatFailed(params.result.reason);
5
+ const answerMessage = params.mode === 'answer' ? params.result.assistantMessage?.trim() : '';
6
+ if (answerMessage)
7
+ return answerMessage;
5
8
  if (params.mode === 'review')
6
9
  return text.cli.chatReviewCompleted;
7
10
  if (params.mode === 'research')
@@ -244,8 +244,9 @@ export async function handleRunCommand(options, command) {
244
244
  }
245
245
  const profile = resolveExecutionProfile(mode);
246
246
  const permissionModeOptionSource = getOptionValueSourceWithGlobalFallback(command, 'mode');
247
+ const configuredPermissionMode = normalizePermissionMode(resolvedConfig.raw?.mode);
247
248
  const rawPermissionMode = (permissionModeOptionSource === 'cli' ? allOptions.mode : undefined) ??
248
- resolvedConfig.permissionMode ??
249
+ configuredPermissionMode ??
249
250
  profile.defaultPermissionMode ??
250
251
  'interactive';
251
252
  const permissionMode = normalizePermissionMode(rawPermissionMode);
@@ -1,7 +1,7 @@
1
1
  import { normalizePermissionMode } from '../../core/config/index.js';
2
2
  import { buildA2AAgentCard, createAcpFormalAgent, createAgentServerRuntime, createInteractionFacade, createSalmonTaskExecutor, createTaskEventBus, createPluginRegistry, createPromptRegistry, getUserAcpSessionStorePath, GitSnapshotCheckpointService, getLogger, mergeResolvedExtensions, PACKAGE_VERSION, PlainReporter, PluginLoader, resolveExtensions, resolveExecutionProfile, runSalmonLoop, setPluginRegistry, setPromptRegistry, startAcpStdioServer, StderrReporter, } from '../../core/facades/cli-serve.js';
3
- import { selectPublicCapabilitiesForSurface, toA2APublicSkills, } from '../../core/public-capabilities/projections.js';
4
- import { buildPublicCapabilityRegistry } from '../../core/public-capabilities/registry.js';
3
+ import { readPlan } from '../../core/plan/index.js';
4
+ import { toA2APublicSkills } from '../../core/public-capabilities/projections.js';
5
5
  import { createTerminalAuthorizationProvider } from '../authorization/provider.js';
6
6
  import { text } from '../locales/index.js';
7
7
  import { getOptionValueSourceWithGlobalFallback } from '../utils/command-option-source.js';
@@ -41,7 +41,7 @@ function buildServeLoopExecutionDefaults(mode) {
41
41
  };
42
42
  }
43
43
  function registerServeShutdown({ message, closeRuntime, closeAcpStdio, }) {
44
- process.once('SIGINT', async () => {
44
+ const shutdown = async () => {
45
45
  getLogger().info(message);
46
46
  try {
47
47
  await closeRuntime?.();
@@ -50,39 +50,12 @@ function registerServeShutdown({ message, closeRuntime, closeAcpStdio, }) {
50
50
  closeAcpStdio?.();
51
51
  process.exit(0);
52
52
  }
53
- });
54
- }
55
- export function registerServeCommands(program) {
56
- const serve = program
57
- .command('serve')
58
- .description(text.cli.serveDescription)
59
- .option('--a2a-host <host>', text.cli.a2aHostOption)
60
- .option('--a2a-port <port>', text.cli.a2aPortOption)
61
- .option('--a2a-token <token>', text.cli.a2aTokenOption, (value, previous) => previous.concat([value]), [])
62
- .option('--no-acp-stdio', text.cli.acpStdioDisableOption)
63
- .option('--no-color', text.cli.noColorOption)
64
- .action(handleServeCommand);
65
- serve
66
- .command('acp')
67
- .description(text.cli.serveAcpDescription)
68
- .option('--no-color', text.cli.noColorOption)
69
- .action(handleServeAcpCommand);
53
+ };
54
+ process.once('SIGINT', shutdown);
55
+ process.once('SIGTERM', shutdown);
70
56
  }
71
- export async function handleServeCommand(_options, command) {
72
- const allOptions = command.optsWithGlobals();
73
- const configResult = await resolveCliConfig({
74
- repo: allOptions.repo,
75
- cwd: process.cwd(),
76
- configPath: allOptions.config,
77
- enableConfigFile: allOptions.configFile !== false,
78
- auditScope: allOptions.auditScope,
79
- logMode: allOptions.logMode,
80
- });
81
- if (!configResult.ok) {
82
- getLogger().error(configResult.message, true);
83
- process.exit(1);
84
- }
85
- const { resolvedConfig, auditScope, repoPath: defaultRepoPath } = configResult;
57
+ async function resolveServeCommonSetup(params) {
58
+ const { command, allOptions, defaultRepoPath, resolvedConfig, auditScope } = params;
86
59
  const defaultFlowMode = 'autopilot';
87
60
  const defaultPermissionMode = resolveServePermissionMode({
88
61
  command,
@@ -90,19 +63,6 @@ export async function handleServeCommand(_options, command) {
90
63
  rawConfiguredPermissionMode: resolvedConfig.raw?.mode,
91
64
  flowMode: defaultFlowMode,
92
65
  });
93
- const serverConfig = resolvedConfig.server;
94
- const rawA2aHost = allOptions.a2aHost ?? serverConfig?.a2a?.host;
95
- const a2aHost = String(rawA2aHost ?? '127.0.0.1');
96
- const rawA2aPort = allOptions.a2aPort ?? serverConfig?.a2a?.port;
97
- const a2aPort = parsePort(rawA2aPort, 7431);
98
- if (!Number.isFinite(a2aPort) || a2aPort <= 0) {
99
- getLogger().error(text.cli.invalidA2APort(String(rawA2aPort ?? '')), true);
100
- process.exit(1);
101
- }
102
- const acpStdioEnabled = allOptions.acpStdio !== false;
103
- if (acpStdioEnabled) {
104
- getLogger().setReporter(allOptions.color === false ? new StderrReporter() : new PlainReporter());
105
- }
106
66
  const languagePlugins = createPluginRegistry();
107
67
  setPluginRegistry(languagePlugins);
108
68
  setPromptRegistry(createPromptRegistry());
@@ -168,6 +128,87 @@ export async function handleServeCommand(_options, command) {
168
128
  executeTask: executor.execute,
169
129
  eventBus: sharedEventBus,
170
130
  });
131
+ return {
132
+ defaultPermissionMode,
133
+ executor,
134
+ sharedEventBus,
135
+ checkpointService,
136
+ acpFacade,
137
+ };
138
+ }
139
+ function buildAcpAgentOptions(params) {
140
+ return {
141
+ agentInfo: { name: 'salmon-loop', version: PACKAGE_VERSION },
142
+ defaultModeId: 'autopilot',
143
+ defaultPermissionPolicy: resolveDefaultAcpPermissionPolicy(params.defaultPermissionMode),
144
+ checkpointReader: {
145
+ listBySession: async ({ repoPath, sessionId, limit, }) => await params.checkpointService.list({ repoPath, sessionId, limit }),
146
+ getById: async ({ repoPath, checkpointId }) => (await params.checkpointService.loadWithStatus({ repoPath, checkpointId })).handle,
147
+ probeById: async ({ repoPath, checkpointId }) => {
148
+ const status = await params.checkpointService.loadWithStatus({ repoPath, checkpointId });
149
+ return { valid: Boolean(status.handle), reason: status.reason };
150
+ },
151
+ },
152
+ planReader: {
153
+ readBySession: async ({ repoPath, sessionId }) => await readPlan({ persistenceRoot: repoPath, sessionId }),
154
+ },
155
+ facade: params.facade,
156
+ sessionPersistencePath: getUserAcpSessionStorePath(),
157
+ sessionStorePolicy: params.sessionStorePolicy,
158
+ eventBus: params.sharedEventBus,
159
+ };
160
+ }
161
+ export function registerServeCommands(program) {
162
+ const serve = program
163
+ .command('serve')
164
+ .description(text.cli.serveDescription)
165
+ .option('--a2a-host <host>', text.cli.a2aHostOption)
166
+ .option('--a2a-port <port>', text.cli.a2aPortOption)
167
+ .option('--a2a-token <token>', text.cli.a2aTokenOption, (value, previous) => previous.concat([value]), [])
168
+ .option('--no-acp-stdio', text.cli.acpStdioDisableOption)
169
+ .option('--no-color', text.cli.noColorOption)
170
+ .action(handleServeCommand);
171
+ serve
172
+ .command('acp')
173
+ .description(text.cli.serveAcpDescription)
174
+ .option('--no-color', text.cli.noColorOption)
175
+ .action(handleServeAcpCommand);
176
+ }
177
+ export async function handleServeCommand(_options, command) {
178
+ const allOptions = command.optsWithGlobals();
179
+ const configResult = await resolveCliConfig({
180
+ repo: allOptions.repo,
181
+ cwd: process.cwd(),
182
+ configPath: allOptions.config,
183
+ enableConfigFile: allOptions.configFile !== false,
184
+ auditScope: allOptions.auditScope,
185
+ logMode: allOptions.logMode,
186
+ });
187
+ if (!configResult.ok) {
188
+ getLogger().error(configResult.message, true);
189
+ process.exit(1);
190
+ }
191
+ const { resolvedConfig, repoPath: defaultRepoPath } = configResult;
192
+ const serverConfig = resolvedConfig.server;
193
+ const rawA2aHost = allOptions.a2aHost ?? serverConfig?.a2a?.host;
194
+ const a2aHost = String(rawA2aHost ?? '127.0.0.1');
195
+ const rawA2aPort = allOptions.a2aPort ?? serverConfig?.a2a?.port;
196
+ const a2aPort = parsePort(rawA2aPort, 7431);
197
+ if (!Number.isFinite(a2aPort) || a2aPort <= 0) {
198
+ getLogger().error(text.cli.invalidA2APort(String(rawA2aPort ?? '')), true);
199
+ process.exit(1);
200
+ }
201
+ const acpStdioEnabled = allOptions.acpStdio !== false;
202
+ if (acpStdioEnabled) {
203
+ getLogger().setReporter(allOptions.color === false ? new StderrReporter() : new PlainReporter());
204
+ }
205
+ const { defaultPermissionMode, executor, sharedEventBus, checkpointService, acpFacade } = await resolveServeCommonSetup({
206
+ command,
207
+ allOptions,
208
+ defaultRepoPath,
209
+ resolvedConfig,
210
+ auditScope: configResult.auditScope,
211
+ });
171
212
  const tokens = Array.isArray(allOptions.a2aToken)
172
213
  ? allOptions.a2aToken.filter((token) => typeof token === 'string')
173
214
  : [];
@@ -188,33 +229,22 @@ export async function handleServeCommand(_options, command) {
188
229
  next();
189
230
  }
190
231
  : undefined;
191
- const a2aPublicCapabilities = selectPublicCapabilitiesForSurface('a2a', buildPublicCapabilityRegistry());
192
- const a2aSkills = toA2APublicSkills(a2aPublicCapabilities);
232
+ const a2aSkills = toA2APublicSkills();
193
233
  const agentCard = buildA2AAgentCard({
194
234
  name: 'salmon-loop',
195
- url: `http://${a2aHost}:${a2aPort}`,
235
+ url: `http://${a2aHost}:${a2aPort}/a2a/jsonrpc`,
196
236
  capabilities: a2aSkills,
197
237
  security: authTokens.length > 0 ? [{ type: 'http', scheme: 'bearer' }] : [],
198
238
  });
199
239
  if (acpStdioEnabled) {
200
- startAcpStdioServer((conn) => createAcpFormalAgent({
201
- conn,
202
- agentInfo: { name: 'salmon-loop', version: PACKAGE_VERSION },
203
- defaultModeId: 'autopilot',
204
- defaultPermissionPolicy: resolveDefaultAcpPermissionPolicy(defaultPermissionMode),
205
- checkpointReader: {
206
- listBySession: async ({ repoPath, sessionId, limit }) => await checkpointService.list({ repoPath, sessionId, limit }),
207
- getById: async ({ repoPath, checkpointId }) => (await checkpointService.loadWithStatus({ repoPath, checkpointId })).handle,
208
- probeById: async ({ repoPath, checkpointId }) => {
209
- const status = await checkpointService.loadWithStatus({ repoPath, checkpointId });
210
- return { valid: Boolean(status.handle), reason: status.reason };
211
- },
212
- },
240
+ const agentOptions = buildAcpAgentOptions({
213
241
  facade: acpFacade,
214
- sessionPersistencePath: getUserAcpSessionStorePath(),
242
+ checkpointService,
243
+ defaultPermissionMode,
244
+ sharedEventBus,
215
245
  sessionStorePolicy: resolvedConfig.server?.acp?.sessionStore,
216
- eventBus: sharedEventBus,
217
- }));
246
+ });
247
+ startAcpStdioServer((conn) => createAcpFormalAgent({ conn, ...agentOptions }));
218
248
  getLogger().info(text.cli.acpStdioStarted('n/a (stdio)'));
219
249
  }
220
250
  const runtime = createAgentServerRuntime({
@@ -250,97 +280,23 @@ export async function handleServeAcpCommand(_options, command) {
250
280
  getLogger().error(configResult.message, true);
251
281
  process.exit(1);
252
282
  }
253
- const { resolvedConfig, auditScope, repoPath: defaultRepoPath } = configResult;
254
- const defaultFlowMode = 'autopilot';
255
- const defaultPermissionMode = resolveServePermissionMode({
283
+ const { resolvedConfig, repoPath: defaultRepoPath } = configResult;
284
+ getLogger().setReporter(allOptions.color === false ? new StderrReporter() : new PlainReporter());
285
+ const { defaultPermissionMode, acpFacade, checkpointService, sharedEventBus } = await resolveServeCommonSetup({
256
286
  command,
257
287
  allOptions,
258
- rawConfiguredPermissionMode: resolvedConfig.raw?.mode,
259
- flowMode: defaultFlowMode,
288
+ defaultRepoPath,
289
+ resolvedConfig,
290
+ auditScope: configResult.auditScope,
260
291
  });
261
- getLogger().setReporter(allOptions.color === false ? new StderrReporter() : new PlainReporter());
262
- const languagePlugins = createPluginRegistry();
263
- setPluginRegistry(languagePlugins);
264
- setPromptRegistry(createPromptRegistry());
265
- await PluginLoader.loadPlugins(languagePlugins, defaultRepoPath);
266
- const extensions = await resolveExtensions({ repoRoot: defaultRepoPath });
267
- const { llm } = createRuntimeLlmAndWarn({
268
- llmConfig: resolvedConfig.llm,
269
- langfuseEnabled: resolvedConfig.observability.langfuse.enabled,
270
- });
271
- const outcomeReporter = createOutcomeReporter({
272
- enabled: resolvedConfig.observability.langfuse.outcome,
273
- endpoint: resolvedConfig.observability.langfuse.endpoint,
274
- llmBaseUrl: resolvedConfig.llm.api.baseUrl,
275
- langfuseApiKey: resolvedConfig.observability.langfuse.apiKey,
276
- });
277
- const defaultAuthorizationProvider = createTerminalAuthorizationProvider({
278
- config: resolvedConfig.toolAuthorization,
279
- extensions: extensions.resolved,
280
- forceNonInteractive: true,
281
- });
282
- const executor = createSalmonTaskExecutor({
283
- runLoop: async ({ instruction, mode, repoPath, onEvent, signal, authorizationProvider, authorizationMode, extensions: taskExtensions, }) => {
284
- const effectiveRepoPath = repoPath ?? defaultRepoPath;
285
- const flowMode = mode;
286
- const executionDefaults = buildServeLoopExecutionDefaults(flowMode);
287
- const permissionMode = resolveServePermissionMode({
288
- command,
289
- allOptions,
290
- rawConfiguredPermissionMode: resolvedConfig.raw?.mode,
291
- flowMode,
292
- });
293
- return await runSalmonLoop({
294
- instruction,
295
- repoPath: effectiveRepoPath,
296
- llm,
297
- mode: flowMode,
298
- verify: resolvedConfig.verify.command,
299
- ...executionDefaults,
300
- llmOutput: resolvedConfig.llmOutput,
301
- outcomeReporter,
302
- langfuseSessionId: resolvedConfig.observability.langfuse.sessionId,
303
- langfuseUserId: resolvedConfig.observability.langfuse.userId,
304
- auditScope,
305
- permissionMode,
306
- languagePlugins,
307
- authorizationProvider: authorizationProvider ?? defaultAuthorizationProvider,
308
- authorizationMode,
309
- extensions: mergeResolvedExtensions(extensions.resolved, taskExtensions),
310
- onEvent,
311
- signal,
312
- });
313
- },
314
- });
315
- const sharedEventBus = createTaskEventBus();
316
- const checkpointService = new GitSnapshotCheckpointService(undefined, resolvedConfig.server?.acp?.checkpointManifest);
317
- await checkpointService.gc({
318
- repoPath: defaultRepoPath,
319
- olderThanMs: 1000 * 60 * 60 * 24 * 14,
320
- maxPerSession: 30,
321
- });
322
- const acpFacade = createInteractionFacade({
323
- executeTask: executor.execute,
324
- eventBus: sharedEventBus,
325
- });
326
- startAcpStdioServer((conn) => createAcpFormalAgent({
327
- conn,
328
- agentInfo: { name: 'salmon-loop', version: PACKAGE_VERSION },
329
- defaultModeId: 'autopilot',
330
- defaultPermissionPolicy: resolveDefaultAcpPermissionPolicy(defaultPermissionMode),
331
- checkpointReader: {
332
- listBySession: async ({ repoPath, sessionId, limit }) => await checkpointService.list({ repoPath, sessionId, limit }),
333
- getById: async ({ repoPath, checkpointId }) => (await checkpointService.loadWithStatus({ repoPath, checkpointId })).handle,
334
- probeById: async ({ repoPath, checkpointId }) => {
335
- const status = await checkpointService.loadWithStatus({ repoPath, checkpointId });
336
- return { valid: Boolean(status.handle), reason: status.reason };
337
- },
338
- },
292
+ const agentOptions = buildAcpAgentOptions({
339
293
  facade: acpFacade,
340
- sessionPersistencePath: getUserAcpSessionStorePath(),
294
+ checkpointService,
295
+ defaultPermissionMode,
296
+ sharedEventBus,
341
297
  sessionStorePolicy: resolvedConfig.server?.acp?.sessionStore,
342
- eventBus: sharedEventBus,
343
- }));
298
+ });
299
+ startAcpStdioServer((conn) => createAcpFormalAgent({ conn, ...agentOptions }));
344
300
  getLogger().info(text.cli.acpStdioStarted('n/a (stdio)'));
345
301
  registerServeShutdown({
346
302
  message: 'Received SIGINT, shutting down ACP server...',
@@ -63,7 +63,7 @@ export function encodeJsonResult(params) {
63
63
  const safeHint = overrides?.safeHint ?? params.loopResult.safeHint ?? params.loopResult.reason;
64
64
  const remediationSteps = overrides?.remediationSteps ?? params.loopResult.remediationSteps ?? [];
65
65
  const diagnosticCode = overrides?.diagnosticCode ?? params.loopResult.diagnosticCode ?? params.loopResult.reasonCode;
66
- const reason = overrides?.reason ?? safeHint;
66
+ const reason = overrides?.reason ?? params.loopResult.reason;
67
67
  const reasonCode = overrides?.reasonCode ?? params.loopResult.reasonCode;
68
68
  const errorCode = overrides?.errorCode ?? params.loopResult.errorCode;
69
69
  const warnings = normalizeHeadlessWarnings(params.warnings);
@@ -76,6 +76,7 @@ export function encodeStreamLoopEvent(params) {
76
76
  export function encodeStreamResult(params) {
77
77
  const exitCode = getStreamExitCode(params.loopResult);
78
78
  const warnings = normalizeHeadlessWarnings(params.warnings);
79
+ const safeHint = params.loopResult.safeHint ?? params.loopResult.reason;
79
80
  const patchArtifact = params.loopResult.benchmarkPatchArtifact
80
81
  ? {
81
82
  kind: params.loopResult.benchmarkPatchArtifact.kind,
@@ -102,10 +103,10 @@ export function encodeStreamResult(params) {
102
103
  timestamp: toIso(params.at),
103
104
  success: Boolean(params.loopResult.success),
104
105
  exit_code: exitCode,
105
- reason: params.loopResult.safeHint ?? params.loopResult.reason,
106
+ reason: params.loopResult.reason,
106
107
  reason_code: params.loopResult.reasonCode,
107
108
  diagnostic_code: params.loopResult.diagnosticCode ?? params.loopResult.reasonCode,
108
- safe_hint: params.loopResult.safeHint ?? params.loopResult.reason,
109
+ safe_hint: safeHint,
109
110
  remediation_steps: params.loopResult.remediationSteps ?? [],
110
111
  attempts: params.loopResult.attempts,
111
112
  changed_files: params.loopResult.changedFiles ?? [],
@@ -151,9 +151,10 @@ export async function createCliSlashRuntime(options) {
151
151
  dryRun: false,
152
152
  verbose: undefined,
153
153
  }, silentEmit);
154
+ let toolstack;
154
155
  try {
155
156
  await env.setup();
156
- const toolstack = await createStandardToolstack({
157
+ toolstack = await createStandardToolstack({
157
158
  repoRoot: env.activeRepoPath,
158
159
  persistenceRoot: options.repoRoot,
159
160
  worktreeRoot: env.activeRepoPath,
@@ -188,6 +189,9 @@ export async function createCliSlashRuntime(options) {
188
189
  return { kind: 'rewrite', input: res.injectedPrompt };
189
190
  }
190
191
  finally {
192
+ await toolstack
193
+ ?.dispose?.()
194
+ .catch((error) => logIgnoredError('[SlashRuntime] toolstack cleanup failed', error));
191
195
  await env
192
196
  .teardown()
193
197
  .catch((error) => logIgnoredError('[SlashRuntime] env teardown failed', error));
@@ -4,4 +4,5 @@ export const promises = fsPromises;
4
4
  export const syncFs = Object.assign({}, fs, fsPromises);
5
5
  export const { access, appendFile, chmod, chown, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, unlink, utimes, writeFile, } = fsPromises;
6
6
  export const { existsSync, readFileSync, realpathSync } = fs;
7
+ export const { constants } = fs;
7
8
  //# sourceMappingURL=node-fs.js.map
@@ -23,7 +23,7 @@ const DIFF_ARGS = [
23
23
  '.',
24
24
  ];
25
25
  function normalizeGitPatch(text) {
26
- const normalized = text.replace(/\r\n/g, '\n').replace(/\s+$/, '');
26
+ const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n+$/, '');
27
27
  return normalized.length > 0 ? `${normalized}\n` : '';
28
28
  }
29
29
  function splitLines(text) {
@@ -163,7 +163,7 @@ export class ContextService {
163
163
  const absoluteFile = defaultPathAdapter.resolve(repoPath, relativeFile);
164
164
  try {
165
165
  const stat = await this.fileAdapter.stat(absoluteFile);
166
- parts.push(`${relativeFile}:${stat.mtimeMs}:${stat.size}`);
166
+ parts.push(this.formatStatSignature(relativeFile, stat));
167
167
  }
168
168
  catch {
169
169
  parts.push(`${relativeFile}:missing`);
@@ -182,7 +182,7 @@ export class ContextService {
182
182
  const gitPath = defaultPathAdapter.resolve(repoPath, rel);
183
183
  try {
184
184
  const stat = await this.fileAdapter.stat(gitPath);
185
- parts.push(`${rel}:${stat.mtimeMs}:${stat.size}`);
185
+ parts.push(this.formatStatSignature(rel, stat));
186
186
  }
187
187
  catch {
188
188
  parts.push(`${rel}:missing`);
@@ -190,6 +190,9 @@ export class ContextService {
190
190
  }
191
191
  return parts;
192
192
  }
193
+ formatStatSignature(relativePath, stat) {
194
+ return `${relativePath}:${stat.mtimeMs}:${stat.ctimeMs ?? 0}:${stat.size}`;
195
+ }
193
196
  getEntryTimestamp(entry) {
194
197
  return entry.lastAccessedAt ?? entry.createdAt ?? 0;
195
198
  }