salmon-loop 0.3.0 → 0.3.2

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 (93) 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 +123 -154
  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/cli/ui/components/CommandSuggestionList.js +1 -1
  11. package/dist/core/adapters/fs/node-fs.js +1 -0
  12. package/dist/core/benchmark/patch-artifact.js +1 -1
  13. package/dist/core/context/service.js +36 -10
  14. package/dist/core/extensions/index.js +2 -35
  15. package/dist/core/extensions/redact.js +9 -3
  16. package/dist/core/extensions/schemas.js +2 -51
  17. package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
  18. package/dist/core/facades/cli-serve.js +0 -1
  19. package/dist/core/grizzco/dsl/strategies.js +1 -3
  20. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
  21. package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
  22. package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
  23. package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
  24. package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
  25. package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
  26. package/dist/core/grizzco/steps/apply.js +0 -7
  27. package/dist/core/grizzco/steps/autopilot.js +108 -6
  28. package/dist/core/grizzco/steps/preflight.js +10 -0
  29. package/dist/core/grizzco/steps/tool-runtime.js +1 -0
  30. package/dist/core/interaction/events/bus.js +14 -0
  31. package/dist/core/interaction/orchestration/facade.js +10 -0
  32. package/dist/core/mcp/bridge/index.js +4 -0
  33. package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
  34. package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
  35. package/dist/core/mcp/bridge/tool-bridge.js +303 -0
  36. package/dist/core/mcp/cache/resource-cache.js +41 -0
  37. package/dist/core/mcp/catalog/discovery.js +51 -0
  38. package/dist/core/mcp/catalog/notification-router.js +28 -0
  39. package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
  40. package/dist/core/mcp/catalog/resource-catalog.js +7 -0
  41. package/dist/core/mcp/catalog/tool-catalog.js +4 -0
  42. package/dist/core/mcp/client/connection-manager.js +239 -0
  43. package/dist/core/mcp/client/lifecycle.js +13 -0
  44. package/dist/core/mcp/client/transport-factory.js +168 -0
  45. package/dist/core/mcp/config/index.js +32 -0
  46. package/dist/core/mcp/config/schema-v2.js +129 -0
  47. package/dist/core/mcp/host/elicitation-provider.js +209 -0
  48. package/dist/core/mcp/host/roots-provider.js +70 -0
  49. package/dist/core/mcp/host/sampling-provider.js +170 -0
  50. package/dist/core/mcp/index.js +4 -0
  51. package/dist/core/mcp/observability/events.js +19 -0
  52. package/dist/core/mcp/policy/approval-policy.js +2 -0
  53. package/dist/core/mcp/policy/classifier.js +172 -0
  54. package/dist/core/mcp/policy/grants.js +356 -0
  55. package/dist/core/mcp/policy/uri-policy.js +60 -0
  56. package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
  57. package/dist/core/mcp/types.js +2 -0
  58. package/dist/core/protocols/a2a/agent-card.js +36 -11
  59. package/dist/core/protocols/a2a/sdk/executor.js +105 -36
  60. package/dist/core/protocols/a2a/sdk/server.js +1311 -3
  61. package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
  62. package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
  63. package/dist/core/protocols/acp/acp-types.js +17 -0
  64. package/dist/core/protocols/acp/formal-agent.js +271 -603
  65. package/dist/core/protocols/acp/handlers.js +3 -0
  66. package/dist/core/protocols/acp/permission-provider.js +11 -39
  67. package/dist/core/protocols/acp/stdio-server.js +20 -1
  68. package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
  69. package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
  70. package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
  71. package/dist/core/public-capabilities/projections.js +1 -0
  72. package/dist/core/runtime/agent-server-runtime.js +2 -3
  73. package/dist/core/runtime/spawn-command.js +8 -2
  74. package/dist/core/runtime/spawn-interactive.js +26 -0
  75. package/dist/core/session/manager.js +65 -35
  76. package/dist/core/tools/builtin/index.js +6 -1
  77. package/dist/core/tools/builtin/proposal.js +0 -7
  78. package/dist/core/tools/builtin/workspace.js +76 -0
  79. package/dist/core/tools/dispatcher.js +1 -0
  80. package/dist/core/tools/loader.js +92 -46
  81. package/dist/core/verification/runner.js +60 -31
  82. package/dist/core/workspace/capabilities.js +80 -0
  83. package/dist/locales/en.js +17 -3
  84. package/package.json +4 -2
  85. package/dist/core/protocols/a2a/mapper.js +0 -14
  86. package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
  87. package/dist/core/protocols/a2a/task-projection.js +0 -45
  88. package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
  89. package/dist/core/tools/mcp/client.js +0 -309
  90. package/dist/core/tools/mcp/loader.js +0 -110
  91. package/dist/core/tools/mcp/schema.js +0 -54
  92. package/dist/core/tools/mcp/streamable-http.js +0 -101
  93. 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,8 @@
1
+ import crypto from 'node:crypto';
1
2
  import { normalizePermissionMode } from '../../core/config/index.js';
2
3
  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';
4
+ import { readPlan } from '../../core/plan/index.js';
5
+ import { toA2APublicSkills } from '../../core/public-capabilities/projections.js';
5
6
  import { createTerminalAuthorizationProvider } from '../authorization/provider.js';
6
7
  import { text } from '../locales/index.js';
7
8
  import { getOptionValueSourceWithGlobalFallback } from '../utils/command-option-source.js';
@@ -41,7 +42,7 @@ function buildServeLoopExecutionDefaults(mode) {
41
42
  };
42
43
  }
43
44
  function registerServeShutdown({ message, closeRuntime, closeAcpStdio, }) {
44
- process.once('SIGINT', async () => {
45
+ const shutdown = async () => {
45
46
  getLogger().info(message);
46
47
  try {
47
48
  await closeRuntime?.();
@@ -50,39 +51,12 @@ function registerServeShutdown({ message, closeRuntime, closeAcpStdio, }) {
50
51
  closeAcpStdio?.();
51
52
  process.exit(0);
52
53
  }
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);
54
+ };
55
+ process.once('SIGINT', shutdown);
56
+ process.once('SIGTERM', shutdown);
70
57
  }
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;
58
+ async function resolveServeCommonSetup(params) {
59
+ const { command, allOptions, defaultRepoPath, resolvedConfig, auditScope } = params;
86
60
  const defaultFlowMode = 'autopilot';
87
61
  const defaultPermissionMode = resolveServePermissionMode({
88
62
  command,
@@ -90,19 +64,6 @@ export async function handleServeCommand(_options, command) {
90
64
  rawConfiguredPermissionMode: resolvedConfig.raw?.mode,
91
65
  flowMode: defaultFlowMode,
92
66
  });
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
67
  const languagePlugins = createPluginRegistry();
107
68
  setPluginRegistry(languagePlugins);
108
69
  setPromptRegistry(createPromptRegistry());
@@ -168,6 +129,87 @@ export async function handleServeCommand(_options, command) {
168
129
  executeTask: executor.execute,
169
130
  eventBus: sharedEventBus,
170
131
  });
132
+ return {
133
+ defaultPermissionMode,
134
+ executor,
135
+ sharedEventBus,
136
+ checkpointService,
137
+ acpFacade,
138
+ };
139
+ }
140
+ function buildAcpAgentOptions(params) {
141
+ return {
142
+ agentInfo: { name: 'salmon-loop', version: PACKAGE_VERSION },
143
+ defaultModeId: 'autopilot',
144
+ defaultPermissionPolicy: resolveDefaultAcpPermissionPolicy(params.defaultPermissionMode),
145
+ checkpointReader: {
146
+ listBySession: async ({ repoPath, sessionId, limit, }) => await params.checkpointService.list({ repoPath, sessionId, limit }),
147
+ getById: async ({ repoPath, checkpointId }) => (await params.checkpointService.loadWithStatus({ repoPath, checkpointId })).handle,
148
+ probeById: async ({ repoPath, checkpointId }) => {
149
+ const status = await params.checkpointService.loadWithStatus({ repoPath, checkpointId });
150
+ return { valid: Boolean(status.handle), reason: status.reason };
151
+ },
152
+ },
153
+ planReader: {
154
+ readBySession: async ({ repoPath, sessionId }) => await readPlan({ persistenceRoot: repoPath, sessionId }),
155
+ },
156
+ facade: params.facade,
157
+ sessionPersistencePath: getUserAcpSessionStorePath(),
158
+ sessionStorePolicy: params.sessionStorePolicy,
159
+ eventBus: params.sharedEventBus,
160
+ };
161
+ }
162
+ export function registerServeCommands(program) {
163
+ const serve = program
164
+ .command('serve')
165
+ .description(text.cli.serveDescription)
166
+ .option('--a2a-host <host>', text.cli.a2aHostOption)
167
+ .option('--a2a-port <port>', text.cli.a2aPortOption)
168
+ .option('--a2a-token <token>', text.cli.a2aTokenOption, (value, previous) => previous.concat([value]), [])
169
+ .option('--no-acp-stdio', text.cli.acpStdioDisableOption)
170
+ .option('--no-color', text.cli.noColorOption)
171
+ .action(handleServeCommand);
172
+ serve
173
+ .command('acp')
174
+ .description(text.cli.serveAcpDescription)
175
+ .option('--no-color', text.cli.noColorOption)
176
+ .action(handleServeAcpCommand);
177
+ }
178
+ export async function handleServeCommand(_options, command) {
179
+ const allOptions = command.optsWithGlobals();
180
+ const configResult = await resolveCliConfig({
181
+ repo: allOptions.repo,
182
+ cwd: process.cwd(),
183
+ configPath: allOptions.config,
184
+ enableConfigFile: allOptions.configFile !== false,
185
+ auditScope: allOptions.auditScope,
186
+ logMode: allOptions.logMode,
187
+ });
188
+ if (!configResult.ok) {
189
+ getLogger().error(configResult.message, true);
190
+ process.exit(1);
191
+ }
192
+ const { resolvedConfig, repoPath: defaultRepoPath } = configResult;
193
+ const serverConfig = resolvedConfig.server;
194
+ const rawA2aHost = allOptions.a2aHost ?? serverConfig?.a2a?.host;
195
+ const a2aHost = String(rawA2aHost ?? '127.0.0.1');
196
+ const rawA2aPort = allOptions.a2aPort ?? serverConfig?.a2a?.port;
197
+ const a2aPort = parsePort(rawA2aPort, 7431);
198
+ if (!Number.isFinite(a2aPort) || a2aPort <= 0) {
199
+ getLogger().error(text.cli.invalidA2APort(String(rawA2aPort ?? '')), true);
200
+ process.exit(1);
201
+ }
202
+ const acpStdioEnabled = allOptions.acpStdio !== false;
203
+ if (acpStdioEnabled) {
204
+ getLogger().setReporter(allOptions.color === false ? new StderrReporter() : new PlainReporter());
205
+ }
206
+ const { defaultPermissionMode, executor, sharedEventBus, checkpointService, acpFacade } = await resolveServeCommonSetup({
207
+ command,
208
+ allOptions,
209
+ defaultRepoPath,
210
+ resolvedConfig,
211
+ auditScope: configResult.auditScope,
212
+ });
171
213
  const tokens = Array.isArray(allOptions.a2aToken)
172
214
  ? allOptions.a2aToken.filter((token) => typeof token === 'string')
173
215
  : [];
@@ -181,40 +223,41 @@ export async function handleServeCommand(_options, command) {
181
223
  return;
182
224
  }
183
225
  const [scheme, token] = authHeader.split(' ');
184
- if (scheme?.toLowerCase() !== 'bearer' || !authTokens.includes(token)) {
226
+ let isAuthenticated = false;
227
+ if (scheme?.toLowerCase() === 'bearer' && token) {
228
+ const tokenBuffer = Buffer.from(token);
229
+ for (const authToken of authTokens) {
230
+ const authTokenBuffer = Buffer.from(authToken);
231
+ if (tokenBuffer.length === authTokenBuffer.length &&
232
+ crypto.timingSafeEqual(tokenBuffer, authTokenBuffer)) {
233
+ isAuthenticated = true;
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ if (!isAuthenticated) {
185
239
  res.status(401).json({ error: 'Unauthorized' });
186
240
  return;
187
241
  }
188
242
  next();
189
243
  }
190
244
  : undefined;
191
- const a2aPublicCapabilities = selectPublicCapabilitiesForSurface('a2a', buildPublicCapabilityRegistry());
192
- const a2aSkills = toA2APublicSkills(a2aPublicCapabilities);
245
+ const a2aSkills = toA2APublicSkills();
193
246
  const agentCard = buildA2AAgentCard({
194
247
  name: 'salmon-loop',
195
- url: `http://${a2aHost}:${a2aPort}`,
248
+ url: `http://${a2aHost}:${a2aPort}/a2a/jsonrpc`,
196
249
  capabilities: a2aSkills,
197
250
  security: authTokens.length > 0 ? [{ type: 'http', scheme: 'bearer' }] : [],
198
251
  });
199
252
  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
- },
253
+ const agentOptions = buildAcpAgentOptions({
213
254
  facade: acpFacade,
214
- sessionPersistencePath: getUserAcpSessionStorePath(),
255
+ checkpointService,
256
+ defaultPermissionMode,
257
+ sharedEventBus,
215
258
  sessionStorePolicy: resolvedConfig.server?.acp?.sessionStore,
216
- eventBus: sharedEventBus,
217
- }));
259
+ });
260
+ startAcpStdioServer((conn) => createAcpFormalAgent({ conn, ...agentOptions }));
218
261
  getLogger().info(text.cli.acpStdioStarted('n/a (stdio)'));
219
262
  }
220
263
  const runtime = createAgentServerRuntime({
@@ -250,97 +293,23 @@ export async function handleServeAcpCommand(_options, command) {
250
293
  getLogger().error(configResult.message, true);
251
294
  process.exit(1);
252
295
  }
253
- const { resolvedConfig, auditScope, repoPath: defaultRepoPath } = configResult;
254
- const defaultFlowMode = 'autopilot';
255
- const defaultPermissionMode = resolveServePermissionMode({
296
+ const { resolvedConfig, repoPath: defaultRepoPath } = configResult;
297
+ getLogger().setReporter(allOptions.color === false ? new StderrReporter() : new PlainReporter());
298
+ const { defaultPermissionMode, acpFacade, checkpointService, sharedEventBus } = await resolveServeCommonSetup({
256
299
  command,
257
300
  allOptions,
258
- rawConfiguredPermissionMode: resolvedConfig.raw?.mode,
259
- flowMode: defaultFlowMode,
260
- });
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,
301
+ defaultRepoPath,
302
+ resolvedConfig,
303
+ auditScope: configResult.auditScope,
321
304
  });
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
- },
305
+ const agentOptions = buildAcpAgentOptions({
339
306
  facade: acpFacade,
340
- sessionPersistencePath: getUserAcpSessionStorePath(),
307
+ checkpointService,
308
+ defaultPermissionMode,
309
+ sharedEventBus,
341
310
  sessionStorePolicy: resolvedConfig.server?.acp?.sessionStore,
342
- eventBus: sharedEventBus,
343
- }));
311
+ });
312
+ startAcpStdioServer((conn) => createAcpFormalAgent({ conn, ...agentOptions }));
344
313
  getLogger().info(text.cli.acpStdioStarted('n/a (stdio)'));
345
314
  registerServeShutdown({
346
315
  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));
@@ -14,7 +14,7 @@ export const CommandSuggestionList = ({ suggestions, selectedIndex, parentComman
14
14
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: COLORS.border.subtle, marginTop: 0, marginBottom: 0, paddingX: 0, width: "100%", children: [_jsx(Box, { flexDirection: "column", paddingY: 0, children: suggestions.map((item, index) => {
15
15
  const isSelected = index === selectedIndex;
16
16
  const hasSubcommands = !!item.command?.subcommands?.length;
17
- return (_jsxs(Box, { flexDirection: "row", paddingX: 1, children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: COLORS.semantic.salmon, children: isSelected ? ' ' : ' ' }) }), _jsx(Box, { width: maxNameLength + 4, children: _jsx(Text, { color: isSelected ? COLORS.semantic.cyan : COLORS.semantic.blue, bold: isSelected, children: item.name }) }), _jsx(Box, { width: 2, marginRight: 1, children: hasSubcommands ? _jsx(Text, { color: COLORS.text.muted, children: "\u203A" }) : _jsx(Text, { children: " " }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: isSelected ? COLORS.text.primary : COLORS.text.muted, wrap: "truncate", children: item.description }) })] }, `${item.name}-${index}`));
17
+ return (_jsxs(Box, { flexDirection: "row", paddingX: 1, children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: COLORS.semantic.salmon, children: isSelected ? ' ' : ' ' }) }), _jsx(Box, { width: maxNameLength + 4, children: _jsx(Text, { color: isSelected ? COLORS.semantic.cyan : COLORS.semantic.blue, bold: isSelected, children: item.name }) }), _jsx(Box, { width: 2, marginRight: 1, children: hasSubcommands ? _jsx(Text, { color: COLORS.text.muted, children: "\u203A" }) : _jsx(Text, { children: " " }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: isSelected ? COLORS.text.primary : COLORS.text.muted, wrap: "truncate", children: item.description }) })] }, `${item.name}-${index}`));
18
18
  }) }), suggestions[selectedIndex]?.command?.usage && (_jsxs(Box, { flexDirection: "row", borderStyle: "single", borderTop: true, borderLeft: false, borderRight: false, borderBottom: false, borderColor: COLORS.border.subtle, paddingX: 1, paddingY: 0, children: [_jsx(Text, { color: COLORS.semantic.blue, children: "TIP: " }), _jsx(Text, { color: COLORS.text.muted, children: "Usage: " }), _jsx(Text, { color: COLORS.text.primary, children: suggestions[selectedIndex].command?.usage })] })), _jsxs(Box, { flexDirection: "row", borderStyle: "single", borderTop: true, borderLeft: false, borderRight: false, borderBottom: false, borderColor: COLORS.border.subtle, paddingX: 1, paddingY: 0, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: COLORS.semantic.salmon, children: "\u2502 " }), _jsx(Text, { color: COLORS.semantic.blue, bold: true, children: title })] }), _jsx(Box, { children: _jsx(Text, { color: COLORS.text.muted, dimColor: true, children: "\u2191\u2193 nav \u00B7 \u23CE select \u00B7 esc close" }) })] })] }));
19
19
  };
20
20
  //# sourceMappingURL=CommandSuggestionList.js.map
@@ -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) {