salmon-loop 0.2.3 → 0.2.13

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 (42) hide show
  1. package/dist/cli/chat.js +1 -0
  2. package/dist/cli/commands/chat.js +17 -18
  3. package/dist/cli/commands/context.js +15 -3
  4. package/dist/cli/commands/help-format.js +12 -0
  5. package/dist/cli/commands/registry.js +4 -7
  6. package/dist/cli/commands/run/config-resolution.js +30 -24
  7. package/dist/cli/commands/run/handler.js +16 -17
  8. package/dist/cli/commands/run/loop-params.js +1 -0
  9. package/dist/cli/commands/run/parse-options.js +2 -2
  10. package/dist/cli/commands/run/validate-options.js +0 -5
  11. package/dist/cli/commands/run/verbose.js +2 -7
  12. package/dist/cli/commands/serve.js +29 -22
  13. package/dist/cli/locales/en.js +2 -0
  14. package/dist/cli/program-bootstrap.js +6 -1
  15. package/dist/cli/program-commands.js +4 -0
  16. package/dist/cli/program-options.js +1 -0
  17. package/dist/cli/slash/runtime.js +3 -3
  18. package/dist/cli/utils/output-format.js +6 -0
  19. package/dist/cli/utils/resolve-cli-config.js +98 -0
  20. package/dist/cli/utils/verbose-level.js +8 -0
  21. package/dist/core/config/load.js +22 -8
  22. package/dist/core/config/merge.js +27 -0
  23. package/dist/core/config/paths.js +24 -5
  24. package/dist/core/config/resolve.js +7 -5
  25. package/dist/core/config/validate.js +21 -0
  26. package/dist/core/facades/cli-command-chat.js +1 -1
  27. package/dist/core/facades/cli-context.js +1 -0
  28. package/dist/core/grizzco/engine/transaction/transaction-runner.js +8 -0
  29. package/dist/core/grizzco/steps/preflight.js +4 -1
  30. package/dist/core/intent/chat-intent.js +0 -4
  31. package/dist/core/llm/ai-sdk/request-params.js +1 -1
  32. package/dist/core/protocols/a2a/sdk/executor.js +6 -5
  33. package/dist/core/protocols/acp/formal-agent.js +163 -20
  34. package/dist/core/protocols/acp/permission-provider.js +20 -0
  35. package/dist/core/protocols/shared/execution-request.js +24 -0
  36. package/dist/core/session/compression.js +4 -4
  37. package/dist/core/session/manager.js +3 -2
  38. package/dist/core/strata/layers/worktree.js +4 -4
  39. package/dist/core/tools/builtin/fs.js +4 -4
  40. package/dist/interfaces/cli/task-runner.js +4 -3
  41. package/dist/locales/en.js +52 -0
  42. package/package.json +3 -3
@@ -60,6 +60,23 @@ function toToolCallUpdate(request) {
60
60
  };
61
61
  }
62
62
  export function createAcpToolAuthorizationProvider(params) {
63
+ const inProgressEmitted = new Set();
64
+ const emitInProgressBestEffort = async (toolCallId) => {
65
+ if (inProgressEmitted.has(toolCallId))
66
+ return;
67
+ inProgressEmitted.add(toolCallId);
68
+ try {
69
+ const update = {
70
+ sessionUpdate: 'tool_call_update',
71
+ toolCallId,
72
+ status: 'in_progress',
73
+ };
74
+ await params.conn.sessionUpdate({ sessionId: params.sessionId, update });
75
+ }
76
+ catch {
77
+ // Best-effort: do not block authorization if the client can't accept updates.
78
+ }
79
+ };
63
80
  return {
64
81
  async requestAuthorization(request) {
65
82
  const enforceClientCapabilities = params.enforceClientCapabilities ?? true;
@@ -86,6 +103,7 @@ export function createAcpToolAuthorizationProvider(params) {
86
103
  }
87
104
  const permissionPolicy = params.getPermissionPolicy?.() ?? 'ask';
88
105
  if (permissionPolicy === 'allow_all') {
106
+ await emitInProgressBestEffort(request.id);
89
107
  return { outcome: 'allow_session', source: 'auto', reason: 'session_mode:yolo' };
90
108
  }
91
109
  const hasSideEffects = request.sideEffects.some((effect) => effect !== 'fs_read');
@@ -103,8 +121,10 @@ export function createAcpToolAuthorizationProvider(params) {
103
121
  }
104
122
  switch (response.outcome.optionId) {
105
123
  case 'allow_once':
124
+ await emitInProgressBestEffort(request.id);
106
125
  return { outcome: 'allow_once', source: 'user' };
107
126
  case 'allow_always':
127
+ await emitInProgressBestEffort(request.id);
108
128
  return { outcome: 'allow_session', source: 'user' };
109
129
  case 'reject_once':
110
130
  case 'reject_always':
@@ -0,0 +1,24 @@
1
+ export function normalizeInstructionText(instruction, options) {
2
+ const normalized = instruction.replace(/\r\n?/g, '\n').trim();
3
+ if (normalized.length > 0)
4
+ return normalized;
5
+ return options?.fallbackInstruction ?? '';
6
+ }
7
+ export function buildInstructionFromParts(parts, options) {
8
+ return normalizeInstructionText(parts.join('\n'), options);
9
+ }
10
+ export function buildCanonicalExecutionRequest(input) {
11
+ const request = {
12
+ instruction: normalizeInstructionText(input.instruction, {
13
+ fallbackInstruction: input.fallbackInstruction,
14
+ }),
15
+ checkpointSessionId: input.checkpointSessionId,
16
+ repoPath: input.repoPath,
17
+ };
18
+ return {
19
+ capability: input.capability,
20
+ request,
21
+ taskId: input.taskId,
22
+ };
23
+ }
24
+ //# sourceMappingURL=execution-request.js.map
@@ -178,7 +178,7 @@ export class SessionCompressor {
178
178
  return stats;
179
179
  }
180
180
  determineOutcome(iteration) {
181
- // 简化的结果判断逻辑
181
+ // Simplified result determination logic
182
182
  if (iteration.result?.success === true)
183
183
  return 'success';
184
184
  if (iteration.result?.success === false)
@@ -186,7 +186,7 @@ export class SessionCompressor {
186
186
  return 'partial';
187
187
  }
188
188
  generateIterationSummary(iteration) {
189
- // 生成迭代的简短摘要
189
+ // Generate brief iteration summary
190
190
  const result = iteration.result;
191
191
  if (!result)
192
192
  return 'No result data';
@@ -198,7 +198,7 @@ export class SessionCompressor {
198
198
  }
199
199
  }
200
200
  countErrors(iteration) {
201
- // 计算迭代中的错误数量
201
+ // Count errors in iteration
202
202
  const result = iteration.result;
203
203
  if (!result)
204
204
  return 0;
@@ -313,7 +313,7 @@ export class CompressedSessionStore {
313
313
  }
314
314
  async readFile(path) {
315
315
  const data = await this.fileAdapter.readFile(path);
316
- // FileAdapter返回base64字符串,需要解码为Uint8Array
316
+ // FileAdapter returns base64 string, need to decode to Uint8Array
317
317
  if (typeof data === 'string') {
318
318
  return new Uint8Array(Buffer.from(data, 'base64'));
319
319
  }
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from 'crypto';
2
2
  import { join } from 'path';
3
3
  import { FileAdapter } from '../adapters/fs/index.js';
4
+ import { getLogger } from '../observability/logger.js';
4
5
  import { SessionCompressor, CompressedSessionStore } from './compression.js';
5
6
  import { SessionPruningEngine } from './pruning-strategy.js';
6
7
  /**
@@ -258,7 +259,7 @@ export class ChatSessionManager {
258
259
  }
259
260
  catch (error) {
260
261
  // Skip corrupted session files
261
- console.warn(`Failed to load session file ${file}:`, error);
262
+ getLogger().warn(`Failed to load session file ${file}: ${error}`);
262
263
  }
263
264
  }
264
265
  return sessions;
@@ -272,7 +273,7 @@ export class ChatSessionManager {
272
273
  await this.fileAdapter.deleteFile(filePath);
273
274
  }
274
275
  catch (error) {
275
- console.warn(`Failed to delete session ${sessionId}:`, error);
276
+ getLogger().warn(`Failed to delete session ${sessionId}: ${error}`);
276
277
  }
277
278
  }
278
279
  /**
@@ -101,17 +101,17 @@ export class WorkspaceManager {
101
101
  if (environmentMode === 'parity') {
102
102
  const parityRoot = worktreeRootImpl.normalize(resolveParityWorktreeRoot(options.repoPath));
103
103
  if (!isPathWithinDirectory(parityRoot, normalizedWorktreePath, { allowEqual: false })) {
104
- throw new Error('Worktree path must be under parity worktree root');
104
+ throw new Error(text.errors.worktreePathMustBeUnderParityRoot);
105
105
  }
106
106
  }
107
107
  else {
108
108
  const tmpDir = worktreeRootImpl.normalize(tmpdir());
109
109
  if (!isPathWithinDirectory(tmpDir, normalizedWorktreePath, { allowEqual: false })) {
110
- throw new Error('Worktree path must be in system temp directory');
110
+ throw new Error(text.errors.worktreePathMustBeInTempDir);
111
111
  }
112
112
  }
113
113
  if (isPathWithinDirectory(options.repoPath, worktreePath, { allowEqual: true })) {
114
- throw new Error('Worktree path must not be inside repo path');
114
+ throw new Error(text.errors.worktreePathMustNotBeInsideRepo);
115
115
  }
116
116
  // Use GitAdapter for worktree creation
117
117
  await git.query(['worktree', 'add', '--quiet', '--detach', worktreePath, baseRef.trim()]);
@@ -225,7 +225,7 @@ export class WorkspaceManager {
225
225
  }
226
226
  if (!removed) {
227
227
  if (!isManagedWorktreePath(workspace.baseRepoPath, workspace.workPath)) {
228
- throw new Error('Worktree path not in managed roots, refusing to delete');
228
+ throw new Error(text.errors.worktreePathNotInManagedRoots);
229
229
  }
230
230
  await rm(workspace.workPath, {
231
231
  recursive: true,
@@ -172,13 +172,13 @@ function toRepoRelativeChildPath(dir, name) {
172
172
  export async function executeFsList(input, ctx) {
173
173
  const { path: dir, includeHidden, maxEntries } = input;
174
174
  if (isAbsolute(dir)) {
175
- throw new Error('Access denied: Path is outside of repository root.');
175
+ throw new Error(text.errors.pathOutsideRepo);
176
176
  }
177
177
  const absoluteRoot = resolve(ctx.repoRoot);
178
178
  const absolutePath = resolve(absoluteRoot, dir);
179
179
  const relPath = relative(absoluteRoot, absolutePath);
180
180
  if (relPath.startsWith('..') || isAbsolute(relPath)) {
181
- throw new Error('Access denied: Path is outside of repository root.');
181
+ throw new Error(text.errors.pathOutsideRepo);
182
182
  }
183
183
  try {
184
184
  const dirents = await readdir(absolutePath, { withFileTypes: true });
@@ -217,7 +217,7 @@ export async function executeFsList(input, ctx) {
217
217
  export async function executeFsReadFile(input, ctx) {
218
218
  const { file } = input;
219
219
  if (isAbsolute(file)) {
220
- throw new Error('Access denied: Path is outside of repository root.');
220
+ throw new Error(text.errors.pathOutsideRepo);
221
221
  }
222
222
  // CRITICAL SAFETY: Path traversal check using relative path resolution
223
223
  // We resolve to absolute paths to handle '.' and '..' correctly
@@ -226,7 +226,7 @@ export async function executeFsReadFile(input, ctx) {
226
226
  const absolutePath = resolve(absoluteRoot, file);
227
227
  const relPath = relative(absoluteRoot, absolutePath);
228
228
  if (relPath.startsWith('..') || isAbsolute(relPath)) {
229
- throw new Error('Access denied: Path is outside of repository root.');
229
+ throw new Error(text.errors.pathOutsideRepo);
230
230
  }
231
231
  try {
232
232
  const fileStat = await stat(absolutePath);
@@ -1,10 +1,11 @@
1
+ import { buildCanonicalExecutionRequest } from '../../core/protocols/shared/execution-request.js';
1
2
  export function createCliTaskRunner(deps) {
2
3
  return {
3
4
  async run(input) {
4
- return deps.facade.createTask({
5
+ return deps.facade.createTask(buildCanonicalExecutionRequest({
5
6
  capability: input.capability,
6
- request: { instruction: input.instruction },
7
- });
7
+ instruction: input.instruction,
8
+ }));
8
9
  },
9
10
  };
10
11
  }
@@ -86,6 +86,55 @@ export const en = {
86
86
  allowlistAtomicWriteBackupFailed: 'Allowlist atomic write backup failed.',
87
87
  allowlistAtomicRestoreFailed: 'Allowlist atomic restore failed.',
88
88
  allowlistPathBlocked: 'Allowlist blocked the requested path.',
89
+ // File system security errors
90
+ pathOutsideRepo: 'Access denied: Path is outside of repository root.',
91
+ // Worktree errors
92
+ worktreePathMustBeUnderParityRoot: 'Worktree path must be under parity worktree root',
93
+ worktreePathMustBeInTempDir: 'Worktree path must be in system temp directory',
94
+ worktreePathMustNotBeInsideRepo: 'Worktree path must not be inside repo path',
95
+ worktreePathNotInManagedRoots: 'Worktree path not in managed roots, refusing to delete',
96
+ // Shadow driver errors
97
+ aggressiveStrategyLinuxOnly: 'AGGRESSIVE strategy only supported on Linux',
98
+ commandTimedOut: 'Command timed out',
99
+ // Lock errors
100
+ failedToAcquireLock: 'Failed to acquire lock due to concurrent lock updates',
101
+ lockAcquisitionAborted: 'Lock acquisition aborted',
102
+ // Checkpoint errors
103
+ workspaceDirtyUseForce: 'Workspace is dirty. Use --force to overwrite.',
104
+ // Session errors
105
+ noActiveSession: 'No active session',
106
+ // Runtime errors
107
+ bunRuntimeNotAvailable: 'Bun runtime is not available',
108
+ runtimeAlreadyStarted: 'Runtime already started',
109
+ // Protocol errors
110
+ acpSessionPersistLockTimeout: 'ACP_SESSION_PERSIST_LOCK_TIMEOUT',
111
+ // Registry errors
112
+ subAgentRegistryNotInitialized: 'SubAgentRegistry is not initialized. Call setSubAgentRegistry() at startup.',
113
+ promptRegistryNotInitialized: 'PromptRegistry is not initialized. Call setPromptRegistry() at startup.',
114
+ pluginRegistryNotInitialized: 'PluginRegistry is not initialized. Call setPluginRegistry() at startup.',
115
+ monitorNotInitialized: 'Monitor is not initialized. Call setMonitor(createMonitor()) at startup.',
116
+ loggerNotInitialized: 'Logger is not initialized. Call setLogger(createLogger()) at startup.',
117
+ // Plan storage errors
118
+ invalidSessionId: 'Invalid sessionId (expected 6-64 chars of [a-zA-Z0-9_-]).',
119
+ invalidStepId: 'Invalid stepId.',
120
+ // Audit errors
121
+ invalidAuditFile: 'Invalid audit file: context.eventsRef.path is required',
122
+ // LLM errors
123
+ operationAborted: 'Operation aborted',
124
+ emptyLlmResponse: 'Empty LLM response',
125
+ streamAborted: 'Stream aborted',
126
+ // Worker errors
127
+ repoRootRequired: 'repoRoot context is required for union-merge reading',
128
+ patchContentEmpty: 'Patch content is empty',
129
+ // Transaction errors
130
+ runtimeEnvironmentMissingWorkspace: 'Runtime environment missing workspace after setup',
131
+ executionTerminatedWithoutReport: 'SalmonLoop execution terminated without a FlowReport',
132
+ // Decision engine errors
133
+ planBuilderContextNotBound: 'PlanBuilder: context not bound',
134
+ // Pipeline errors
135
+ operationCancelledByUser: 'Operation cancelled by user',
136
+ // Sub-agent errors
137
+ stopRequestedBeforeLaunch: 'Stop requested before launching Smallfry',
89
138
  },
90
139
  acp: {
91
140
  slashHelpDescription: 'Show available ACP slash commands',
@@ -617,6 +666,9 @@ Please return the patch in PURE unified diff format:`;
617
666
  findingItem: (index, summary, confidence, uncertainty) => `Finding ${index}: ${summary}${typeof confidence === 'number' ? ` (confidence: ${confidence})` : ''}${uncertainty ? ` [uncertainty: ${uncertainty}]` : ''}`,
618
667
  },
619
668
  },
669
+ intent: {
670
+ researchKeywords: ['deep research', 'research', 'investigate', 'investigation'],
671
+ },
620
672
  skills: {
621
673
  maxRetriesExceeded: (id) => `Max retries exceeded for skill: ${id}. Possible circular dependency in dynamic data.`,
622
674
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salmon-loop",
3
- "version": "0.2.3",
3
+ "version": "0.2.13",
4
4
  "description": "A chat-first coding agent CLI for safe, reviewable repository changes",
5
5
  "type": "module",
6
6
  "bin": {
@@ -107,7 +107,7 @@
107
107
  "eslint-import-resolver-typescript": "^4.4.4",
108
108
  "eslint-plugin-import": "^2.32.0",
109
109
  "eslint-plugin-prettier": "^5.5.5",
110
- "fast-check": "^4.5.3",
110
+ "fast-check": "^4.6.0",
111
111
  "jsdom": "^27.4.0",
112
112
  "prettier": "^3.4.2",
113
113
  "react-devtools-core": "^7.0.1",
@@ -138,7 +138,7 @@
138
138
  "ink-gradient": "^3.0.0",
139
139
  "ink-spinner": "^5.0.0",
140
140
  "ink-text-input": "^6.0.0",
141
- "marked": "^17.0.1",
141
+ "marked": "^16.4.2",
142
142
  "marked-terminal": "^7.3.0",
143
143
  "openai": "^6.16.0",
144
144
  "progress": "^2.0.3",