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.
- package/dist/cli/authorization/non-interactive.js +7 -21
- package/dist/cli/commands/chat.js +1 -1
- package/dist/cli/commands/parallel.js +46 -41
- package/dist/cli/commands/run/assistant-message.js +3 -0
- package/dist/cli/commands/run/handler.js +2 -1
- package/dist/cli/commands/serve.js +123 -154
- package/dist/cli/headless/json-protocol.js +1 -1
- package/dist/cli/headless/stream-json-protocol.js +3 -2
- package/dist/cli/slash/runtime.js +5 -1
- package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
- package/dist/core/adapters/fs/node-fs.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +1 -1
- package/dist/core/context/service.js +36 -10
- package/dist/core/extensions/index.js +2 -35
- package/dist/core/extensions/redact.js +9 -3
- package/dist/core/extensions/schemas.js +2 -51
- package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
- package/dist/core/facades/cli-serve.js +0 -1
- package/dist/core/grizzco/dsl/strategies.js +1 -3
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
- package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
- package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
- package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
- package/dist/core/grizzco/steps/apply.js +0 -7
- package/dist/core/grizzco/steps/autopilot.js +108 -6
- package/dist/core/grizzco/steps/preflight.js +10 -0
- package/dist/core/grizzco/steps/tool-runtime.js +1 -0
- package/dist/core/interaction/events/bus.js +14 -0
- package/dist/core/interaction/orchestration/facade.js +10 -0
- package/dist/core/mcp/bridge/index.js +4 -0
- package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
- package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
- package/dist/core/mcp/bridge/tool-bridge.js +303 -0
- package/dist/core/mcp/cache/resource-cache.js +41 -0
- package/dist/core/mcp/catalog/discovery.js +51 -0
- package/dist/core/mcp/catalog/notification-router.js +28 -0
- package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
- package/dist/core/mcp/catalog/resource-catalog.js +7 -0
- package/dist/core/mcp/catalog/tool-catalog.js +4 -0
- package/dist/core/mcp/client/connection-manager.js +239 -0
- package/dist/core/mcp/client/lifecycle.js +13 -0
- package/dist/core/mcp/client/transport-factory.js +168 -0
- package/dist/core/mcp/config/index.js +32 -0
- package/dist/core/mcp/config/schema-v2.js +129 -0
- package/dist/core/mcp/host/elicitation-provider.js +209 -0
- package/dist/core/mcp/host/roots-provider.js +70 -0
- package/dist/core/mcp/host/sampling-provider.js +170 -0
- package/dist/core/mcp/index.js +4 -0
- package/dist/core/mcp/observability/events.js +19 -0
- package/dist/core/mcp/policy/approval-policy.js +2 -0
- package/dist/core/mcp/policy/classifier.js +172 -0
- package/dist/core/mcp/policy/grants.js +356 -0
- package/dist/core/mcp/policy/uri-policy.js +60 -0
- package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
- package/dist/core/mcp/types.js +2 -0
- package/dist/core/protocols/a2a/agent-card.js +36 -11
- package/dist/core/protocols/a2a/sdk/executor.js +105 -36
- package/dist/core/protocols/a2a/sdk/server.js +1311 -3
- package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
- package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
- package/dist/core/protocols/acp/acp-types.js +17 -0
- package/dist/core/protocols/acp/formal-agent.js +271 -603
- package/dist/core/protocols/acp/handlers.js +3 -0
- package/dist/core/protocols/acp/permission-provider.js +11 -39
- package/dist/core/protocols/acp/stdio-server.js +20 -1
- package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
- package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
- package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
- package/dist/core/public-capabilities/projections.js +1 -0
- package/dist/core/runtime/agent-server-runtime.js +2 -3
- package/dist/core/runtime/spawn-command.js +8 -2
- package/dist/core/runtime/spawn-interactive.js +26 -0
- package/dist/core/session/manager.js +65 -35
- package/dist/core/tools/builtin/index.js +6 -1
- package/dist/core/tools/builtin/proposal.js +0 -7
- package/dist/core/tools/builtin/workspace.js +76 -0
- package/dist/core/tools/dispatcher.js +1 -0
- package/dist/core/tools/loader.js +92 -46
- package/dist/core/verification/runner.js +60 -31
- package/dist/core/workspace/capabilities.js +80 -0
- package/dist/locales/en.js +17 -3
- package/package.json +4 -2
- package/dist/core/protocols/a2a/mapper.js +0 -14
- package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
- package/dist/core/protocols/a2a/task-projection.js +0 -45
- package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
- package/dist/core/tools/mcp/client.js +0 -309
- package/dist/core/tools/mcp/loader.js +0 -110
- package/dist/core/tools/mcp/schema.js +0 -54
- package/dist/core/tools/mcp/streamable-http.js +0 -101
- package/dist/core/tools/mcp/types.js +0 -26
|
@@ -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(
|
|
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(
|
|
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
|
}
|
|
@@ -211,25 +214,48 @@ export class ContextService {
|
|
|
211
214
|
return true;
|
|
212
215
|
}
|
|
213
216
|
async evictExpiredEntries() {
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
const entries = Array.from(await this.cacheStore.entries());
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
const expiredEntries = entries.filter(([, entry]) => {
|
|
220
|
+
const last = this.getEntryTimestamp(entry);
|
|
221
|
+
return last && now - last > this.cacheTtlMs;
|
|
222
|
+
});
|
|
223
|
+
for (let i = 0; i < expiredEntries.length; i += 10) {
|
|
224
|
+
const chunk = expiredEntries.slice(i, i + 10);
|
|
225
|
+
await Promise.all(chunk.map(([key, entry]) => this.isExpired(key, entry)));
|
|
216
226
|
}
|
|
217
227
|
}
|
|
218
228
|
async evictLruIfNeeded() {
|
|
219
|
-
|
|
229
|
+
const size = await this.cacheStore.size();
|
|
230
|
+
if (size <= this.cacheMaxEntries)
|
|
231
|
+
return;
|
|
232
|
+
const excess = size - this.cacheMaxEntries;
|
|
233
|
+
const entries = Array.from(await this.cacheStore.entries());
|
|
234
|
+
if (excess === 1) {
|
|
220
235
|
let victimKey;
|
|
221
236
|
let victimTs = Number.POSITIVE_INFINITY;
|
|
222
|
-
for (const [key, entry] of
|
|
237
|
+
for (const [key, entry] of entries) {
|
|
223
238
|
const ts = this.getEntryTimestamp(entry);
|
|
224
239
|
if (ts < victimTs) {
|
|
225
240
|
victimTs = ts;
|
|
226
241
|
victimKey = key;
|
|
227
242
|
}
|
|
228
243
|
}
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
if (victimKey) {
|
|
245
|
+
await this.cacheStore.delete(victimKey);
|
|
246
|
+
this.cacheMetrics.evictions += 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
entries.sort((a, b) => (this.getEntryTimestamp(a[1]) || 0) - (this.getEntryTimestamp(b[1]) || 0));
|
|
251
|
+
const victims = entries.slice(0, excess);
|
|
252
|
+
for (let i = 0; i < victims.length; i += 10) {
|
|
253
|
+
const chunk = victims.slice(i, i + 10);
|
|
254
|
+
await Promise.all(chunk.map(async ([key]) => {
|
|
255
|
+
await this.cacheStore.delete(key);
|
|
256
|
+
this.cacheMetrics.evictions += 1;
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
233
259
|
}
|
|
234
260
|
}
|
|
235
261
|
async getCacheStats() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { buildResolvedMcpServersV2 } from '../mcp/config/index.js';
|
|
2
3
|
import { getLogger } from '../observability/logger.js';
|
|
3
4
|
import { loadConfig } from './load.js';
|
|
4
5
|
import { mergeScopedEntries } from './merge.js';
|
|
@@ -14,40 +15,6 @@ function resolvePathForScope(value, scope, repoRoot) {
|
|
|
14
15
|
const expanded = expandHome(value);
|
|
15
16
|
return scope === 'repo' ? resolveRepoRelative(repoRoot, expanded) : resolveUserRelative(expanded);
|
|
16
17
|
}
|
|
17
|
-
function buildResolvedServers(entries, repoRoot) {
|
|
18
|
-
return entries.map((entry) => {
|
|
19
|
-
const scope = entry.scope;
|
|
20
|
-
const source = entry.entry;
|
|
21
|
-
const enabled = source.enabled ?? defaultEnabled(scope);
|
|
22
|
-
if (source.url) {
|
|
23
|
-
return {
|
|
24
|
-
name: entry.key,
|
|
25
|
-
enabled,
|
|
26
|
-
transport: 'http',
|
|
27
|
-
url: source.url,
|
|
28
|
-
headers: source.headers ?? {},
|
|
29
|
-
allowTools: source.allow?.tools ?? [],
|
|
30
|
-
allowResources: source.allow?.resources ?? [],
|
|
31
|
-
scope,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
if (!source.command) {
|
|
35
|
-
throw new Error(`Invalid MCP server entry ${entry.key}: missing "command" or "url"`);
|
|
36
|
-
}
|
|
37
|
-
return {
|
|
38
|
-
name: entry.key,
|
|
39
|
-
enabled,
|
|
40
|
-
transport: 'stdio',
|
|
41
|
-
command: source.command,
|
|
42
|
-
args: source.args ?? [],
|
|
43
|
-
env: source.env ?? {},
|
|
44
|
-
cwd: resolvePathForScope(source.cwd, scope, repoRoot),
|
|
45
|
-
allowTools: source.allow?.tools ?? [],
|
|
46
|
-
allowResources: source.allow?.resources ?? [],
|
|
47
|
-
scope,
|
|
48
|
-
};
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
18
|
function buildResolvedPlugins(entries, repoRoot) {
|
|
52
19
|
return entries.map((entry) => {
|
|
53
20
|
const scope = entry.scope;
|
|
@@ -117,7 +84,7 @@ export async function resolveExtensions(options) {
|
|
|
117
84
|
const mergedServers = mergeScopedEntries(userMcp?.config.servers, repoMcp?.config.servers);
|
|
118
85
|
const mergedPlugins = mergeScopedEntries(userTools?.config.plugins, repoTools?.config.plugins);
|
|
119
86
|
const resolved = {
|
|
120
|
-
mcpServers:
|
|
87
|
+
mcpServers: buildResolvedMcpServersV2(mergedServers, repoRoot),
|
|
121
88
|
toolPlugins: buildResolvedPlugins(mergedPlugins, repoRoot),
|
|
122
89
|
skillDiscovery: buildResolvedSkills(userSkills?.config, repoSkills?.config, repoRoot),
|
|
123
90
|
};
|
|
@@ -17,15 +17,21 @@ function redactHeaders(headers) {
|
|
|
17
17
|
return output;
|
|
18
18
|
}
|
|
19
19
|
function redactServer(server) {
|
|
20
|
-
if (server.transport === 'http') {
|
|
20
|
+
if (server.transport.type === 'http') {
|
|
21
21
|
return {
|
|
22
22
|
...server,
|
|
23
|
-
|
|
23
|
+
transport: {
|
|
24
|
+
...server.transport,
|
|
25
|
+
headers: redactHeaders(server.transport.headers || {}),
|
|
26
|
+
},
|
|
24
27
|
};
|
|
25
28
|
}
|
|
26
29
|
return {
|
|
27
30
|
...server,
|
|
28
|
-
|
|
31
|
+
transport: {
|
|
32
|
+
...server.transport,
|
|
33
|
+
env: redactEnv(server.transport.env || {}),
|
|
34
|
+
},
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
export function redactExtensions(extensions) {
|
|
@@ -1,55 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
resources: z.array(z.string()).optional(),
|
|
5
|
-
});
|
|
6
|
-
const mcpServerSchema = z
|
|
7
|
-
.object({
|
|
8
|
-
enabled: z.boolean().optional(),
|
|
9
|
-
command: z.string().optional(),
|
|
10
|
-
url: z.string().url().optional(),
|
|
11
|
-
args: z.array(z.string()).optional(),
|
|
12
|
-
env: z.record(z.string(), z.string()).optional(),
|
|
13
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
14
|
-
cwd: z.string().optional(),
|
|
15
|
-
allow: mcpAllowSchema.optional(),
|
|
16
|
-
})
|
|
17
|
-
.superRefine((value, ctx) => {
|
|
18
|
-
const hasCommand = Boolean(value.command);
|
|
19
|
-
const hasUrl = Boolean(value.url);
|
|
20
|
-
if (hasCommand === hasUrl) {
|
|
21
|
-
ctx.addIssue({
|
|
22
|
-
code: z.ZodIssueCode.custom,
|
|
23
|
-
message: 'MCP server entry must include exactly one of "command" or "url".',
|
|
24
|
-
path: ['command'],
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
if (hasUrl && value.args && value.args.length > 0) {
|
|
28
|
-
ctx.addIssue({
|
|
29
|
-
code: z.ZodIssueCode.custom,
|
|
30
|
-
message: '"args" is only valid for stdio MCP servers ("command" transport).',
|
|
31
|
-
path: ['args'],
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
if (hasUrl && value.cwd) {
|
|
35
|
-
ctx.addIssue({
|
|
36
|
-
code: z.ZodIssueCode.custom,
|
|
37
|
-
message: '"cwd" is only valid for stdio MCP servers ("command" transport).',
|
|
38
|
-
path: ['cwd'],
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
if (hasUrl && value.env && Object.keys(value.env).length > 0) {
|
|
42
|
-
ctx.addIssue({
|
|
43
|
-
code: z.ZodIssueCode.custom,
|
|
44
|
-
message: '"env" is only valid for stdio MCP servers ("command" transport).',
|
|
45
|
-
path: ['env'],
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
export const McpConfigSchema = z.object({
|
|
50
|
-
version: z.literal(1),
|
|
51
|
-
servers: z.record(z.string(), mcpServerSchema).optional().default({}),
|
|
52
|
-
});
|
|
2
|
+
import { McpConfigV2Schema } from '../mcp/config/schema-v2.js';
|
|
3
|
+
export const McpConfigSchema = McpConfigV2Schema;
|
|
53
4
|
const toolPluginSchema = z.object({
|
|
54
5
|
enabled: z.boolean().optional(),
|
|
55
6
|
path: z.string(),
|
|
@@ -10,7 +10,6 @@ export { PluginLoader } from '../plugin/loader.js';
|
|
|
10
10
|
export { clearPluginRegistry, createPluginRegistry, setPluginRegistry, } from '../plugin/registry.js';
|
|
11
11
|
export { clearPromptRegistry, createPromptRegistry, setPromptRegistry, } from '../prompts/registry.js';
|
|
12
12
|
export { buildA2AAgentCard } from '../protocols/a2a/agent-card.js';
|
|
13
|
-
export { buildA2AFlowSkills } from '../protocols/shared/flow-mode-mapping.js';
|
|
14
13
|
export { createAcpFormalAgent } from '../protocols/acp/formal-agent.js';
|
|
15
14
|
export { startAcpStdioServer } from '../protocols/acp/stdio-server.js';
|
|
16
15
|
export { createAgentServerRuntime } from '../runtime/agent-server-runtime.js';
|
|
@@ -15,9 +15,7 @@ export const SafetyChecks = (engine) => {
|
|
|
15
15
|
.require((c) => !c.file.isIgnored || c.options.force, 'Refusing to modify ignored file without --force')
|
|
16
16
|
.phase('Lock Check')
|
|
17
17
|
.requireData('remote_lock')
|
|
18
|
-
.require((c) => !c.data?.remote_lock?.isLocked, text.grizzco.remoteLocked)
|
|
19
|
-
.requireData('git_config')
|
|
20
|
-
.require((c) => !!(c.data?.git_config?.user?.name && c.data?.git_config?.user?.email), text.grizzco.gitUserConfigMissing);
|
|
18
|
+
.require((c) => !c.data?.remote_lock?.isLocked, text.grizzco.remoteLocked);
|
|
21
19
|
};
|
|
22
20
|
export const IntentRouting = (engine) => {
|
|
23
21
|
return engine
|
|
@@ -135,17 +135,21 @@ export function buildLoopResultFromTransaction({ executionReport, flowMode, opti
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
const retryFailureReason = executionReport.history.at(-1)?.error ?? text.loop.loopExecutionFailed;
|
|
138
|
-
const failureReason = executionReport.
|
|
139
|
-
|
|
138
|
+
const failureReason = executionReport.retryExhausted
|
|
139
|
+
? text.loop.exceededMaxRetriesSimple
|
|
140
|
+
: executionReport.terminalReason || retryFailureReason;
|
|
140
141
|
const safeHint = executionReport.terminalSafeHint ||
|
|
141
142
|
(executionReport.retryExhausted
|
|
142
|
-
?
|
|
143
|
+
? failureReason
|
|
143
144
|
: executionReport.terminalReason || failureReason);
|
|
144
145
|
const remediationSteps = executionReport.terminalRemediationSteps ?? [];
|
|
145
|
-
const reasonCode = executionReport.
|
|
146
|
-
|
|
146
|
+
const reasonCode = executionReport.retryExhausted
|
|
147
|
+
? 'MAX_RETRIES'
|
|
148
|
+
: executionReport.terminalReasonCode || 'LOOP_FAILED';
|
|
149
|
+
const diagnosticCode = executionReport.terminalDiagnosticCode ?? executionReport.terminalReasonCode ?? reasonCode;
|
|
147
150
|
const failurePhase = executionReport.terminalFailurePhase ||
|
|
148
151
|
(executionReport.retryExhausted ? Phase.VERIFY : undefined);
|
|
152
|
+
const resultReason = executionReport.retryExhausted ? failureReason : safeHint;
|
|
149
153
|
const usage = getTokenUsageFromAuditTrail() ?? undefined;
|
|
150
154
|
const budgetSummary = getBudgetRunSummary() ?? undefined;
|
|
151
155
|
const errorEnvelope = buildFailureEnvelope({
|
|
@@ -157,11 +161,11 @@ export function buildLoopResultFromTransaction({ executionReport, flowMode, opti
|
|
|
157
161
|
});
|
|
158
162
|
return {
|
|
159
163
|
success: false,
|
|
160
|
-
reason:
|
|
164
|
+
reason: resultReason,
|
|
161
165
|
reasonCode,
|
|
162
166
|
terminalReason,
|
|
163
167
|
rootCause,
|
|
164
|
-
diagnosticCode
|
|
168
|
+
diagnosticCode,
|
|
165
169
|
safeHint,
|
|
166
170
|
remediationSteps,
|
|
167
171
|
errorEnvelope,
|
|
@@ -171,6 +175,7 @@ export function buildLoopResultFromTransaction({ executionReport, flowMode, opti
|
|
|
171
175
|
usage,
|
|
172
176
|
authorizationDecisions,
|
|
173
177
|
history: telemetry.getHistory(),
|
|
178
|
+
changedFiles,
|
|
174
179
|
failurePhase,
|
|
175
180
|
errorType: ErrorType.UNKNOWN,
|
|
176
181
|
errorCode: executionReport.lastErrorCode,
|
|
@@ -122,6 +122,29 @@ export function resolveAttemptFailure(params) {
|
|
|
122
122
|
return undefined;
|
|
123
123
|
}
|
|
124
124
|
const errorCode = extractErrorCode(flowReport.error) ?? extractErrorCodeFromTraces(flowReport);
|
|
125
|
+
if (profile.verifyPolicy !== 'never' && context?.verifyResult?.ok === false) {
|
|
126
|
+
const verifyOutput = context.verifyResult.output || text.loop.loopExecutionFailed;
|
|
127
|
+
const errorType = classifyError(verifyOutput);
|
|
128
|
+
const fallbackReason = sanitizeReason(context.lastError || verifyOutput);
|
|
129
|
+
const guidance = buildFailureGuidance({
|
|
130
|
+
reasonCode: 'VERIFY_FAILED',
|
|
131
|
+
failurePhase: 'VERIFY',
|
|
132
|
+
errorCode: String(errorType),
|
|
133
|
+
verifyOutput,
|
|
134
|
+
environmentMode,
|
|
135
|
+
fallbackReason,
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
reason: guidance.safeHint,
|
|
139
|
+
reasonCode: 'VERIFY_FAILED',
|
|
140
|
+
failurePhase: 'VERIFY',
|
|
141
|
+
retryable: isRetryable(errorType),
|
|
142
|
+
errorCode: String(errorType),
|
|
143
|
+
diagnosticCode: guidance.diagnosticCode,
|
|
144
|
+
safeHint: guidance.safeHint,
|
|
145
|
+
remediationSteps: guidance.remediationSteps,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
125
148
|
if (flowMode === 'autopilot' && autopilotCompletion) {
|
|
126
149
|
if (autopilotCompletion.status === 'changed' ||
|
|
127
150
|
autopilotCompletion.status === 'read_only_answer') {
|
|
@@ -238,29 +261,6 @@ export function resolveAttemptFailure(params) {
|
|
|
238
261
|
remediationSteps: guidance.remediationSteps,
|
|
239
262
|
};
|
|
240
263
|
}
|
|
241
|
-
if (profile.verifyPolicy !== 'never' && context?.verifyResult?.ok === false) {
|
|
242
|
-
const verifyOutput = context.verifyResult.output || text.loop.loopExecutionFailed;
|
|
243
|
-
const errorType = classifyError(verifyOutput);
|
|
244
|
-
const fallbackReason = sanitizeReason(context.lastError || verifyOutput);
|
|
245
|
-
const guidance = buildFailureGuidance({
|
|
246
|
-
reasonCode: 'VERIFY_FAILED',
|
|
247
|
-
failurePhase: 'VERIFY',
|
|
248
|
-
errorCode: String(errorType),
|
|
249
|
-
verifyOutput,
|
|
250
|
-
environmentMode,
|
|
251
|
-
fallbackReason,
|
|
252
|
-
});
|
|
253
|
-
return {
|
|
254
|
-
reason: guidance.safeHint,
|
|
255
|
-
reasonCode: 'VERIFY_FAILED',
|
|
256
|
-
failurePhase: 'VERIFY',
|
|
257
|
-
retryable: isRetryable(errorType),
|
|
258
|
-
errorCode: String(errorType),
|
|
259
|
-
diagnosticCode: guidance.diagnosticCode,
|
|
260
|
-
safeHint: guidance.safeHint,
|
|
261
|
-
remediationSteps: guidance.remediationSteps,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
264
|
const failurePhase = inferFailurePhase(flowReport);
|
|
265
265
|
const fallbackReason = sanitizeReason(context?.lastError || flowReport.error);
|
|
266
266
|
if (isRecoverableToolInputErrorCode(errorCode)) {
|
|
@@ -58,7 +58,10 @@ export function mapRetryExhaustedReport(params) {
|
|
|
58
58
|
lastRecentReadArtifacts,
|
|
59
59
|
lastToolResultPreviewArtifacts,
|
|
60
60
|
terminalFailurePhase: failure?.failurePhase,
|
|
61
|
+
terminalReasonCode: failure?.reasonCode,
|
|
61
62
|
terminalDiagnosticCode: failure?.diagnosticCode,
|
|
63
|
+
terminalSafeHint: failure?.safeHint,
|
|
64
|
+
terminalRemediationSteps: failure ? [...failure.remediationSteps] : undefined,
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
67
|
//# sourceMappingURL=report-mapper.js.map
|
|
@@ -128,6 +128,7 @@ export class FlowTransactionRunner {
|
|
|
128
128
|
lastRecentReadArtifacts;
|
|
129
129
|
lastToolResultPreviewArtifacts;
|
|
130
130
|
lastReplacementState;
|
|
131
|
+
pendingAutopilotVerification;
|
|
131
132
|
constructor(params) {
|
|
132
133
|
this.params = params;
|
|
133
134
|
this.lastVerifyArtifact = params.options.artifactHints?.verifyArtifact;
|
|
@@ -182,6 +183,7 @@ export class FlowTransactionRunner {
|
|
|
182
183
|
: undefined,
|
|
183
184
|
},
|
|
184
185
|
replacementState: this.lastReplacementState,
|
|
186
|
+
pendingVerification: this.pendingAutopilotVerification,
|
|
185
187
|
lastError: this.currentLastError,
|
|
186
188
|
applyBackRuntime: {
|
|
187
189
|
activeRepoPath: this.params.env.activeRepoPath,
|
|
@@ -212,6 +214,18 @@ export class FlowTransactionRunner {
|
|
|
212
214
|
flowMode: this.params.flowMode,
|
|
213
215
|
});
|
|
214
216
|
lastAttemptFailure = attemptFailure;
|
|
217
|
+
if (this.params.flowMode === 'autopilot') {
|
|
218
|
+
if (attemptFailure?.reasonCode === 'VERIFY_FAILED') {
|
|
219
|
+
this.pendingAutopilotVerification = {
|
|
220
|
+
changedFiles: terminalCtx && 'changedFiles' in terminalCtx
|
|
221
|
+
? (terminalCtx.changedFiles ?? undefined)
|
|
222
|
+
: undefined,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
else if (!attemptFailure) {
|
|
226
|
+
this.pendingAutopilotVerification = undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
215
229
|
const entry = {
|
|
216
230
|
attempt,
|
|
217
231
|
plan: shrinkCtx?.plan ?? null,
|
|
@@ -10,6 +10,7 @@ export async function executeAutopilotFlow(initCtx) {
|
|
|
10
10
|
.step('VERIFY_GATE', runAutopilotVerifyGate)
|
|
11
11
|
.step('REPORT', displayReport);
|
|
12
12
|
const report = await pipeline.execute();
|
|
13
|
+
await report.data?.toolstack?.dispose?.();
|
|
13
14
|
report.auditPath = await saveAudit(report, initCtx.options);
|
|
14
15
|
report.strategyName = initCtx.mode;
|
|
15
16
|
report.fsMode = initCtx.mode;
|
|
@@ -93,6 +93,7 @@ function buildPipelineByMode(initCtx) {
|
|
|
93
93
|
export async function executeSalmonLoopFlow(initCtx) {
|
|
94
94
|
const pipeline = buildPipelineByMode(initCtx);
|
|
95
95
|
const report = await pipeline.execute();
|
|
96
|
+
await report.data?.toolstack?.dispose?.();
|
|
96
97
|
// Save audit log
|
|
97
98
|
report.auditPath = await saveAudit(report, initCtx.options);
|
|
98
99
|
report.strategyName = initCtx.mode;
|
|
@@ -7,10 +7,7 @@ import { MicroTaskRunner } from '../dsl/MicroTaskRunner.js';
|
|
|
7
7
|
import { StandardStrategy } from '../dsl/strategies.js';
|
|
8
8
|
import { Executor } from '../execution/Executor.js';
|
|
9
9
|
import { WorkerFactory } from '../execution/WorkerFactory.js';
|
|
10
|
-
import { CachedService } from '../services/CachedService.js';
|
|
11
|
-
import { GitConfigService } from '../services/implementations/default/GitConfigService.js';
|
|
12
10
|
import { MockLockService } from '../services/implementations/mock/MockLockService.js';
|
|
13
|
-
import { MockUserQuotaService } from '../services/implementations/mock/MockUserQuotaService.js';
|
|
14
11
|
import { registry } from '../services/registry.js';
|
|
15
12
|
/**
|
|
16
13
|
* Bootstraps the service registry with required providers.
|
|
@@ -18,10 +15,6 @@ import { registry } from '../services/registry.js';
|
|
|
18
15
|
export function bootstrapRegistry() {
|
|
19
16
|
if (!registry.has('remote_lock'))
|
|
20
17
|
registry.register(new MockLockService());
|
|
21
|
-
if (!registry.has('user_quota'))
|
|
22
|
-
registry.register(new MockUserQuotaService());
|
|
23
|
-
if (!registry.has('git_config'))
|
|
24
|
-
registry.register(new CachedService(new GitConfigService()));
|
|
25
18
|
}
|
|
26
19
|
export const runApply = async (ctx) => {
|
|
27
20
|
const { workspace, diff, fileStateResolver, emit } = ctx;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { text } from '../../../locales/index.js';
|
|
4
|
-
import { lstat, readlink } from '../../adapters/fs/node-fs.js';
|
|
4
|
+
import { lstat, readFile, readdir, readlink } from '../../adapters/fs/node-fs.js';
|
|
5
5
|
import { GitAdapter } from '../../adapters/git/git-adapter.js';
|
|
6
6
|
import { LIMITS } from '../../config/limits.js';
|
|
7
7
|
import { supportsLlmStreaming } from '../../llm/capabilities.js';
|
|
@@ -23,6 +23,20 @@ const GIT_HASH_OUTPUT_LIMITS = {
|
|
|
23
23
|
maxStdoutBytes: 256,
|
|
24
24
|
maxStderrChars: 4_096,
|
|
25
25
|
};
|
|
26
|
+
const FILESYSTEM_SAMPLE_LIMITS = {
|
|
27
|
+
maxEntries: 10_000,
|
|
28
|
+
maxBytesPerFile: 5 * 1024 * 1024,
|
|
29
|
+
};
|
|
30
|
+
const FILESYSTEM_EXCLUDED_SEGMENTS = new Set([
|
|
31
|
+
'.git',
|
|
32
|
+
'.salmonloop',
|
|
33
|
+
'node_modules',
|
|
34
|
+
'.next',
|
|
35
|
+
'dist',
|
|
36
|
+
'build',
|
|
37
|
+
'.turbo',
|
|
38
|
+
'.cache',
|
|
39
|
+
]);
|
|
26
40
|
function hashFingerprintValue(value) {
|
|
27
41
|
return createHash('sha256').update(value).digest('hex');
|
|
28
42
|
}
|
|
@@ -165,6 +179,7 @@ async function captureWorkspaceFingerprint(workspacePath) {
|
|
|
165
179
|
}
|
|
166
180
|
const workingContent = hashFingerprintValue(workingEntries.map(([path, fingerprint]) => `${path}:${fingerprint}`).join('\n'));
|
|
167
181
|
return {
|
|
182
|
+
kind: 'git',
|
|
168
183
|
head,
|
|
169
184
|
index,
|
|
170
185
|
statusMetadata: hashFingerprintBuffer(statusOutput),
|
|
@@ -173,6 +188,79 @@ async function captureWorkspaceFingerprint(workspacePath) {
|
|
|
173
188
|
workingEntries,
|
|
174
189
|
};
|
|
175
190
|
}
|
|
191
|
+
function isFilesystemSamplingExcludedPath(path) {
|
|
192
|
+
const normalized = path.replace(/\\/g, '/');
|
|
193
|
+
if (isRuntimeGeneratedPath(normalized))
|
|
194
|
+
return true;
|
|
195
|
+
return normalized.split('/').some((segment) => FILESYSTEM_EXCLUDED_SEGMENTS.has(segment));
|
|
196
|
+
}
|
|
197
|
+
function formatFilesystemMode(mode) {
|
|
198
|
+
return (mode & 0o7777).toString(8);
|
|
199
|
+
}
|
|
200
|
+
async function hashFilesystemPath(absolutePath) {
|
|
201
|
+
const stats = await lstat(absolutePath);
|
|
202
|
+
if (stats.isSymbolicLink()) {
|
|
203
|
+
return `symlink:${formatFilesystemMode(stats.mode)}:${hashFingerprintValue(await readlink(absolutePath))}`;
|
|
204
|
+
}
|
|
205
|
+
if (stats.isDirectory()) {
|
|
206
|
+
return `dir:${formatFilesystemMode(stats.mode)}`;
|
|
207
|
+
}
|
|
208
|
+
if (stats.size > FILESYSTEM_SAMPLE_LIMITS.maxBytesPerFile) {
|
|
209
|
+
throw new Error(`Workspace sampling skipped file larger than ${FILESYSTEM_SAMPLE_LIMITS.maxBytesPerFile} bytes`);
|
|
210
|
+
}
|
|
211
|
+
if (stats.isFile()) {
|
|
212
|
+
return `file:${formatFilesystemMode(stats.mode)}:${hashFingerprintBuffer(await readFile(absolutePath))}`;
|
|
213
|
+
}
|
|
214
|
+
return `other:${formatFilesystemMode(stats.mode)}:${stats.size}`;
|
|
215
|
+
}
|
|
216
|
+
function appendFilesystemEntry(entries, path, fingerprint) {
|
|
217
|
+
if (entries.length >= FILESYSTEM_SAMPLE_LIMITS.maxEntries) {
|
|
218
|
+
throw new Error(`Workspace sampling exceeded ${FILESYSTEM_SAMPLE_LIMITS.maxEntries} entries`);
|
|
219
|
+
}
|
|
220
|
+
entries.push([path, fingerprint]);
|
|
221
|
+
}
|
|
222
|
+
async function collectFilesystemEntries(params) {
|
|
223
|
+
const absoluteDir = join(params.workspacePath, params.relativeDir);
|
|
224
|
+
const dirents = await readdir(absoluteDir, { withFileTypes: true });
|
|
225
|
+
const sorted = [...dirents].sort((left, right) => left.name.localeCompare(right.name));
|
|
226
|
+
for (const dirent of sorted) {
|
|
227
|
+
const relativePath = params.relativeDir ? `${params.relativeDir}/${dirent.name}` : dirent.name;
|
|
228
|
+
if (isFilesystemSamplingExcludedPath(relativePath)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const absolutePath = join(params.workspacePath, relativePath);
|
|
232
|
+
if (dirent.isDirectory()) {
|
|
233
|
+
appendFilesystemEntry(params.entries, `${relativePath}/`, await hashFilesystemPath(absolutePath));
|
|
234
|
+
await collectFilesystemEntries({
|
|
235
|
+
workspacePath: params.workspacePath,
|
|
236
|
+
relativeDir: relativePath,
|
|
237
|
+
entries: params.entries,
|
|
238
|
+
});
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
appendFilesystemEntry(params.entries, relativePath, await hashFilesystemPath(absolutePath));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function captureFilesystemFingerprint(workspacePath) {
|
|
245
|
+
const workingEntries = [];
|
|
246
|
+
await collectFilesystemEntries({ workspacePath, relativeDir: '', entries: workingEntries });
|
|
247
|
+
const statusMetadata = hashFingerprintValue(workingEntries.map(([path, fingerprint]) => `${path}:${fingerprint}`).join('\n'));
|
|
248
|
+
return {
|
|
249
|
+
kind: 'filesystem',
|
|
250
|
+
head: '',
|
|
251
|
+
index: '',
|
|
252
|
+
statusMetadata,
|
|
253
|
+
workingContent: statusMetadata,
|
|
254
|
+
statusEntries: workingEntries,
|
|
255
|
+
workingEntries,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function captureWorkspaceFingerprintForContext(ctx) {
|
|
259
|
+
if (ctx.workspace.capabilities?.git.insideWorkTree === false) {
|
|
260
|
+
return captureFilesystemFingerprint(ctx.workspace.workPath);
|
|
261
|
+
}
|
|
262
|
+
return captureWorkspaceFingerprint(ctx.workspace.workPath);
|
|
263
|
+
}
|
|
176
264
|
function collectChangedWorkspacePaths(before, after) {
|
|
177
265
|
const beforeStatusEntries = new Map(before.statusEntries);
|
|
178
266
|
const afterStatusEntries = new Map(after.statusEntries);
|
|
@@ -280,7 +368,7 @@ export async function runAutopilot(ctx) {
|
|
|
280
368
|
let samplingFailedClosed = false;
|
|
281
369
|
if (supportsTools) {
|
|
282
370
|
try {
|
|
283
|
-
workspaceFingerprintBefore = await
|
|
371
|
+
workspaceFingerprintBefore = await captureWorkspaceFingerprintForContext(ctx);
|
|
284
372
|
}
|
|
285
373
|
catch {
|
|
286
374
|
samplingFailedClosed = true;
|
|
@@ -339,10 +427,11 @@ export async function runAutopilot(ctx) {
|
|
|
339
427
|
}
|
|
340
428
|
else {
|
|
341
429
|
try {
|
|
342
|
-
const workspaceFingerprintAfter = await
|
|
430
|
+
const workspaceFingerprintAfter = await captureWorkspaceFingerprintForContext(ctx);
|
|
343
431
|
changedFiles = collectChangedWorkspacePaths(workspaceFingerprintBefore, workspaceFingerprintAfter);
|
|
344
432
|
mutated =
|
|
345
433
|
changedFiles.length > 0 ||
|
|
434
|
+
workspaceFingerprintBefore.statusMetadata !== workspaceFingerprintAfter.statusMetadata ||
|
|
346
435
|
workspaceFingerprintBefore.head !== workspaceFingerprintAfter.head ||
|
|
347
436
|
workspaceFingerprintBefore.index !== workspaceFingerprintAfter.index;
|
|
348
437
|
}
|
|
@@ -351,11 +440,23 @@ export async function runAutopilot(ctx) {
|
|
|
351
440
|
}
|
|
352
441
|
}
|
|
353
442
|
}
|
|
443
|
+
const pendingChangedFiles = ctx.pendingVerification?.changedFiles && ctx.pendingVerification.changedFiles.length > 0
|
|
444
|
+
? ctx.pendingVerification.changedFiles
|
|
445
|
+
: undefined;
|
|
446
|
+
const effectiveChangedFiles = changedFiles && changedFiles.length > 0
|
|
447
|
+
? changedFiles
|
|
448
|
+
: pendingChangedFiles && pendingChangedFiles.length > 0
|
|
449
|
+
? pendingChangedFiles
|
|
450
|
+
: undefined;
|
|
451
|
+
const effectiveMutated = mutated || Boolean(ctx.pendingVerification);
|
|
354
452
|
return {
|
|
355
453
|
...ctx,
|
|
356
|
-
mutated,
|
|
357
|
-
changedFiles:
|
|
358
|
-
|
|
454
|
+
mutated: effectiveMutated,
|
|
455
|
+
changedFiles: effectiveChangedFiles,
|
|
456
|
+
pendingVerification: effectiveMutated
|
|
457
|
+
? { changedFiles: effectiveChangedFiles }
|
|
458
|
+
: ctx.pendingVerification,
|
|
459
|
+
completion: resolveAutopilotCompletion({ content, mutated: effectiveMutated, localAudit }),
|
|
359
460
|
toolCallingAudit: mergedAudit,
|
|
360
461
|
report: {
|
|
361
462
|
kind: 'answer',
|
|
@@ -390,6 +491,7 @@ export async function runAutopilotVerifyGate(ctx) {
|
|
|
390
491
|
const nextCtx = {
|
|
391
492
|
...ctx,
|
|
392
493
|
verifyResult,
|
|
494
|
+
pendingVerification: verifyResult.ok ? undefined : ctx.pendingVerification,
|
|
393
495
|
};
|
|
394
496
|
return verifyArtifact ? { ...nextCtx, verifyArtifact } : nextCtx;
|
|
395
497
|
}
|
|
@@ -3,11 +3,17 @@ import { recordAuditEvent } from '../../observability/audit-trail.js';
|
|
|
3
3
|
import { resolveExecutionProfile } from '../../runtime/execution-profile.js';
|
|
4
4
|
import { createStandardToolstack } from '../../tools/loader.js';
|
|
5
5
|
import { preflight } from '../../verification/runner.js';
|
|
6
|
+
import { requiresGitWorkspace } from '../../workspace/capabilities.js';
|
|
6
7
|
import { resolveLlmToolCallingPolicy } from '../dsl/llm-strategy.js';
|
|
7
8
|
export const runPreflight = async (ctx) => {
|
|
8
9
|
const executionProfile = resolveExecutionProfile(ctx.mode);
|
|
9
10
|
const result = await preflight(ctx.workspace, ctx.emit, {
|
|
10
11
|
ignoreDirty: executionProfile.ignoreDirtyPreflight,
|
|
12
|
+
requireWrite: !executionProfile.readOnly,
|
|
13
|
+
requireGit: requiresGitWorkspace({
|
|
14
|
+
mode: ctx.mode,
|
|
15
|
+
strategy: ctx.workspace.strategy,
|
|
16
|
+
}),
|
|
11
17
|
});
|
|
12
18
|
if (!result.ok) {
|
|
13
19
|
const reason = result.reason || text.loop.preflightFailedNotGit;
|
|
@@ -27,12 +33,16 @@ export const runPreflight = async (ctx) => {
|
|
|
27
33
|
message: text.loop.preflightPassed,
|
|
28
34
|
timestamp: new Date(),
|
|
29
35
|
});
|
|
36
|
+
if (result.capabilities) {
|
|
37
|
+
ctx.workspace.capabilities = result.capabilities;
|
|
38
|
+
}
|
|
30
39
|
const toolstack = resolveLlmToolCallingPolicy(executionProfile.entryPhase, ctx.options.llm)
|
|
31
40
|
.enabled
|
|
32
41
|
? await createStandardToolstack({
|
|
33
42
|
repoRoot: ctx.workspace.workPath,
|
|
34
43
|
persistenceRoot: ctx.workspace.baseRepoPath || ctx.workspace.workPath,
|
|
35
44
|
worktreeRoot: ctx.workspace.strategy === 'worktree' ? ctx.workspace.workPath : undefined,
|
|
45
|
+
workspaceCapabilities: ctx.workspace.capabilities,
|
|
36
46
|
attemptId: ctx.attempt ?? 1,
|
|
37
47
|
dryRun: Boolean(ctx.options?.dryRun),
|
|
38
48
|
allowedToolNames: Array.isArray(ctx.options.allowedToolNames)
|
|
@@ -4,6 +4,7 @@ export function buildPhaseToolRuntimeContext(ctx, phase, cacheSurface) {
|
|
|
4
4
|
repoRoot: ctx.workspace.workPath,
|
|
5
5
|
persistenceRoot: ctx.workspace.baseRepoPath || ctx.workspace.workPath,
|
|
6
6
|
worktreeRoot: ctx.workspace.strategy === 'worktree' ? ctx.workspace.workPath : undefined,
|
|
7
|
+
workspaceCapabilities: ctx.workspace.capabilities,
|
|
7
8
|
flowMode: ctx.mode,
|
|
8
9
|
attemptId: ctx.attempt ?? 1,
|
|
9
10
|
dryRun: Boolean(ctx.options?.dryRun),
|