salmon-loop 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +109 -153
- 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/core/adapters/fs/node-fs.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +1 -1
- package/dist/core/context/service.js +5 -2
- 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 +48 -25
- 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
|
@@ -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),
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
const MAX_EVENTS = 10_000;
|
|
1
2
|
export function createTaskEventBus() {
|
|
2
3
|
const listeners = new Set();
|
|
3
4
|
const events = [];
|
|
4
5
|
let nextId = 1;
|
|
6
|
+
function evictIfNeeded() {
|
|
7
|
+
if (events.length > MAX_EVENTS) {
|
|
8
|
+
events.splice(0, events.length - MAX_EVENTS);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
5
11
|
return {
|
|
6
12
|
subscribe(listener) {
|
|
7
13
|
listeners.add(listener);
|
|
@@ -28,6 +34,7 @@ export function createTaskEventBus() {
|
|
|
28
34
|
id: resolvedId,
|
|
29
35
|
};
|
|
30
36
|
events.push(persistedEvent);
|
|
37
|
+
evictIfNeeded();
|
|
31
38
|
for (const listener of listeners) {
|
|
32
39
|
listener(persistedEvent);
|
|
33
40
|
}
|
|
@@ -47,6 +54,13 @@ export function createTaskEventBus() {
|
|
|
47
54
|
}
|
|
48
55
|
return filtered;
|
|
49
56
|
},
|
|
57
|
+
clearTask(taskId) {
|
|
58
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
59
|
+
if (events[i].taskId === taskId) {
|
|
60
|
+
events.splice(i, 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
66
|
//# sourceMappingURL=bus.js.map
|
|
@@ -48,6 +48,16 @@ export function createInteractionFacade(deps) {
|
|
|
48
48
|
else if (result.state === 'awaiting_input') {
|
|
49
49
|
deps.eventBus?.publish({ type: 'task.awaiting_input', taskId: result.id });
|
|
50
50
|
}
|
|
51
|
+
})
|
|
52
|
+
.catch((error) => {
|
|
53
|
+
taskControllers.delete(task.id);
|
|
54
|
+
const failedTask = {
|
|
55
|
+
...task,
|
|
56
|
+
state: 'failed',
|
|
57
|
+
statusMessage: error instanceof Error ? error.message : 'Task execution failed',
|
|
58
|
+
};
|
|
59
|
+
store.update(failedTask);
|
|
60
|
+
deps.eventBus?.publish({ type: 'task.failed', taskId: task.id });
|
|
51
61
|
});
|
|
52
62
|
return { task, signal: controller.signal };
|
|
53
63
|
},
|