Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ Sends notifications for:
- PR opened (each run)
- Failures (each run)

## Error Codes

When a task fails, the failure reason includes a structured error code in brackets (e.g. `[ERR_GIT_FAILED] Git setup failed: ...`). These codes appear in ATS task failure reasons, log output, and Telegram notifications.

| Code | Meaning |
|------|---------|
| `ERR_TASK_NOT_FOUND` | Could not fetch or find the task in ATS |
| `ERR_CLAIM_FAILED` | Failed to claim the run task lease |
| `ERR_CLAUDE_TIMEOUT` | Claude Code timed out or exited with a non-zero code |
| `ERR_GIT_FAILED` | Git operation failed (checkout, pull, branch creation, push) |
| `ERR_PR_FAILED` | `gh pr create` failed |

Errors that don't match a known code are reported as `ERR_UNKNOWN`.

## Original Task Policy

The runner **never** claims, completes, fails, or modifies the original task. It is treated as a read-only trigger/reference. All state management happens on the suffixed copy tasks.
81 changes: 58 additions & 23 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ const ACTOR_FLAGS = ['--actor-type', 'agent', '--actor-id', 'ats-project-runner'
const ONESHOT_KEYWORDS = ['fix typo', 'update version', 'rename', 'bump', 'typo', 'version bump'];
const ITERATIVE_KEYWORDS = ['add', 'implement', 'refactor', 'debug', 'investigate', 'build', 'create', 'feature'];

// === Error codes ===
const ERR_TASK_NOT_FOUND = 'ERR_TASK_NOT_FOUND';
const ERR_CLAIM_FAILED = 'ERR_CLAIM_FAILED';
const ERR_CLAUDE_TIMEOUT = 'ERR_CLAUDE_TIMEOUT';
const ERR_GIT_FAILED = 'ERR_GIT_FAILED';
const ERR_PR_FAILED = 'ERR_PR_FAILED';
const ERR_UNKNOWN = 'ERR_UNKNOWN';
const KNOWN_ERROR_CODES = new Set([ERR_TASK_NOT_FOUND, ERR_CLAIM_FAILED, ERR_CLAUDE_TIMEOUT, ERR_GIT_FAILED, ERR_PR_FAILED, ERR_UNKNOWN]);

class ProjectRunnerError extends Error {
constructor(message, code) {
super(message);
this.name = 'ProjectRunnerError';
this.code = code;
}
}

let running = true;
let currentChild = null; // active Claude child process
let processing = false; // true while a task is being processed (watch mode)
Expand Down Expand Up @@ -211,9 +228,9 @@ function runClaude(prompt, repoPath) {
child.on('close', (code, signal) => {
clearTimeout(timer);
if (signal === 'SIGTERM' || code === 143) {
reject(new Error('Claude timed out or was cancelled'));
reject(new ProjectRunnerError('Claude timed out or was cancelled', ERR_CLAUDE_TIMEOUT));
} else if (code !== 0) {
reject(new Error(`Claude exited with code ${code}`));
reject(new ProjectRunnerError(`Claude exited with code ${code}`, ERR_CLAUDE_TIMEOUT));
} else {
resolve(stdout);
}
Expand Down Expand Up @@ -280,8 +297,9 @@ async function processTask(task, project, projectName, runNumber, modeOverride,
claimTask(runTaskId);
postMessage(runTaskId, `Processing original task #${origTaskId}: ${title}`);
} catch (err) {
log('error', 'Failed to claim run task', { runTaskId, error: err.message });
return { success: false, error: err.message, run_number: runNumber };
const claimErr = new ProjectRunnerError(`Failed to claim run task: ${err.message}`, ERR_CLAIM_FAILED);
log('error', 'Failed to claim run task', { runTaskId, code: claimErr.code, error: claimErr.message });
return { success: false, error: claimErr.message, code: ERR_CLAIM_FAILED, run_number: runNumber };
}

// 3. Lease renewal heartbeat (only on the run task)
Expand All @@ -308,12 +326,20 @@ async function processTask(task, project, projectName, runNumber, modeOverride,
catch { try { git(repoPath, 'rev-parse', '--verify', 'origin/master'); defaultBranch = 'master'; } catch {} }
}

git(repoPath, 'checkout', defaultBranch);
git(repoPath, 'pull', '--ff-only');
try {
git(repoPath, 'checkout', defaultBranch);
git(repoPath, 'pull', '--ff-only');
} catch (err) {
throw new ProjectRunnerError(`Git setup failed: ${err.message}`, ERR_GIT_FAILED);
}

const branchSuffix = runNumber > 1 ? `-run${runNumber}` : '';
const branchName = `task/${origTaskId}-${slugify(title)}${branchSuffix}`;
git(repoPath, 'checkout', '-b', branchName);
try {
git(repoPath, 'checkout', '-b', branchName);
} catch (err) {
throw new ProjectRunnerError(`Failed to create branch ${branchName}: ${err.message}`, ERR_GIT_FAILED);
}
log('info', 'Created branch', { branchName });
postMessage(runTaskId, `Created branch: ${branchName}`);

Expand Down Expand Up @@ -390,14 +416,21 @@ async function processTask(task, project, projectName, runNumber, modeOverride,
let prUrl = null;
if (hasChanges(repoPath)) {
postMessage(runTaskId, 'Committing changes');
git(repoPath, 'add', '-A');

const commitMsg = `task/${origTaskId}: ${title}`;
git(repoPath, 'commit', '-m', commitMsg);
try {
git(repoPath, 'add', '-A');
const commitMsg = `task/${origTaskId}: ${title}`;
git(repoPath, 'commit', '-m', commitMsg);
} catch (err) {
throw new ProjectRunnerError(`Git commit failed: ${err.message}`, ERR_GIT_FAILED);
}
log('info', 'Committed changes', { runTaskId, branchName });

postMessage(runTaskId, 'Pushing branch');
git(repoPath, 'push', '-u', 'origin', branchName);
try {
git(repoPath, 'push', '-u', 'origin', branchName);
} catch (err) {
throw new ProjectRunnerError(`Git push failed: ${err.message}`, ERR_GIT_FAILED);
}
log('info', 'Pushed branch', { runTaskId, branchName });

postMessage(runTaskId, 'Creating pull request');
Expand All @@ -420,8 +453,9 @@ async function processTask(task, project, projectName, runNumber, modeOverride,
log('info', 'PR created', { runTaskId, prUrl });
postMessage(runTaskId, `PR created: ${prUrl}`);
} catch (err) {
log('error', 'Failed to create PR', { runTaskId, error: err.message, stderr: err.stderr });
postMessage(runTaskId, `PR creation failed: ${err.message}`);
const prErr = new ProjectRunnerError(`PR creation failed: ${err.message}`, ERR_PR_FAILED);
log('error', 'Failed to create PR', { runTaskId, code: prErr.code, error: prErr.message, stderr: err.stderr });
postMessage(runTaskId, `[${ERR_PR_FAILED}] ${prErr.message}`);
}
} else {
log('info', 'No changes to commit', { runTaskId });
Expand Down Expand Up @@ -453,16 +487,17 @@ async function processTask(task, project, projectName, runNumber, modeOverride,
return { success: true, runTaskId, prUrl, branch: branchName, outputs, run_number: runNumber, summary };

} catch (err) {
log('error', 'Task processing failed', { runTaskId, error: err.message });
postMessage(runTaskId, `Failed: ${err.message}`);
const errorCode = (err.code && KNOWN_ERROR_CODES.has(err.code)) ? err.code : ERR_UNKNOWN;
log('error', 'Task processing failed', { runTaskId, code: errorCode, error: err.message });
postMessage(runTaskId, `Failed [${errorCode}]: ${err.message}`);

try { failTask(runTaskId, err.message); } catch {}
telegram(`\u274C <b>Failed</b> on <b>${projectName}</b> (run-${runNumber})\nTask: ${title} (#${origTaskId})\n${err.message.slice(0, 200)}`);
try { failTask(runTaskId, `[${errorCode}] ${err.message}`); } catch {}
telegram(`\u274C <b>Failed</b> on <b>${projectName}</b> (run-${runNumber})\nTask: ${title} (#${origTaskId})\n[${errorCode}] ${err.message.slice(0, 200)}`);

// Clean up: try to get back to default branch
try { git(repoPath, 'checkout', '-'); } catch {}

return { success: false, runTaskId, error: err.message, run_number: runNumber };
return { success: false, runTaskId, error: err.message, code: errorCode, run_number: runNumber };
} finally {
clearInterval(renewInterval);
}
Expand Down Expand Up @@ -772,11 +807,11 @@ function handleWatchEvent(event, projectName, channel, project) {
try {
fullTask = getTask(taskId);
} catch (err) {
log('error', 'Failed to fetch task details', { taskId, error: err.message });
log('error', 'Failed to fetch task details', { taskId, code: ERR_TASK_NOT_FOUND, error: err.message });
return;
}
if (!fullTask) {
log('error', 'Task not found when fetching details', { taskId });
log('error', 'Task not found when fetching details', { taskId, code: ERR_TASK_NOT_FOUND });
return;
}

Expand Down Expand Up @@ -880,11 +915,11 @@ async function main() {
try {
task = getTask(taskId);
} catch (err) {
log('error', 'Failed to fetch task', { taskId, error: err.message });
log('error', 'Failed to fetch task', { taskId, code: ERR_TASK_NOT_FOUND, error: err.message });
process.exit(1);
}
if (!task) {
log('error', 'Task not found', { taskId });
log('error', 'Task not found', { taskId, code: ERR_TASK_NOT_FOUND });
process.exit(1);
}

Expand Down