skimpyclaw 0.3.14 → 0.4.0
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/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
// Code Agent Executor - Background execution logic
|
|
2
2
|
import { spawn, exec, execSync } from 'child_process';
|
|
3
3
|
import { createWriteStream, existsSync, readFileSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
/**
|
|
6
|
+
* Ensure PATH contains the directory of the running node binary. The claude /
|
|
7
|
+
* codex CLIs use `#!/usr/bin/env node` shebangs — when launchd-spawned processes
|
|
8
|
+
* inherit a minimal PATH, `env node` fails and spawn returns ENOENT.
|
|
9
|
+
*/
|
|
10
|
+
function ensureNodeInPath(env) {
|
|
11
|
+
const nodeDir = dirname(process.execPath);
|
|
12
|
+
const path = env.PATH ?? '';
|
|
13
|
+
if (!path.split(':').includes(nodeDir)) {
|
|
14
|
+
env.PATH = path ? `${nodeDir}:${path}` : nodeDir;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
5
17
|
// SKIMPYCLAW_ROOT for log paths
|
|
6
18
|
const SKIMPYCLAW_ROOT = join(import.meta.dirname || process.cwd(), '..', '..');
|
|
7
19
|
import { toErrorMessage } from '../utils.js';
|
|
8
20
|
import { VALIDATE_TIMEOUT_MS } from './types.js';
|
|
9
21
|
import { getCodeAgentsDir, ensureCodeAgentsDir, writeCodeAgentTask, setCodeAgentCanceller, deleteCodeAgentCanceller, getCodeAgent, } from './registry.js';
|
|
10
|
-
import { buildCodeAgentArgs, notifyCodeAgentResult } from './utils.js';
|
|
22
|
+
import { buildCodeAgentArgs, buildCodeAgentSpawnEnv, notifyCodeAgentResult } from './utils.js';
|
|
23
|
+
import { cleanupCodeAgentWorktree } from './worktrees.js';
|
|
11
24
|
import { parseStreamJsonForLive, parseClaudeOutput, parseCodexOutput } from './parser.js';
|
|
12
25
|
import { startTrace, addEvent, endTrace } from '../audit.js';
|
|
13
26
|
import { buildUsageRecord, recordUsage } from '../usage.js';
|
|
14
|
-
import { ensureContainer, SANDBOX_DEFAULTS, getRuntime } from '../sandbox/index.js';
|
|
15
27
|
const CANCELLED_MESSAGE = 'Cancelled by user';
|
|
16
28
|
/**
|
|
17
29
|
* Detect the package manager for a project directory.
|
|
@@ -112,6 +124,11 @@ function getChangedPackageDirs(workdir) {
|
|
|
112
124
|
return [];
|
|
113
125
|
}
|
|
114
126
|
}
|
|
127
|
+
// Reject any value that could break out of an unquoted shell argument.
|
|
128
|
+
// npm package names: optional "@scope/", then [a-zA-Z0-9._-]. Max 214 chars.
|
|
129
|
+
const NPM_PKG_NAME_RE = /^(?:@[a-zA-Z0-9._-]+\/)?[a-zA-Z0-9._-]+$/;
|
|
130
|
+
// Workspace directory paths derived from git: allow nested path segments only.
|
|
131
|
+
const SAFE_PATH_RE = /^[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)*$/;
|
|
115
132
|
/**
|
|
116
133
|
* Build scoped validation commands for a monorepo by detecting changed packages.
|
|
117
134
|
* Returns a combined command that builds/tests only affected packages, or null
|
|
@@ -127,6 +144,11 @@ function buildMonorepoValidationCommand(workdir) {
|
|
|
127
144
|
const pm = detectPackageManager(workdir);
|
|
128
145
|
const parts = [];
|
|
129
146
|
for (const dir of changedDirs) {
|
|
147
|
+
// Defence-in-depth: `dir` flows into shell command strings below.
|
|
148
|
+
if (!SAFE_PATH_RE.test(dir) || dir.length > 256) {
|
|
149
|
+
console.warn(`[validation] Skipping unsafe package dir: ${JSON.stringify(dir)}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
130
152
|
const pkgJsonPath = join(workdir, dir, 'package.json');
|
|
131
153
|
if (!existsSync(pkgJsonPath))
|
|
132
154
|
continue;
|
|
@@ -136,6 +158,13 @@ function buildMonorepoValidationCommand(workdir) {
|
|
|
136
158
|
const scripts = pkg.scripts || {};
|
|
137
159
|
if (!pkgName)
|
|
138
160
|
continue;
|
|
161
|
+
// pkgName comes from an untrusted package.json and is interpolated into
|
|
162
|
+
// a shell command executed by exec(). Enforce npm's legal name charset
|
|
163
|
+
// to prevent command injection via crafted "name" fields.
|
|
164
|
+
if (typeof pkgName !== 'string' || pkgName.length > 214 || !NPM_PKG_NAME_RE.test(pkgName)) {
|
|
165
|
+
console.warn(`[validation] Skipping package with unsafe name: ${JSON.stringify(pkgName)}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
139
168
|
// Build with workspace command
|
|
140
169
|
if (scripts.build) {
|
|
141
170
|
if (pm === 'pnpm')
|
|
@@ -252,7 +281,7 @@ export function buildValidationCommand(workdir, validationCommands) {
|
|
|
252
281
|
}
|
|
253
282
|
return parts.join(' && ');
|
|
254
283
|
}
|
|
255
|
-
/** Run build/test validation.
|
|
284
|
+
/** Run build/test validation. */
|
|
256
285
|
export function runValidation(workdir, validationCommands) {
|
|
257
286
|
const cmd = buildValidationCommand(workdir, validationCommands);
|
|
258
287
|
if (!cmd) {
|
|
@@ -323,7 +352,9 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
323
352
|
agent,
|
|
324
353
|
workdir,
|
|
325
354
|
model: input.model,
|
|
355
|
+
effort: input.effort,
|
|
326
356
|
max_turns: input.max_turns,
|
|
357
|
+
sessionId: caTask.cliSessionId,
|
|
327
358
|
});
|
|
328
359
|
let stdout = '';
|
|
329
360
|
let stderr = '';
|
|
@@ -331,29 +362,25 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
331
362
|
const logPath = join(getCodeAgentsDir(), `${id}.log`);
|
|
332
363
|
ensureCodeAgentsDir();
|
|
333
364
|
const logStream = createWriteStream(logPath, { flags: 'w' });
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
365
|
+
let logStreamEnded = false;
|
|
366
|
+
const logWrite = (data) => { if (!logStreamEnded)
|
|
367
|
+
logStream.write(data); };
|
|
368
|
+
logWrite(`=== ${id} | ${agent} | ${new Date().toISOString()} ===\n`);
|
|
369
|
+
logWrite(`Task: ${task.slice(0, 500)}\n`);
|
|
370
|
+
logWrite(`Workdir: ${workdir}\n\n`);
|
|
371
|
+
if (caTask.sourceWorkdir)
|
|
372
|
+
logWrite(`Source workdir: ${caTask.sourceWorkdir}\n`);
|
|
373
|
+
if (caTask.worktreePath)
|
|
374
|
+
logWrite(`Worktree: ${caTask.worktreePath}\n`);
|
|
337
375
|
try {
|
|
338
|
-
// Resolve sandbox container name if enabled (used for spawn wrapping)
|
|
339
|
-
let sandboxContainer;
|
|
340
|
-
if (options?.sandboxConfig?.enabled) {
|
|
341
|
-
const merged = { ...SANDBOX_DEFAULTS, ...options.sandboxConfig };
|
|
342
|
-
sandboxContainer = await ensureContainer(`code-${id}`, merged, options.allowedPaths || [workdir]);
|
|
343
|
-
console.log(`[code-agent] Running in sandbox container: ${sandboxContainer}`);
|
|
344
|
-
}
|
|
345
376
|
ensureNotCancelled();
|
|
346
377
|
const exitCode = await new Promise((resolvePromise, reject) => {
|
|
347
|
-
const spawnEnv =
|
|
348
|
-
|
|
349
|
-
// Apply extra env vars (e.g. team mode feature flag)
|
|
378
|
+
const spawnEnv = buildCodeAgentSpawnEnv();
|
|
379
|
+
ensureNodeInPath(spawnEnv);
|
|
350
380
|
if (options?.env)
|
|
351
381
|
Object.assign(spawnEnv, options.env);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const spawnArgs = sandboxContainer ? ['exec', sandboxContainer, cmd, ...args] : args;
|
|
355
|
-
const proc = spawn(spawnCmd, spawnArgs, {
|
|
356
|
-
cwd: sandboxContainer ? undefined : workdir, // container has its own cwd
|
|
382
|
+
const proc = spawn(cmd, args, {
|
|
383
|
+
cwd: workdir,
|
|
357
384
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
358
385
|
env: spawnEnv,
|
|
359
386
|
});
|
|
@@ -362,7 +389,7 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
362
389
|
const STATUS_WRITE_INTERVAL = 3000;
|
|
363
390
|
proc.stdout.on('data', (chunk) => {
|
|
364
391
|
stdout += chunk.toString();
|
|
365
|
-
|
|
392
|
+
logWrite(chunk);
|
|
366
393
|
const now = Date.now();
|
|
367
394
|
if (now - lastStatusWrite > STATUS_WRITE_INTERVAL) {
|
|
368
395
|
lastStatusWrite = now;
|
|
@@ -375,7 +402,7 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
375
402
|
});
|
|
376
403
|
proc.stderr.on('data', (chunk) => {
|
|
377
404
|
stderr += chunk.toString();
|
|
378
|
-
|
|
405
|
+
logWrite(chunk);
|
|
379
406
|
const now = Date.now();
|
|
380
407
|
if (now - lastStatusWrite > STATUS_WRITE_INTERVAL) {
|
|
381
408
|
lastStatusWrite = now;
|
|
@@ -412,8 +439,11 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
412
439
|
clearTimeout(activeTimer);
|
|
413
440
|
activeTimer = null;
|
|
414
441
|
activeProc = null;
|
|
415
|
-
|
|
416
|
-
|
|
442
|
+
if (!logStreamEnded) {
|
|
443
|
+
logStreamEnded = true;
|
|
444
|
+
logStream.write(`\n=== EXIT ${code} | ${new Date().toISOString()} ===\n`);
|
|
445
|
+
logStream.end();
|
|
446
|
+
}
|
|
417
447
|
if (cancelled || caTask.status === 'cancelled') {
|
|
418
448
|
reject(new Error(CANCELLED_MESSAGE));
|
|
419
449
|
return;
|
|
@@ -430,8 +460,11 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
430
460
|
clearTimeout(activeTimer);
|
|
431
461
|
activeTimer = null;
|
|
432
462
|
activeProc = null;
|
|
433
|
-
|
|
434
|
-
|
|
463
|
+
if (!logStreamEnded) {
|
|
464
|
+
logStreamEnded = true;
|
|
465
|
+
logStream.write(`\n=== ERROR: ${err.message} ===\n`);
|
|
466
|
+
logStream.end();
|
|
467
|
+
}
|
|
435
468
|
reject(err);
|
|
436
469
|
});
|
|
437
470
|
});
|
|
@@ -479,8 +512,7 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
479
512
|
liveOutput: undefined,
|
|
480
513
|
});
|
|
481
514
|
writeCodeAgentTask(caTask);
|
|
482
|
-
|
|
483
|
-
await notifyCodeAgentResult(caTask, (id) => getCodeAgent(id) ?? null);
|
|
515
|
+
await notifyCodeAgentResult(caTask);
|
|
484
516
|
return;
|
|
485
517
|
}
|
|
486
518
|
// Post-validation gate
|
|
@@ -490,6 +522,25 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
490
522
|
caTask.liveOutput = undefined;
|
|
491
523
|
writeCodeAgentTask(caTask);
|
|
492
524
|
const validationCmd = buildValidationCommand(workdir, options?.validationCommands);
|
|
525
|
+
if (!validationCmd) {
|
|
526
|
+
// No build/test scripts — skip validation, mark complete
|
|
527
|
+
const endedAt = new Date();
|
|
528
|
+
const duration = Math.round((endedAt.getTime() - startedAt.getTime()) / 1000);
|
|
529
|
+
addEvent(traceId, { type: 'validation', summary: 'Skipped (no build/test scripts found)', durationMs: Date.now() - startedAt.getTime() });
|
|
530
|
+
await endTrace(traceId, 'ok');
|
|
531
|
+
Object.assign(caTask, {
|
|
532
|
+
status: 'completed',
|
|
533
|
+
endedAt: endedAt.toISOString(),
|
|
534
|
+
durationSeconds: duration,
|
|
535
|
+
exitCode,
|
|
536
|
+
validationPassed: true,
|
|
537
|
+
validationOutput: undefined,
|
|
538
|
+
outputPreview: agentOutput.slice(0, 5000),
|
|
539
|
+
});
|
|
540
|
+
writeCodeAgentTask(caTask);
|
|
541
|
+
await notifyCodeAgentResult(caTask);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
493
544
|
const runValidationPromise = () => new Promise((res) => {
|
|
494
545
|
const validationProc = exec(validationCmd, {
|
|
495
546
|
cwd: workdir,
|
|
@@ -531,15 +582,14 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
531
582
|
agent,
|
|
532
583
|
workdir,
|
|
533
584
|
model: input.model,
|
|
585
|
+
effort: input.effort,
|
|
534
586
|
max_turns: input.max_turns,
|
|
535
587
|
});
|
|
536
588
|
const retryExitCode = await new Promise((resolveRetry, rejectRetry) => {
|
|
537
|
-
const spawnEnv =
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
const retryProc = spawn(retrySpawnCmd, retrySpawnArgs, {
|
|
542
|
-
cwd: sandboxContainer ? undefined : workdir,
|
|
589
|
+
const spawnEnv = buildCodeAgentSpawnEnv();
|
|
590
|
+
ensureNodeInPath(spawnEnv);
|
|
591
|
+
const retryProc = spawn(retryCmd, retryArgs, {
|
|
592
|
+
cwd: workdir,
|
|
543
593
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
544
594
|
env: spawnEnv,
|
|
545
595
|
});
|
|
@@ -633,7 +683,7 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
633
683
|
}
|
|
634
684
|
}
|
|
635
685
|
else {
|
|
636
|
-
agentOutput = stdout
|
|
686
|
+
agentOutput = parseCodexOutput(stdout);
|
|
637
687
|
}
|
|
638
688
|
caTask.status = 'validating';
|
|
639
689
|
caTask.liveOutput = undefined;
|
|
@@ -659,8 +709,7 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
659
709
|
error: 'Validation failed',
|
|
660
710
|
});
|
|
661
711
|
writeCodeAgentTask(caTask);
|
|
662
|
-
|
|
663
|
-
await notifyCodeAgentResult(caTask, (id) => getCodeAgent(id) ?? null);
|
|
712
|
+
await notifyCodeAgentResult(caTask);
|
|
664
713
|
return;
|
|
665
714
|
}
|
|
666
715
|
addEvent(traceId, { type: 'validation', summary: 'Build/test validation passed', durationMs: Date.now() - startedAt.getTime() });
|
|
@@ -675,8 +724,7 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
675
724
|
outputPreview: agentOutput.slice(0, 5000),
|
|
676
725
|
});
|
|
677
726
|
writeCodeAgentTask(caTask);
|
|
678
|
-
|
|
679
|
-
await notifyCodeAgentResult(caTask, (id) => getCodeAgent(id) ?? null);
|
|
727
|
+
await notifyCodeAgentResult(caTask);
|
|
680
728
|
return;
|
|
681
729
|
}
|
|
682
730
|
// No validation — mark complete
|
|
@@ -691,8 +739,7 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
691
739
|
liveOutput: undefined,
|
|
692
740
|
});
|
|
693
741
|
writeCodeAgentTask(caTask);
|
|
694
|
-
|
|
695
|
-
await notifyCodeAgentResult(caTask, (id) => getCodeAgent(id) ?? null);
|
|
742
|
+
await notifyCodeAgentResult(caTask);
|
|
696
743
|
}
|
|
697
744
|
catch (err) {
|
|
698
745
|
const errMsg = toErrorMessage(err);
|
|
@@ -710,8 +757,8 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
710
757
|
liveOutput: undefined,
|
|
711
758
|
});
|
|
712
759
|
writeCodeAgentTask(caTask);
|
|
713
|
-
if (
|
|
714
|
-
await notifyCodeAgentResult(caTask
|
|
760
|
+
if (caTask.status !== "cancelled")
|
|
761
|
+
await notifyCodeAgentResult(caTask);
|
|
715
762
|
}
|
|
716
763
|
finally {
|
|
717
764
|
if (activeTimer)
|
|
@@ -719,6 +766,15 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
719
766
|
activeTimer = null;
|
|
720
767
|
activeProc = null;
|
|
721
768
|
activeExecProc = null;
|
|
769
|
+
if (caTask.worktreePath && !['running', 'validating', 'pending'].includes(caTask.status)) {
|
|
770
|
+
caTask.worktreeCleanup = cleanupCodeAgentWorktree({
|
|
771
|
+
sourceWorkdir: caTask.sourceWorkdir,
|
|
772
|
+
worktreePath: caTask.worktreePath,
|
|
773
|
+
worktreeRef: caTask.worktreeRef,
|
|
774
|
+
config: options?.worktreeConfig,
|
|
775
|
+
});
|
|
776
|
+
writeCodeAgentTask(caTask);
|
|
777
|
+
}
|
|
722
778
|
deleteCodeAgentCanceller(id);
|
|
723
779
|
}
|
|
724
780
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { ToolConfig } from '../types.js';
|
|
2
2
|
import type { ExecuteToolContext } from '../tools/execute-context.js';
|
|
3
|
-
export type { CodeAgentTask,
|
|
3
|
+
export type { CodeAgentTask, CodeAgentBackgroundOptions, BuildCodeAgentArgsInput, ValidationResult, } from './types.js';
|
|
4
4
|
export { CODE_AGENT_TIMEOUT_MS, VALIDATE_TIMEOUT_MS } from './types.js';
|
|
5
5
|
export { getActiveCodeAgents, getRecentCodeAgents, getAllCodeAgents, getCodeAgent, cancelCodeAgent, restoreCodeAgentTasks, getCodeAgentsDir, } from './registry.js';
|
|
6
6
|
export { runCodeAgentBackground, runValidation } from './executor.js';
|
|
7
|
-
export {
|
|
8
|
-
export { setCodeAgentConfig, getCodeAgentConfig, buildCodeAgentArgs, resolveSelectedCodeAgent, resolveWorkdir, resolveModelAlias, readTeamState, } from './utils.js';
|
|
7
|
+
export { setCodeAgentConfig, getCodeAgentConfig, buildCodeAgentArgs, resolveSelectedCodeAgent, resolveCodeAgentModelLabel, resolveWorkdir, resolveModelAlias, } from './utils.js';
|
|
9
8
|
export { parseStreamJsonForLive, parseClaudeOutput, parseCodexOutput } from './parser.js';
|
|
10
9
|
export type { ClaudeOutputResult } from './parser.js';
|
|
11
10
|
/**
|
|
@@ -16,7 +15,3 @@ export declare function executeCheckCodeAgent(input: Record<string, any>): strin
|
|
|
16
15
|
* Execute code_with_agent tool - single agent mode.
|
|
17
16
|
*/
|
|
18
17
|
export declare function executeCodeWithAgent(input: Record<string, any>, config: ToolConfig, context?: ExecuteToolContext): Promise<string>;
|
|
19
|
-
/**
|
|
20
|
-
* Execute code_with_team tool - multi-agent team mode.
|
|
21
|
-
*/
|
|
22
|
-
export declare function executeCodeWithTeam(input: Record<string, any>, config: ToolConfig, context?: ExecuteToolContext): Promise<string>;
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
// Code Agents - Public API
|
|
2
2
|
// Background multi-agent coding task execution
|
|
3
3
|
import { resolve } from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
4
5
|
import { isPathAllowed } from '../tools/path-utils.js';
|
|
5
6
|
import { getNextCodeAgentId, storeCodeAgentTask, writeCodeAgentTask, getActiveCodeAgents, getRecentCodeAgents, getCodeAgent, } from './registry.js';
|
|
6
7
|
import { runCodeAgentBackground } from './executor.js';
|
|
7
|
-
import {
|
|
8
|
-
import { resolveSelectedCodeAgent, resolveWorkdir, resolveModelAlias, getCodingCliPreflightError, } from './utils.js';
|
|
8
|
+
import { addPendingSession } from './interactive-sessions.js';
|
|
9
|
+
import { resolveSelectedCodeAgent, resolveCodeAgentModelLabel, isModelCompatibleWithAgent, resolveWorkdir, resolveModelAlias, getCodingCliPreflightError, } from './utils.js';
|
|
10
|
+
import { normalizeWorktreeRequest, prepareCodeAgentWorktree, shouldUseCodeAgentWorktree, } from './worktrees.js';
|
|
9
11
|
// Re-export timeout constants
|
|
10
12
|
export { CODE_AGENT_TIMEOUT_MS, VALIDATE_TIMEOUT_MS } from './types.js';
|
|
11
13
|
// Re-export registry functions
|
|
12
14
|
export { getActiveCodeAgents, getRecentCodeAgents, getAllCodeAgents, getCodeAgent, cancelCodeAgent, restoreCodeAgentTasks, getCodeAgentsDir, } from './registry.js';
|
|
13
15
|
// Re-export executor functions
|
|
14
16
|
export { runCodeAgentBackground, runValidation } from './executor.js';
|
|
15
|
-
// Re-export orchestrator functions
|
|
16
|
-
export { runTeamOrchestrator, computeWaves, decomposeTask, synthesizeResults, gatherCodebaseContext, } from './orchestrator.js';
|
|
17
17
|
// Re-export utility functions
|
|
18
|
-
export { setCodeAgentConfig, getCodeAgentConfig, buildCodeAgentArgs, resolveSelectedCodeAgent, resolveWorkdir, resolveModelAlias,
|
|
18
|
+
export { setCodeAgentConfig, getCodeAgentConfig, buildCodeAgentArgs, resolveSelectedCodeAgent, resolveCodeAgentModelLabel, resolveWorkdir, resolveModelAlias, } from './utils.js';
|
|
19
19
|
// Re-export parser functions
|
|
20
20
|
export { parseStreamJsonForLive, parseClaudeOutput, parseCodexOutput } from './parser.js';
|
|
21
21
|
// SKIMPYCLAW_ROOT for workdir default
|
|
22
22
|
const SKIMPYCLAW_ROOT = resolve(import.meta.dirname || process.cwd(), '..', '..');
|
|
23
|
+
const EFFORT_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh']);
|
|
23
24
|
/**
|
|
24
25
|
* Execute check_code_agent tool — list all or get details for one agent.
|
|
25
26
|
*/
|
|
@@ -35,7 +36,12 @@ export function executeCheckCodeAgent(input) {
|
|
|
35
36
|
status: task.status,
|
|
36
37
|
task: task.task,
|
|
37
38
|
workdir: task.workdir,
|
|
39
|
+
sourceWorkdir: task.sourceWorkdir,
|
|
40
|
+
worktreePath: task.worktreePath,
|
|
41
|
+
worktreeRef: task.worktreeRef,
|
|
38
42
|
model: task.model,
|
|
43
|
+
modelLabel: task.modelLabel || resolveCodeAgentModelLabel(task.agent, task.model),
|
|
44
|
+
effort: task.effort,
|
|
39
45
|
startedAt: task.startedAt,
|
|
40
46
|
endedAt: task.endedAt,
|
|
41
47
|
durationSeconds: task.durationSeconds,
|
|
@@ -58,7 +64,9 @@ export function executeCheckCodeAgent(input) {
|
|
|
58
64
|
? (t.durationSeconds < 60 ? `${t.durationSeconds}s` : `${Math.floor(t.durationSeconds / 60)}m`)
|
|
59
65
|
: (Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1000) + 's');
|
|
60
66
|
const taskPreview = t.task.length > 60 ? t.task.slice(0, 60) + '...' : t.task;
|
|
61
|
-
|
|
67
|
+
const model = `, ${t.modelLabel || resolveCodeAgentModelLabel(t.agent, t.model)}`;
|
|
68
|
+
const effort = t.effort ? `, effort ${t.effort}` : '';
|
|
69
|
+
return `${t.id}: ${t.status.toUpperCase()} (${t.agent}${model}${effort}, ${elapsed}) — ${taskPreview}`;
|
|
62
70
|
});
|
|
63
71
|
return lines.join('\n');
|
|
64
72
|
}
|
|
@@ -70,7 +78,7 @@ export async function executeCodeWithAgent(input, config, context) {
|
|
|
70
78
|
if (!task)
|
|
71
79
|
return 'Error: task is required';
|
|
72
80
|
// Resolve model alias first so agent auto-selection can inspect it.
|
|
73
|
-
// Fall back to current session model so
|
|
81
|
+
// Fall back to current session model so Codex models auto-select the right CLI.
|
|
74
82
|
let rawModel = input.model;
|
|
75
83
|
if (!rawModel) {
|
|
76
84
|
try {
|
|
@@ -84,7 +92,42 @@ export async function executeCodeWithAgent(input, config, context) {
|
|
|
84
92
|
const requestedAgent = input.agent;
|
|
85
93
|
const agent = resolveSelectedCodeAgent(requestedAgent, configDefault, resolvedModel);
|
|
86
94
|
if (!agent) {
|
|
87
|
-
return `Error: Invalid agent "${requestedAgent}". Must be claude
|
|
95
|
+
return `Error: Invalid agent "${requestedAgent}". Must be claude or codex.`;
|
|
96
|
+
}
|
|
97
|
+
// Interactive mode prerequisites: Discord server channel (NOT DM) + claude only.
|
|
98
|
+
// Discord DMs do not support threads, which interactive mode requires.
|
|
99
|
+
const isInteractive = input.interactive === true;
|
|
100
|
+
if (isInteractive) {
|
|
101
|
+
if (context?.channel !== 'discord') {
|
|
102
|
+
return 'Error: interactive mode requires Discord. Telegram and other channels are not supported yet.';
|
|
103
|
+
}
|
|
104
|
+
if (context?.isDm === true) {
|
|
105
|
+
return 'Error: interactive mode requires a Discord server channel. Direct messages do not support threads. Please move to a server channel and try again.';
|
|
106
|
+
}
|
|
107
|
+
if (agent !== 'claude' && agent !== 'codex') {
|
|
108
|
+
return `Error: interactive mode supports claude and codex only (requested: ${agent}).`;
|
|
109
|
+
}
|
|
110
|
+
if (agent === 'codex') {
|
|
111
|
+
return 'Error: interactive mode for codex is not yet implemented. Use claude for now.';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Don't pass a non-matching session model to a different agent CLI.
|
|
115
|
+
// e.g. if session is gpt-5.3-codex but agent is claude, let claude use its own default.
|
|
116
|
+
const isModelFromSession = !input.model;
|
|
117
|
+
const modelIncompatible = isModelFromSession && resolvedModel && !isModelCompatibleWithAgent(resolvedModel, agent);
|
|
118
|
+
const modelForAgent = modelIncompatible ? undefined : resolvedModel;
|
|
119
|
+
const effortInput = typeof input.effort === 'string'
|
|
120
|
+
? input.effort
|
|
121
|
+
: typeof input.thinking === 'string'
|
|
122
|
+
? input.thinking
|
|
123
|
+
: typeof input.reasoning_effort === 'string'
|
|
124
|
+
? input.reasoning_effort
|
|
125
|
+
: undefined;
|
|
126
|
+
const effort = effortInput
|
|
127
|
+
? effortInput.trim().toLowerCase().replace(/^x[-_ ]?high$/, 'xhigh')
|
|
128
|
+
: undefined;
|
|
129
|
+
if (effort && !EFFORT_LEVELS.has(effort)) {
|
|
130
|
+
return `Error: Invalid effort "${effortInput}". Use none, low, medium, high, or xhigh.`;
|
|
88
131
|
}
|
|
89
132
|
const projects = context?.fullConfig?.projects ?? {};
|
|
90
133
|
const rawWorkdir = input.workdir;
|
|
@@ -112,96 +155,84 @@ export async function executeCodeWithAgent(input, config, context) {
|
|
|
112
155
|
// Create task with unique ID
|
|
113
156
|
const id = getNextCodeAgentId();
|
|
114
157
|
const startedAt = new Date();
|
|
158
|
+
const worktreeRequest = normalizeWorktreeRequest(input.worktree);
|
|
159
|
+
const worktreeConfig = context?.fullConfig?.codeAgents?.worktrees;
|
|
160
|
+
const useWorktree = shouldUseCodeAgentWorktree(task, worktreeRequest, worktreeConfig);
|
|
161
|
+
const worktreeRequired = worktreeRequest === true || worktreeConfig?.mode === 'always';
|
|
162
|
+
let executionWorkdir = workdir;
|
|
163
|
+
let sourceWorkdir;
|
|
164
|
+
let worktreePath;
|
|
165
|
+
let worktreeRef;
|
|
166
|
+
let agentTask = task;
|
|
167
|
+
if (useWorktree) {
|
|
168
|
+
try {
|
|
169
|
+
const worktree = prepareCodeAgentWorktree({
|
|
170
|
+
id,
|
|
171
|
+
sourceWorkdir: workdir,
|
|
172
|
+
config: worktreeConfig,
|
|
173
|
+
required: worktreeRequired,
|
|
174
|
+
});
|
|
175
|
+
if (worktree) {
|
|
176
|
+
executionWorkdir = worktree.runWorkdir;
|
|
177
|
+
sourceWorkdir = worktree.sourceWorkdir;
|
|
178
|
+
worktreePath = worktree.worktreePath;
|
|
179
|
+
worktreeRef = worktree.worktreeRef;
|
|
180
|
+
agentTask = `${task}\n\nSkimpyClaw worktree isolation:\n- Source checkout: ${sourceWorkdir}\n- Isolated worktree: ${worktreePath}\n- Run all repository commands from the isolated worktree, not the source checkout.\n- If rebasing a branch that is already checked out elsewhere, create a temporary branch in this worktree and report before pushing.`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Interactive mode: generate session UUID up-front so --session-id can pin it.
|
|
188
|
+
const cliSessionId = (isInteractive && agent === 'claude') ? randomUUID() : undefined;
|
|
115
189
|
const caTask = {
|
|
116
190
|
id,
|
|
117
191
|
agent,
|
|
118
192
|
task,
|
|
119
193
|
status: 'running',
|
|
120
194
|
chatId: context?.chatId,
|
|
195
|
+
discordThreadId: context?.discordThreadId,
|
|
196
|
+
discordChannelId: context?.discordChannelId,
|
|
121
197
|
startedAt: startedAt.toISOString(),
|
|
122
|
-
workdir,
|
|
123
|
-
|
|
198
|
+
workdir: executionWorkdir,
|
|
199
|
+
sourceWorkdir,
|
|
200
|
+
worktreePath,
|
|
201
|
+
worktreeRef,
|
|
202
|
+
model: modelForAgent,
|
|
203
|
+
modelLabel: resolveCodeAgentModelLabel(agent, modelForAgent),
|
|
204
|
+
effort,
|
|
205
|
+
interactive: isInteractive || undefined,
|
|
206
|
+
cliSessionId,
|
|
124
207
|
};
|
|
125
208
|
storeCodeAgentTask(caTask);
|
|
126
209
|
writeCodeAgentTask(caTask);
|
|
210
|
+
// Register a pending interactive session keyed by the task ID.
|
|
211
|
+
// The Discord thread-creation handler will call linkThread(taskId, threadId)
|
|
212
|
+
// to promote it into the threadId-keyed map so follow-up messages work.
|
|
213
|
+
if (isInteractive && cliSessionId) {
|
|
214
|
+
addPendingSession(id, {
|
|
215
|
+
cliSessionId,
|
|
216
|
+
cliAgent: agent,
|
|
217
|
+
status: 'active',
|
|
218
|
+
createdAt: startedAt.toISOString(),
|
|
219
|
+
lastActivityAt: startedAt.toISOString(),
|
|
220
|
+
initialTask: task,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
127
223
|
// Fire-and-forget: spawn background process
|
|
128
224
|
const configTimeout = context?.fullConfig?.codeAgents?.timeoutMinutes ?? 30;
|
|
129
225
|
const soloTimeout = Math.min(input.timeout_minutes || configTimeout, 60);
|
|
130
|
-
const resolvedInput = { ...input, model:
|
|
131
|
-
runCodeAgentBackground(id, agent,
|
|
226
|
+
const resolvedInput = { ...input, model: modelForAgent, effort, timeout_minutes: soloTimeout };
|
|
227
|
+
runCodeAgentBackground(id, agent, agentTask, executionWorkdir, validate, resolvedInput, startedAt, {
|
|
132
228
|
defaultTimeoutMinutes: soloTimeout,
|
|
133
229
|
maxTimeoutMinutes: 60,
|
|
134
230
|
validationCommands: context?.fullConfig?.codeAgents?.validationCommands,
|
|
231
|
+
worktreeConfig,
|
|
135
232
|
}).catch((err) => {
|
|
136
233
|
console.error(`[code-agent] Background error for ${id}:`, err);
|
|
137
234
|
});
|
|
138
235
|
const taskPreview = task.length > 100 ? task.slice(0, 100) + '...' : task;
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Execute code_with_team tool - multi-agent team mode.
|
|
143
|
-
*/
|
|
144
|
-
export async function executeCodeWithTeam(input, config, context) {
|
|
145
|
-
const task = input.task;
|
|
146
|
-
if (!task)
|
|
147
|
-
return 'Error: task is required';
|
|
148
|
-
// Resolve model alias. Only fall back to session model when an explicit model
|
|
149
|
-
// was requested — otherwise the session model (e.g. gpt-5.3-codex) would
|
|
150
|
-
// override agent selection even when the user wants claude.
|
|
151
|
-
const rawTeamModel = input.model;
|
|
152
|
-
const resolvedModel = rawTeamModel
|
|
153
|
-
? resolveModelAlias(rawTeamModel, context?.fullConfig?.models?.aliases)
|
|
154
|
-
: undefined;
|
|
155
|
-
const configDefault = context?.fullConfig?.codeAgents?.defaultAgent || 'claude';
|
|
156
|
-
const requestedAgent = input.agent;
|
|
157
|
-
const agent = resolveSelectedCodeAgent(requestedAgent, configDefault, resolvedModel);
|
|
158
|
-
if (!agent) {
|
|
159
|
-
return `Error: Invalid agent "${requestedAgent}". Must be claude, codex, or kimi.`;
|
|
160
|
-
}
|
|
161
|
-
const teamSize = Math.max(2, Math.min(5, input.team_size || 3));
|
|
162
|
-
const projects = context?.fullConfig?.projects ?? {};
|
|
163
|
-
const rawWorkdir = input.workdir;
|
|
164
|
-
// Resolve project name → path
|
|
165
|
-
const workdir = resolveWorkdir(rawWorkdir, projects, SKIMPYCLAW_ROOT);
|
|
166
|
-
// Project paths are always allowed
|
|
167
|
-
const projectPaths = Object.values(projects).map(p => resolve(p));
|
|
168
|
-
const effectiveAllowedPaths = [...config.allowedPaths, ...projectPaths];
|
|
169
|
-
if (!isPathAllowed(workdir, effectiveAllowedPaths)) {
|
|
170
|
-
const projectNames = Object.keys(projects).length > 0
|
|
171
|
-
? ` (or project names: ${Object.keys(projects).join(', ')})`
|
|
172
|
-
: '';
|
|
173
|
-
return `Error: Working directory not allowed. Permitted: ${config.allowedPaths.join(', ')}${projectNames}`;
|
|
174
|
-
}
|
|
175
|
-
const validate = input.validate !== false;
|
|
176
|
-
// Concurrency check — need room for teamSize children
|
|
177
|
-
const maxConcurrent = context?.fullConfig?.codeAgents?.maxConcurrent ?? 5;
|
|
178
|
-
const activeCount = getActiveCodeAgents().length;
|
|
179
|
-
if (activeCount + teamSize > maxConcurrent) {
|
|
180
|
-
return `Error: Concurrency limit — need ${teamSize} slots but only ${maxConcurrent - activeCount} available (${activeCount}/${maxConcurrent} running). Wait for agents to finish.`;
|
|
181
|
-
}
|
|
182
|
-
const cliPreflightError = getCodingCliPreflightError();
|
|
183
|
-
if (cliPreflightError)
|
|
184
|
-
return cliPreflightError;
|
|
185
|
-
// Create parent task
|
|
186
|
-
const id = getNextCodeAgentId();
|
|
187
|
-
const startedAt = new Date();
|
|
188
|
-
const caTask = {
|
|
189
|
-
id,
|
|
190
|
-
agent: 'team-coordinator',
|
|
191
|
-
task,
|
|
192
|
-
status: 'running',
|
|
193
|
-
chatId: context?.chatId,
|
|
194
|
-
startedAt: startedAt.toISOString(),
|
|
195
|
-
workdir,
|
|
196
|
-
model: resolvedModel,
|
|
197
|
-
childTaskIds: [],
|
|
198
|
-
};
|
|
199
|
-
storeCodeAgentTask(caTask);
|
|
200
|
-
writeCodeAgentTask(caTask);
|
|
201
|
-
// Fire-and-forget: orchestrator decomposes, spawns children, monitors, synthesizes
|
|
202
|
-
runTeamOrchestrator(id, task, teamSize, workdir, validate, agent, resolvedModel, startedAt, context).catch((err) => {
|
|
203
|
-
console.error(`[code-team] Background error for ${id}:`, err);
|
|
204
|
-
});
|
|
205
|
-
const taskPreview = task.length > 100 ? task.slice(0, 100) + '...' : task;
|
|
206
|
-
return `Started coding team ${id} (${teamSize} parallel ${agent} agents). Task: ${taskPreview}\n\nUse check_code_agent to poll status.`;
|
|
236
|
+
const worktreeLine = worktreePath ? `\nWorktree: ${worktreePath}` : '';
|
|
237
|
+
return `Started coding agent ${id} (${agent}). Task: ${taskPreview}${worktreeLine}\n\nUse check_code_agent to poll status.`;
|
|
207
238
|
}
|