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.
- package/dist/cli/chat.js +1 -0
- package/dist/cli/commands/chat.js +17 -18
- package/dist/cli/commands/context.js +15 -3
- package/dist/cli/commands/help-format.js +12 -0
- package/dist/cli/commands/registry.js +4 -7
- package/dist/cli/commands/run/config-resolution.js +30 -24
- package/dist/cli/commands/run/handler.js +16 -17
- package/dist/cli/commands/run/loop-params.js +1 -0
- package/dist/cli/commands/run/parse-options.js +2 -2
- package/dist/cli/commands/run/validate-options.js +0 -5
- package/dist/cli/commands/run/verbose.js +2 -7
- package/dist/cli/commands/serve.js +29 -22
- package/dist/cli/locales/en.js +2 -0
- package/dist/cli/program-bootstrap.js +6 -1
- package/dist/cli/program-commands.js +4 -0
- package/dist/cli/program-options.js +1 -0
- package/dist/cli/slash/runtime.js +3 -3
- package/dist/cli/utils/output-format.js +6 -0
- package/dist/cli/utils/resolve-cli-config.js +98 -0
- package/dist/cli/utils/verbose-level.js +8 -0
- package/dist/core/config/load.js +22 -8
- package/dist/core/config/merge.js +27 -0
- package/dist/core/config/paths.js +24 -5
- package/dist/core/config/resolve.js +7 -5
- package/dist/core/config/validate.js +21 -0
- package/dist/core/facades/cli-command-chat.js +1 -1
- package/dist/core/facades/cli-context.js +1 -0
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +8 -0
- package/dist/core/grizzco/steps/preflight.js +4 -1
- package/dist/core/intent/chat-intent.js +0 -4
- package/dist/core/llm/ai-sdk/request-params.js +1 -1
- package/dist/core/protocols/a2a/sdk/executor.js +6 -5
- package/dist/core/protocols/acp/formal-agent.js +163 -20
- package/dist/core/protocols/acp/permission-provider.js +20 -0
- package/dist/core/protocols/shared/execution-request.js +24 -0
- package/dist/core/session/compression.js +4 -4
- package/dist/core/session/manager.js +3 -2
- package/dist/core/strata/layers/worktree.js +4 -4
- package/dist/core/tools/builtin/fs.js +4 -4
- package/dist/interfaces/cli/task-runner.js +4 -3
- package/dist/locales/en.js +52 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
110
|
+
throw new Error(text.errors.worktreePathMustBeInTempDir);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
if (isPathWithinDirectory(options.repoPath, worktreePath, { allowEqual: true })) {
|
|
114
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
7
|
-
});
|
|
7
|
+
instruction: input.instruction,
|
|
8
|
+
}));
|
|
8
9
|
},
|
|
9
10
|
};
|
|
10
11
|
}
|
package/dist/locales/en.js
CHANGED
|
@@ -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
|
+
"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.
|
|
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": "^
|
|
141
|
+
"marked": "^16.4.2",
|
|
142
142
|
"marked-terminal": "^7.3.0",
|
|
143
143
|
"openai": "^6.16.0",
|
|
144
144
|
"progress": "^2.0.3",
|