@slope-dev/slope 1.25.1 → 1.25.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/adapters.d.ts +3 -0
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +2 -0
- package/dist/adapters.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +1 -0
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +77 -13
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/loop.d.ts.map +1 -1
- package/dist/cli/commands/loop.js +148 -1
- package/dist/cli/commands/loop.js.map +1 -1
- package/dist/cli/loop/aider-executor.d.ts +10 -0
- package/dist/cli/loop/aider-executor.d.ts.map +1 -0
- package/dist/cli/loop/aider-executor.js +239 -0
- package/dist/cli/loop/aider-executor.js.map +1 -0
- package/dist/cli/loop/executor-adapter.d.ts +18 -0
- package/dist/cli/loop/executor-adapter.d.ts.map +1 -0
- package/dist/cli/loop/executor-adapter.js +37 -0
- package/dist/cli/loop/executor-adapter.js.map +1 -0
- package/dist/cli/loop/executor.d.ts.map +1 -1
- package/dist/cli/loop/executor.js +159 -182
- package/dist/cli/loop/executor.js.map +1 -1
- package/dist/cli/loop/slope-executor.d.ts +35 -0
- package/dist/cli/loop/slope-executor.d.ts.map +1 -0
- package/dist/cli/loop/slope-executor.js +794 -0
- package/dist/cli/loop/slope-executor.js.map +1 -0
- package/dist/cli/loop/types.d.ts +46 -0
- package/dist/cli/loop/types.d.ts.map +1 -1
- package/dist/cli/loop/types.js.map +1 -1
- package/dist/core/adapters/ob1.d.ts +41 -0
- package/dist/core/adapters/ob1.d.ts.map +1 -0
- package/dist/core/adapters/ob1.js +313 -0
- package/dist/core/adapters/ob1.js.map +1 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.d.ts.map +1 -1
- package/dist/core/harness.js +1 -1
- package/dist/core/harness.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlopeExecutor — custom agentic tool loop using the Anthropic Messages API.
|
|
3
|
+
*
|
|
4
|
+
* Implements ExecutorAdapter with:
|
|
5
|
+
* - 6 tools: read_file, write_file, edit_file, bash, glob, grep
|
|
6
|
+
* - Token/cost tracking from response.usage
|
|
7
|
+
* - Full transcript recording
|
|
8
|
+
* - Stuck detection (repeated identical tool calls)
|
|
9
|
+
* - Timeout enforcement
|
|
10
|
+
*/
|
|
11
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import { join, resolve, dirname } from 'node:path';
|
|
14
|
+
import { loadConfig } from '../../core/config.js';
|
|
15
|
+
import { loadScorecards } from '../../core/loader.js';
|
|
16
|
+
import { computeHandicapCard } from '../../core/handicap.js';
|
|
17
|
+
import { extractHazardIndex, filterCommonIssues } from '../../core/briefing.js';
|
|
18
|
+
import { extractKeywords } from './planner.js';
|
|
19
|
+
// ── Constants ───────────────────────────────────────
|
|
20
|
+
const MAX_TURNS_DEFAULT = 50;
|
|
21
|
+
const MAX_REPEATED_CALLS = 3;
|
|
22
|
+
const MAX_GUARD_RETRIES = 2;
|
|
23
|
+
const MAX_OVERLOAD_RETRIES = 3;
|
|
24
|
+
const MAX_CONSECUTIVE_BASH = 5;
|
|
25
|
+
/** Turn budget per club — smaller tickets get fewer turns to prevent flailing */
|
|
26
|
+
const CLUB_TURN_LIMITS = {
|
|
27
|
+
putter: 20,
|
|
28
|
+
wedge: 30,
|
|
29
|
+
short_iron: 40,
|
|
30
|
+
long_iron: 50,
|
|
31
|
+
driver: 50,
|
|
32
|
+
};
|
|
33
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
34
|
+
const TOOL_BASH_TIMEOUT = 60_000;
|
|
35
|
+
const TOOL_OUTPUT_CAP = 50_000;
|
|
36
|
+
const FILE_READ_CAP = 100_000;
|
|
37
|
+
const TRANSCRIPT_CAP = 2000;
|
|
38
|
+
const TRUNCATE_KEEP_RECENT = 20; // keep last 10 turns fully intact
|
|
39
|
+
// Approximate cost per million tokens — not intended to be precise
|
|
40
|
+
const COST_TABLE = {
|
|
41
|
+
'claude-haiku-4-5': { in: 0.80, out: 4.00 },
|
|
42
|
+
'claude-sonnet-4-5': { in: 3.00, out: 15.00 },
|
|
43
|
+
'claude-sonnet-4-6': { in: 3.00, out: 15.00 },
|
|
44
|
+
'claude-opus-4-6': { in: 15.00, out: 75.00 },
|
|
45
|
+
};
|
|
46
|
+
const DEFAULT_COST = { in: 1.00, out: 5.00 };
|
|
47
|
+
// Destructive command blocklist — [pattern, human-readable reason]
|
|
48
|
+
const BLOCKED_COMMANDS = [
|
|
49
|
+
[/\brm\s+-\w*r\w*\s+\//, 'rm with recursive flag targeting absolute path'],
|
|
50
|
+
[/\bgit\s+push\b/, 'push is handled by the loop, not the executor'],
|
|
51
|
+
[/\bmkfs\b/, 'filesystem format commands are not allowed'],
|
|
52
|
+
[/\bdd\b.*\bof=\//, 'dd write to absolute path is not allowed'],
|
|
53
|
+
[/\b(shutdown|reboot|halt|poweroff)\b/, 'system power commands are not allowed'],
|
|
54
|
+
[/\bcurl\b.*\|\s*(ba)?sh/, 'piping curl to shell is not allowed'],
|
|
55
|
+
[/\bpnpm\s+slope\b|\bslope\s+/, 'use the slope tool instead of running slope via bash'],
|
|
56
|
+
];
|
|
57
|
+
// ── Tool definitions (Anthropic API format) ─────────
|
|
58
|
+
const TOOLS = [
|
|
59
|
+
{
|
|
60
|
+
name: 'read_file',
|
|
61
|
+
description: 'Read the full contents of a file. Always read a file before editing it.',
|
|
62
|
+
input_schema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
path: { type: 'string', description: 'Relative path from the repo root' },
|
|
66
|
+
},
|
|
67
|
+
required: ['path'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'write_file',
|
|
72
|
+
description: 'Create a new file or completely overwrite an existing one. Use edit_file for targeted changes.',
|
|
73
|
+
input_schema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
path: { type: 'string', description: 'Relative path from the repo root' },
|
|
77
|
+
content: { type: 'string', description: 'Complete file content' },
|
|
78
|
+
},
|
|
79
|
+
required: ['path', 'content'],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'edit_file',
|
|
84
|
+
description: 'Replace an exact string in a file. The old_string must match exactly (including whitespace/indentation). Only the first occurrence is replaced.',
|
|
85
|
+
input_schema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
path: { type: 'string', description: 'Relative path from the repo root' },
|
|
89
|
+
old_string: { type: 'string', description: 'Exact text to find (must be unique enough to match once)' },
|
|
90
|
+
new_string: { type: 'string', description: 'Replacement text' },
|
|
91
|
+
},
|
|
92
|
+
required: ['path', 'old_string', 'new_string'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'bash',
|
|
97
|
+
description: 'Run a shell command. Use for git operations, running tests (pnpm test), type checking (pnpm typecheck), and other CLI tools. Commands time out after 60s.',
|
|
98
|
+
input_schema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
|
102
|
+
},
|
|
103
|
+
required: ['command'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'glob',
|
|
108
|
+
description: 'Find files matching a glob pattern. Returns relative paths, one per line.',
|
|
109
|
+
input_schema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
pattern: { type: 'string', description: 'Glob pattern (e.g., "src/**/*.ts", "*.json")' },
|
|
113
|
+
},
|
|
114
|
+
required: ['pattern'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'grep',
|
|
119
|
+
description: 'Search file contents with a regex pattern. Returns matching lines with file paths and line numbers.',
|
|
120
|
+
input_schema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
pattern: { type: 'string', description: 'Search pattern (regex supported)' },
|
|
124
|
+
path: { type: 'string', description: 'File or directory to search (default: current directory)' },
|
|
125
|
+
include: { type: 'string', description: 'File glob filter (e.g., "*.ts")' },
|
|
126
|
+
},
|
|
127
|
+
required: ['pattern'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'slope',
|
|
132
|
+
description: 'Run a SLOPE CLI command (read-only). Available: search, context, briefing, card, validate, map, plan, prep, status, next, flows, doctor. Example: slope({ command: "briefing --categories=testing" })',
|
|
133
|
+
input_schema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
command: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'SLOPE subcommand and flags (e.g., "search --query=handicap", "briefing", "map")',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ['command'],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
/** Allowlisted read-only slope subcommands */
|
|
146
|
+
const SLOPE_ALLOWLIST = new Set([
|
|
147
|
+
'search', 'context', 'briefing', 'card', 'validate',
|
|
148
|
+
'map', 'plan', 'prep', 'status', 'next', 'flows',
|
|
149
|
+
'doctor', 'version',
|
|
150
|
+
]);
|
|
151
|
+
const SLOPE_TOOL_OUTPUT_CAP = 4000;
|
|
152
|
+
// ── Path security ───────────────────────────────────
|
|
153
|
+
export function safePath(relPath, cwd) {
|
|
154
|
+
const abs = resolve(cwd, relPath);
|
|
155
|
+
if (!abs.startsWith(resolve(cwd))) {
|
|
156
|
+
throw new Error(`Path traversal blocked: ${relPath}`);
|
|
157
|
+
}
|
|
158
|
+
return abs;
|
|
159
|
+
}
|
|
160
|
+
// ── Model ID resolution ─────────────────────────────
|
|
161
|
+
export function resolveModelId(model) {
|
|
162
|
+
return model
|
|
163
|
+
.replace(/^openrouter\/anthropic\//, '')
|
|
164
|
+
.replace(/^anthropic\//, '');
|
|
165
|
+
}
|
|
166
|
+
export function lookupCost(modelId) {
|
|
167
|
+
for (const [key, cost] of Object.entries(COST_TABLE)) {
|
|
168
|
+
if (modelId.includes(key))
|
|
169
|
+
return cost;
|
|
170
|
+
}
|
|
171
|
+
return DEFAULT_COST;
|
|
172
|
+
}
|
|
173
|
+
// ── Executor ────────────────────────────────────────
|
|
174
|
+
export const slopeExecutor = {
|
|
175
|
+
id: 'slope',
|
|
176
|
+
async execute(ctx, config, cwd, log) {
|
|
177
|
+
const start = Date.now();
|
|
178
|
+
const transcript = [];
|
|
179
|
+
let totalIn = 0;
|
|
180
|
+
let totalOut = 0;
|
|
181
|
+
let client;
|
|
182
|
+
try {
|
|
183
|
+
const { default: AnthropicSDK } = await import('@anthropic-ai/sdk');
|
|
184
|
+
// Supports ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL from env
|
|
185
|
+
const baseURL = process.env.ANTHROPIC_BASE_URL;
|
|
186
|
+
client = new AnthropicSDK(baseURL ? { baseURL } : undefined);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
190
|
+
const hint = msg.includes('Cannot find') || msg.includes('MODULE_NOT_FOUND')
|
|
191
|
+
? 'Install: pnpm add @anthropic-ai/sdk'
|
|
192
|
+
: 'Is ANTHROPIC_API_KEY set?';
|
|
193
|
+
log.error(`Anthropic client init failed (${hint}): ${msg}`);
|
|
194
|
+
return errorResult(transcript, start);
|
|
195
|
+
}
|
|
196
|
+
const modelId = resolveModelId(ctx.model);
|
|
197
|
+
const costRate = lookupCost(modelId);
|
|
198
|
+
const deadline = start + ctx.timeout * 1000;
|
|
199
|
+
const systemPrompt = buildSystemPrompt(ctx, config, cwd);
|
|
200
|
+
// Message history for the agentic loop
|
|
201
|
+
const messages = [
|
|
202
|
+
{ role: 'user', content: ctx.prompt },
|
|
203
|
+
];
|
|
204
|
+
const maxTurns = CLUB_TURN_LIMITS[ctx.ticket.club] ?? MAX_TURNS_DEFAULT;
|
|
205
|
+
const recentSigs = [];
|
|
206
|
+
let outcome = 'completed';
|
|
207
|
+
let turn = 0;
|
|
208
|
+
let guardRetries = 0;
|
|
209
|
+
let overloadRetries = 0;
|
|
210
|
+
let innerGuardsPassed = false;
|
|
211
|
+
let consecutiveBash = 0;
|
|
212
|
+
// ── Agent loop ──
|
|
213
|
+
while (turn < maxTurns) {
|
|
214
|
+
const remaining = deadline - Date.now();
|
|
215
|
+
if (remaining <= 0) {
|
|
216
|
+
log.warn(`Timed out after ${ctx.timeout}s`);
|
|
217
|
+
outcome = 'timeout';
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
turn++;
|
|
221
|
+
// Truncate old tool results to stay within context limits
|
|
222
|
+
truncateOldMessages(messages);
|
|
223
|
+
// AbortSignal for per-call timeout
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const callTimeout = setTimeout(() => controller.abort(), remaining);
|
|
226
|
+
let response;
|
|
227
|
+
try {
|
|
228
|
+
response = await client.messages.create({
|
|
229
|
+
model: modelId,
|
|
230
|
+
max_tokens: DEFAULT_MAX_TOKENS,
|
|
231
|
+
system: systemPrompt,
|
|
232
|
+
tools: TOOLS,
|
|
233
|
+
messages,
|
|
234
|
+
}, { signal: controller.signal });
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
if (controller.signal.aborted) {
|
|
239
|
+
log.warn(`API call aborted (deadline reached)`);
|
|
240
|
+
outcome = 'timeout';
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
log.error(`API error (turn ${turn}): ${msg}`);
|
|
244
|
+
if (msg.includes('overloaded') && overloadRetries < MAX_OVERLOAD_RETRIES) {
|
|
245
|
+
overloadRetries++;
|
|
246
|
+
await sleep(5000 * overloadRetries);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
outcome = 'error';
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
clearTimeout(callTimeout);
|
|
254
|
+
}
|
|
255
|
+
// Accumulate usage
|
|
256
|
+
if (response.usage) {
|
|
257
|
+
totalIn += response.usage.input_tokens;
|
|
258
|
+
totalOut += response.usage.output_tokens;
|
|
259
|
+
}
|
|
260
|
+
// Model thinks it's done — run inner guards before accepting
|
|
261
|
+
if (response.stop_reason === 'end_turn') {
|
|
262
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
263
|
+
// Skip inner guards if we've exhausted retries or are past deadline
|
|
264
|
+
if (guardRetries >= MAX_GUARD_RETRIES || Date.now() > deadline) {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
const guardFailure = runInnerGuards(config, cwd, log);
|
|
268
|
+
if (!guardFailure) {
|
|
269
|
+
log.info('Inner guards passed');
|
|
270
|
+
innerGuardsPassed = true;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
// Feed the error back so the model can self-correct
|
|
274
|
+
guardRetries++;
|
|
275
|
+
log.warn(`Inner guard failed (attempt ${guardRetries}/${MAX_GUARD_RETRIES}): ${guardFailure.guard}`);
|
|
276
|
+
messages.push({
|
|
277
|
+
role: 'user',
|
|
278
|
+
content: `Your changes have an issue that must be fixed before this ticket is complete.\n\n## ${guardFailure.guard} failed\n\`\`\`\n${guardFailure.output}\n\`\`\`\n\nPlease fix the issue and verify again.`,
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (response.stop_reason !== 'tool_use') {
|
|
283
|
+
log.warn(`Unexpected stop_reason: ${response.stop_reason}`);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
// Extract tool_use blocks
|
|
287
|
+
const toolBlocks = response.content.filter((b) => b.type === 'tool_use');
|
|
288
|
+
// Stuck detection — same tool calls N times in a row
|
|
289
|
+
const sig = toolBlocks.map(b => `${b.name}:${JSON.stringify(b.input)}`).join('|');
|
|
290
|
+
recentSigs.push(sig);
|
|
291
|
+
if (recentSigs.length > MAX_REPEATED_CALLS)
|
|
292
|
+
recentSigs.shift();
|
|
293
|
+
if (recentSigs.length === MAX_REPEATED_CALLS &&
|
|
294
|
+
recentSigs.every(s => s === sig)) {
|
|
295
|
+
log.warn('Stuck: identical tool calls repeated — stopping');
|
|
296
|
+
outcome = 'stuck';
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
// Execute each tool
|
|
300
|
+
const toolResults = [];
|
|
301
|
+
for (const block of toolBlocks) {
|
|
302
|
+
const toolStart = Date.now();
|
|
303
|
+
const res = runTool(block.name, block.input, cwd, log);
|
|
304
|
+
const duration_ms = Date.now() - toolStart;
|
|
305
|
+
transcript.push({
|
|
306
|
+
timestamp: new Date().toISOString(),
|
|
307
|
+
tool: block.name,
|
|
308
|
+
input: block.input,
|
|
309
|
+
output: res.output.slice(0, TRANSCRIPT_CAP),
|
|
310
|
+
duration_ms,
|
|
311
|
+
});
|
|
312
|
+
toolResults.push({
|
|
313
|
+
type: 'tool_result',
|
|
314
|
+
tool_use_id: block.id,
|
|
315
|
+
content: res.output,
|
|
316
|
+
...(res.isError ? { is_error: true } : {}),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
320
|
+
messages.push({ role: 'user', content: toolResults });
|
|
321
|
+
log.info(`Turn ${turn}: ${toolBlocks.map(b => b.name).join(', ')}`);
|
|
322
|
+
// Bash-loop breaker: if the model runs bash N+ times in a row without
|
|
323
|
+
// reading or editing files, inject a nudge to break the spiral
|
|
324
|
+
const allBash = toolBlocks.every(b => b.name === 'bash');
|
|
325
|
+
consecutiveBash = allBash ? consecutiveBash + 1 : 0;
|
|
326
|
+
if (consecutiveBash >= MAX_CONSECUTIVE_BASH) {
|
|
327
|
+
log.warn(`Bash loop detected (${consecutiveBash} consecutive) — injecting nudge`);
|
|
328
|
+
messages.push({
|
|
329
|
+
role: 'user',
|
|
330
|
+
content: `You have run ${consecutiveBash} consecutive bash commands without reading or editing any files. This is usually a sign you are stuck. Stop and think about what is actually failing. Use read_file to examine the error, or use grep/glob to find the right file to edit. Do not run another bash command until you have read the relevant code.`,
|
|
331
|
+
});
|
|
332
|
+
consecutiveBash = 0;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (turn >= maxTurns) {
|
|
336
|
+
log.warn(`Hit max turns (${maxTurns})`);
|
|
337
|
+
if (outcome === 'completed')
|
|
338
|
+
outcome = 'stuck';
|
|
339
|
+
}
|
|
340
|
+
// Commit any uncommitted changes
|
|
341
|
+
const filesChanged = gitCommit(ctx.ticketKey, ctx.ticket.title, cwd, log);
|
|
342
|
+
const duration_s = Math.round((Date.now() - start) / 1000);
|
|
343
|
+
const cost_usd = (totalIn * costRate.in + totalOut * costRate.out) / 1_000_000;
|
|
344
|
+
return {
|
|
345
|
+
outcome,
|
|
346
|
+
noop: false, // caller checks SHA diff
|
|
347
|
+
tokens_in: totalIn,
|
|
348
|
+
tokens_out: totalOut,
|
|
349
|
+
cost_usd: Math.round(cost_usd * 10000) / 10000,
|
|
350
|
+
duration_s,
|
|
351
|
+
transcript,
|
|
352
|
+
files_changed: filesChanged,
|
|
353
|
+
innerGuardsPassed: innerGuardsPassed && filesChanged.length === 0,
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
// ── System prompt ───────────────────────────────────
|
|
358
|
+
export function buildSystemPrompt(ctx, config, cwd) {
|
|
359
|
+
let guide = '';
|
|
360
|
+
let guideWordCount = 0;
|
|
361
|
+
const guidePath = join(cwd, config.agentGuide);
|
|
362
|
+
if (existsSync(guidePath)) {
|
|
363
|
+
const raw = readFileSync(guidePath, 'utf8');
|
|
364
|
+
guideWordCount = raw.split(/\s+/).length;
|
|
365
|
+
if (guideWordCount <= config.agentGuideMaxWords) {
|
|
366
|
+
guide = `\n\n## Agent Guide\n${raw}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const modules = ctx.ticket.modules;
|
|
370
|
+
const scopeSection = modules.length > 0
|
|
371
|
+
? `\n## Allowed Files (scope)\nOnly modify files in or related to these modules:\n${modules.map((m) => `- ${m}`).join('\n')}\nYou may create new test files for these modules. Do NOT edit files outside this scope.`
|
|
372
|
+
: '';
|
|
373
|
+
// SLOPE sprint context (Layer 1) — hazards, common issues, handicap
|
|
374
|
+
let slopeContext = '';
|
|
375
|
+
try {
|
|
376
|
+
slopeContext = buildSlopeContext(ctx.ticket, config, cwd, guideWordCount);
|
|
377
|
+
if (slopeContext)
|
|
378
|
+
slopeContext = '\n\n' + slopeContext;
|
|
379
|
+
}
|
|
380
|
+
catch { /* non-blocking */ }
|
|
381
|
+
return `You are an autonomous coding agent working on the SLOPE project.
|
|
382
|
+
This is a TypeScript monorepo (pnpm, vitest, strict TypeScript).
|
|
383
|
+
|
|
384
|
+
## Working Directory
|
|
385
|
+
${cwd}
|
|
386
|
+
|
|
387
|
+
## Ticket: ${ctx.ticketKey}
|
|
388
|
+
${ctx.ticket.title}
|
|
389
|
+
${scopeSection}
|
|
390
|
+
|
|
391
|
+
## Rules
|
|
392
|
+
- ALWAYS read a file before editing it — understand existing patterns first
|
|
393
|
+
- Make real, substantive changes — never add only comments or whitespace
|
|
394
|
+
- Keep changes minimal and focused on this ticket only
|
|
395
|
+
- Do NOT edit files outside the allowed scope above
|
|
396
|
+
- After all changes, run: pnpm typecheck && pnpm test
|
|
397
|
+
- If tests fail, read the error output carefully before attempting a fix
|
|
398
|
+
- If stuck after multiple bash attempts, stop and re-read the relevant source files
|
|
399
|
+
- Do NOT run git commit — the system auto-commits after verification
|
|
400
|
+
|
|
401
|
+
## Tools
|
|
402
|
+
- read_file: Read file contents (always do this first)
|
|
403
|
+
- edit_file: Surgical string replacement (preferred for changes)
|
|
404
|
+
- write_file: Create new files or full rewrites only
|
|
405
|
+
- bash: Shell commands (tests, typecheck, git, etc.)
|
|
406
|
+
- glob: Find files by pattern
|
|
407
|
+
- grep: Search file contents
|
|
408
|
+
- slope: Query SLOPE sprint data (briefing, search, card, map, etc.)${guide}${slopeContext}`;
|
|
409
|
+
}
|
|
410
|
+
// ── SLOPE Context Injection (Layer 1) ───────────────
|
|
411
|
+
/**
|
|
412
|
+
* Build SLOPE sprint context for injection into the system prompt.
|
|
413
|
+
* Each section is independently wrapped in try/catch for graceful degradation.
|
|
414
|
+
*/
|
|
415
|
+
export function buildSlopeContext(ticket, config, cwd, guideWordCount = 0) {
|
|
416
|
+
const wordBudget = Math.min(2000, config.agentGuideMaxWords - guideWordCount - 500);
|
|
417
|
+
if (wordBudget <= 0)
|
|
418
|
+
return '';
|
|
419
|
+
const sections = [];
|
|
420
|
+
const keywords = extractKeywords(`${ticket.title} ${ticket.description} ${ticket.modules.join(' ')}`, 5);
|
|
421
|
+
let slopeConfig;
|
|
422
|
+
try {
|
|
423
|
+
slopeConfig = loadConfig(cwd);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
let scorecards;
|
|
429
|
+
try {
|
|
430
|
+
scorecards = loadScorecards(slopeConfig, cwd);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
scorecards = [];
|
|
434
|
+
}
|
|
435
|
+
// Section 1: Hazard briefing
|
|
436
|
+
try {
|
|
437
|
+
if (scorecards.length > 0) {
|
|
438
|
+
const hazards = extractHazardIndex(scorecards);
|
|
439
|
+
const recentHazards = hazards.shot_hazards
|
|
440
|
+
.filter(h => keywords.some(kw => h.description.toLowerCase().includes(kw)))
|
|
441
|
+
.slice(0, 5);
|
|
442
|
+
if (recentHazards.length > 0) {
|
|
443
|
+
sections.push('### Hazard Warnings');
|
|
444
|
+
for (const h of recentHazards) {
|
|
445
|
+
sections.push(`- [S${h.sprint}] ${h.type}: ${h.description}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch { /* skip section */ }
|
|
451
|
+
// Section 2: Common issues
|
|
452
|
+
try {
|
|
453
|
+
const issuesPath = join(cwd, slopeConfig.commonIssuesPath);
|
|
454
|
+
if (existsSync(issuesPath)) {
|
|
455
|
+
const issues = JSON.parse(readFileSync(issuesPath, 'utf8'));
|
|
456
|
+
const filtered = filterCommonIssues(issues, { keywords });
|
|
457
|
+
if (filtered.length > 0) {
|
|
458
|
+
sections.push('### Known Gotchas');
|
|
459
|
+
for (const p of filtered.slice(0, 5)) {
|
|
460
|
+
sections.push(`- [${p.category}] ${p.title}`);
|
|
461
|
+
sections.push(` Prevention: ${p.prevention.slice(0, 120)}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch { /* skip section */ }
|
|
467
|
+
// Section 3: Codebase map section
|
|
468
|
+
try {
|
|
469
|
+
const mapPath = join(cwd, 'CODEBASE.md');
|
|
470
|
+
if (existsSync(mapPath)) {
|
|
471
|
+
const mapContent = readFileSync(mapPath, 'utf8');
|
|
472
|
+
const relevantSection = extractMapSection(mapContent, ticket.modules);
|
|
473
|
+
if (relevantSection) {
|
|
474
|
+
sections.push('### Codebase Context');
|
|
475
|
+
sections.push(relevantSection);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch { /* skip section */ }
|
|
480
|
+
// Section 4: Handicap snapshot
|
|
481
|
+
try {
|
|
482
|
+
if (scorecards.length > 0) {
|
|
483
|
+
const card = computeHandicapCard(scorecards);
|
|
484
|
+
const last5 = card.last_5;
|
|
485
|
+
sections.push('### Handicap Snapshot (last 5)');
|
|
486
|
+
sections.push(`- Handicap: +${last5.handicap.toFixed(1)}`);
|
|
487
|
+
sections.push(`- GIR: ${last5.gir_pct.toFixed(1)}%`);
|
|
488
|
+
sections.push(`- Avg Putts: ${last5.avg_putts.toFixed(1)}`);
|
|
489
|
+
sections.push(`- Penalties: ${last5.penalties_per_round.toFixed(1)}/round`);
|
|
490
|
+
const mp = last5.miss_pattern;
|
|
491
|
+
const totalMisses = mp.long + mp.short + mp.left + mp.right;
|
|
492
|
+
if (totalMisses > 0) {
|
|
493
|
+
const dirs = ['long', 'short', 'left', 'right']
|
|
494
|
+
.filter(d => mp[d] > 0)
|
|
495
|
+
.map(d => `${d}:${mp[d]}`);
|
|
496
|
+
sections.push(`- Miss pattern: ${dirs.join(' ')}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch { /* skip section */ }
|
|
501
|
+
if (sections.length === 0)
|
|
502
|
+
return '';
|
|
503
|
+
let result = '## Sprint Context\n\n' + sections.join('\n');
|
|
504
|
+
const words = result.split(/\s+/);
|
|
505
|
+
if (words.length > wordBudget) {
|
|
506
|
+
result = words.slice(0, wordBudget).join(' ') + '\n...(truncated)';
|
|
507
|
+
}
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Extract the relevant section from CODEBASE.md based on module paths.
|
|
512
|
+
*/
|
|
513
|
+
export function extractMapSection(mapContent, modules) {
|
|
514
|
+
if (modules.length === 0)
|
|
515
|
+
return null;
|
|
516
|
+
const lines = mapContent.split('\n');
|
|
517
|
+
const matchedLines = [];
|
|
518
|
+
let capturing = false;
|
|
519
|
+
let captureDepth = 0;
|
|
520
|
+
for (const line of lines) {
|
|
521
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
|
|
522
|
+
if (headingMatch) {
|
|
523
|
+
const depth = headingMatch[1].length;
|
|
524
|
+
const title = headingMatch[2].toLowerCase();
|
|
525
|
+
const matches = modules.some(mod => {
|
|
526
|
+
const parts = mod.split('/').filter(p => p.length > 2 && !p.includes('.'));
|
|
527
|
+
return parts.some(part => title.includes(part.toLowerCase()));
|
|
528
|
+
});
|
|
529
|
+
if (matches && !capturing) {
|
|
530
|
+
capturing = true;
|
|
531
|
+
captureDepth = depth;
|
|
532
|
+
matchedLines.push(line);
|
|
533
|
+
}
|
|
534
|
+
else if (capturing && depth <= captureDepth) {
|
|
535
|
+
capturing = false;
|
|
536
|
+
if (matches) {
|
|
537
|
+
capturing = true;
|
|
538
|
+
captureDepth = depth;
|
|
539
|
+
matchedLines.push(line);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else if (capturing) {
|
|
543
|
+
matchedLines.push(line);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else if (capturing) {
|
|
547
|
+
matchedLines.push(line);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const result = matchedLines.join('\n').trim();
|
|
551
|
+
return result.length > 0 ? result : null;
|
|
552
|
+
}
|
|
553
|
+
export function runTool(name, input, cwd, log) {
|
|
554
|
+
try {
|
|
555
|
+
switch (name) {
|
|
556
|
+
case 'read_file': return toolReadFile(input, cwd);
|
|
557
|
+
case 'write_file': return toolWriteFile(input, cwd);
|
|
558
|
+
case 'edit_file': return toolEditFile(input, cwd);
|
|
559
|
+
case 'bash': return toolBash(input, cwd);
|
|
560
|
+
case 'glob': return toolGlob(input, cwd);
|
|
561
|
+
case 'grep': return toolGrep(input, cwd);
|
|
562
|
+
case 'slope': return toolSlope(input, cwd);
|
|
563
|
+
default: return { output: `Unknown tool: ${name}`, isError: true };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
568
|
+
log.warn(`Tool ${name} error: ${msg}`);
|
|
569
|
+
return { output: msg, isError: true };
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function toolReadFile(input, cwd) {
|
|
573
|
+
const abs = safePath(input.path, cwd);
|
|
574
|
+
if (!existsSync(abs))
|
|
575
|
+
return { output: `File not found: ${input.path}`, isError: true };
|
|
576
|
+
let content = readFileSync(abs, 'utf8');
|
|
577
|
+
if (content.length > FILE_READ_CAP) {
|
|
578
|
+
content = content.slice(0, FILE_READ_CAP) + '\n... (truncated at 100KB)';
|
|
579
|
+
}
|
|
580
|
+
return { output: content, isError: false };
|
|
581
|
+
}
|
|
582
|
+
function toolWriteFile(input, cwd) {
|
|
583
|
+
const abs = safePath(input.path, cwd);
|
|
584
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
585
|
+
writeFileSync(abs, input.content);
|
|
586
|
+
return { output: `Written: ${input.path}`, isError: false };
|
|
587
|
+
}
|
|
588
|
+
function toolEditFile(input, cwd) {
|
|
589
|
+
const abs = safePath(input.path, cwd);
|
|
590
|
+
if (!existsSync(abs))
|
|
591
|
+
return { output: `File not found: ${input.path}`, isError: true };
|
|
592
|
+
const content = readFileSync(abs, 'utf8');
|
|
593
|
+
const oldStr = input.old_string;
|
|
594
|
+
if (!content.includes(oldStr)) {
|
|
595
|
+
return {
|
|
596
|
+
output: `old_string not found in ${input.path}. Ensure exact match including whitespace and indentation.`,
|
|
597
|
+
isError: true,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
writeFileSync(abs, content.replace(oldStr, input.new_string));
|
|
601
|
+
return { output: `Edited: ${input.path}`, isError: false };
|
|
602
|
+
}
|
|
603
|
+
function toolBash(input, cwd) {
|
|
604
|
+
const cmd = input.command;
|
|
605
|
+
// Block destructive commands — the model should not push (loop handles it),
|
|
606
|
+
// delete broad filesystem paths, or run system-level commands
|
|
607
|
+
const blocked = BLOCKED_COMMANDS.find(([re]) => re.test(cmd));
|
|
608
|
+
if (blocked) {
|
|
609
|
+
return { output: `Blocked: ${blocked[1]}`, isError: true };
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const output = execSync(cmd, {
|
|
613
|
+
cwd,
|
|
614
|
+
encoding: 'utf8',
|
|
615
|
+
timeout: TOOL_BASH_TIMEOUT,
|
|
616
|
+
maxBuffer: 1024 * 1024,
|
|
617
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
618
|
+
});
|
|
619
|
+
return {
|
|
620
|
+
output: (output.length > TOOL_OUTPUT_CAP
|
|
621
|
+
? output.slice(0, TOOL_OUTPUT_CAP) + '\n... (truncated)'
|
|
622
|
+
: output) || '(no output)',
|
|
623
|
+
isError: false,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
const e = err;
|
|
628
|
+
const out = (e.stderr || '') + (e.stdout || '') || e.message || 'Command failed';
|
|
629
|
+
return {
|
|
630
|
+
output: typeof out === 'string' ? out.slice(0, TOOL_OUTPUT_CAP) : 'Command failed',
|
|
631
|
+
isError: true,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function toolGlob(input, cwd) {
|
|
636
|
+
const pattern = input.pattern;
|
|
637
|
+
try {
|
|
638
|
+
const output = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '--', pattern], { cwd, encoding: 'utf8', timeout: 10_000 });
|
|
639
|
+
const files = output.split('\n').filter(Boolean);
|
|
640
|
+
if (files.length === 0)
|
|
641
|
+
return { output: '(no matches)', isError: false };
|
|
642
|
+
return { output: files.slice(0, 200).join('\n'), isError: false };
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return { output: '(no matches)', isError: false };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function toolGrep(input, cwd) {
|
|
649
|
+
const pattern = input.pattern;
|
|
650
|
+
const searchPath = input.path || '.';
|
|
651
|
+
const include = input.include;
|
|
652
|
+
try {
|
|
653
|
+
const args = ['-rn', '--color=never'];
|
|
654
|
+
if (include) {
|
|
655
|
+
args.push(`--include=${include}`);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
args.push('--include=*.ts', '--include=*.js', '--include=*.json', '--include=*.md', '--include=*.sh');
|
|
659
|
+
}
|
|
660
|
+
args.push('--', pattern, searchPath);
|
|
661
|
+
const output = execFileSync('grep', args, {
|
|
662
|
+
cwd,
|
|
663
|
+
encoding: 'utf8',
|
|
664
|
+
timeout: 15_000,
|
|
665
|
+
maxBuffer: 512 * 1024,
|
|
666
|
+
});
|
|
667
|
+
const lines = output.split('\n').filter(Boolean);
|
|
668
|
+
if (lines.length === 0)
|
|
669
|
+
return { output: '(no matches)', isError: false };
|
|
670
|
+
return { output: lines.slice(0, 100).join('\n'), isError: false };
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
// grep exit code 1 = no matches
|
|
674
|
+
return { output: '(no matches)', isError: false };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function toolSlope(input, cwd) {
|
|
678
|
+
const command = (input.command ?? '').trim();
|
|
679
|
+
const parts = command.split(/\s+/);
|
|
680
|
+
const subcommand = parts[0];
|
|
681
|
+
if (!subcommand || !SLOPE_ALLOWLIST.has(subcommand)) {
|
|
682
|
+
return {
|
|
683
|
+
output: `Command "${subcommand ?? ''}" not in allowlist. Use one of: ${[...SLOPE_ALLOWLIST].join(', ')}`,
|
|
684
|
+
isError: true,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
const output = execFileSync('pnpm', ['slope', ...parts], {
|
|
689
|
+
cwd,
|
|
690
|
+
encoding: 'utf8',
|
|
691
|
+
timeout: 30_000,
|
|
692
|
+
});
|
|
693
|
+
if (output.length > SLOPE_TOOL_OUTPUT_CAP) {
|
|
694
|
+
return { output: output.slice(0, SLOPE_TOOL_OUTPUT_CAP) + '\n...(output truncated)', isError: false };
|
|
695
|
+
}
|
|
696
|
+
return { output: output || '(no output)', isError: false };
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
700
|
+
return { output: `slope ${command} failed: ${msg.slice(0, 200)}`, isError: true };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// ── Git helpers ─────────────────────────────────────
|
|
704
|
+
function gitCommit(ticketKey, title, cwd, log) {
|
|
705
|
+
try {
|
|
706
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
707
|
+
cwd,
|
|
708
|
+
encoding: 'utf8',
|
|
709
|
+
}).trim();
|
|
710
|
+
if (!status)
|
|
711
|
+
return [];
|
|
712
|
+
const files = status.split('\n').map(l => l.slice(3).trim()).filter(Boolean);
|
|
713
|
+
execFileSync('git', ['add', '-A'], { cwd, stdio: 'pipe' });
|
|
714
|
+
execFileSync('git', ['commit', '-m', `${ticketKey}: ${title}`], {
|
|
715
|
+
cwd,
|
|
716
|
+
stdio: 'pipe',
|
|
717
|
+
});
|
|
718
|
+
log.info(`Committed: ${ticketKey}: ${title} (${files.length} files)`);
|
|
719
|
+
return files;
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
log.warn(`Commit helper failed: ${err instanceof Error ? err.message : err}`);
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Run typecheck + tests inside the executor loop, giving the model
|
|
728
|
+
* a chance to self-correct before the outer guards revert everything.
|
|
729
|
+
* Returns null on success, or the failure details.
|
|
730
|
+
*/
|
|
731
|
+
function runInnerGuards(config, cwd, log) {
|
|
732
|
+
// Guard 1: Typecheck
|
|
733
|
+
try {
|
|
734
|
+
execSync('pnpm typecheck', { cwd, stdio: 'pipe', timeout: 120_000 });
|
|
735
|
+
log.info('Inner guard: typecheck passed');
|
|
736
|
+
}
|
|
737
|
+
catch (err) {
|
|
738
|
+
const e = err;
|
|
739
|
+
const output = ((e.stderr || '') + (e.stdout || '')).slice(0, 3000) || 'typecheck failed';
|
|
740
|
+
log.warn(`Inner guard failed: typecheck`);
|
|
741
|
+
return { guard: 'typecheck', output };
|
|
742
|
+
}
|
|
743
|
+
// Guard 2: Tests
|
|
744
|
+
try {
|
|
745
|
+
execSync(config.loopTestCmd, { cwd, stdio: 'pipe', timeout: 300_000 });
|
|
746
|
+
log.info('Inner guard: tests passed');
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
const e = err;
|
|
750
|
+
const output = ((e.stderr || '') + (e.stdout || '')).slice(0, 3000) || 'tests failed';
|
|
751
|
+
log.warn(`Inner guard failed: tests`);
|
|
752
|
+
return { guard: 'tests', output };
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
// ── Message truncation ──────────────────────────────
|
|
757
|
+
/**
|
|
758
|
+
* Truncate old tool result contents to prevent context window overflow.
|
|
759
|
+
* Keeps the first message (task prompt) and recent turns fully intact.
|
|
760
|
+
* Older tool results are shrunk to 200 chars.
|
|
761
|
+
*/
|
|
762
|
+
function truncateOldMessages(messages) {
|
|
763
|
+
if (messages.length <= TRUNCATE_KEEP_RECENT + 2)
|
|
764
|
+
return;
|
|
765
|
+
for (let i = 2; i < messages.length - TRUNCATE_KEEP_RECENT; i++) {
|
|
766
|
+
const msg = messages[i];
|
|
767
|
+
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
768
|
+
for (const block of msg.content) {
|
|
769
|
+
if (block.type === 'tool_result' &&
|
|
770
|
+
typeof block.content === 'string' &&
|
|
771
|
+
block.content.length > 200) {
|
|
772
|
+
block.content = block.content.slice(0, 200) + '\n[truncated]';
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// ── Utilities ───────────────────────────────────────
|
|
779
|
+
function sleep(ms) {
|
|
780
|
+
return new Promise(r => setTimeout(r, ms));
|
|
781
|
+
}
|
|
782
|
+
function errorResult(transcript, start) {
|
|
783
|
+
return {
|
|
784
|
+
outcome: 'error',
|
|
785
|
+
noop: false,
|
|
786
|
+
tokens_in: 0,
|
|
787
|
+
tokens_out: 0,
|
|
788
|
+
cost_usd: 0,
|
|
789
|
+
duration_s: Math.round((Date.now() - start) / 1000),
|
|
790
|
+
transcript,
|
|
791
|
+
files_changed: [],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
//# sourceMappingURL=slope-executor.js.map
|