Skip to content
Open
Changes from 1 commit
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
115 changes: 115 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,105 @@ function handleWatchEvent(event, projectName, channel, project) {
});
}

// === Dry-run preview ===
function dryRunPreview(task, project, projectName, attempts, modeOverride) {
const origTaskId = task.id || task.uuid;
const title = task.title || 'Untitled';
const description = task.description || '';

// Detect mode
let mode, iterations;
if (modeOverride) {
mode = modeOverride.mode;
iterations = modeOverride.iterations || (mode === 'oneshot' ? 1 : 8);
iterations = Math.min(iterations, project.max_iterations || 15);
} else {
({ mode, iterations } = detectMode(title, description, project));
}

// Build branch name
const branchName = `task/${origTaskId}-${slugify(title)}`;

// Build first-iteration prompt
const prompt = `You are working on the project at ${project.repo}. Execute this task:\n\n${title}\n${description}\n\nMake progress on this task. Do NOT commit, push, or create PRs.`;

const separator = '─'.repeat(60);

console.log(`\n${separator}`);
console.log(` DRY RUN — nothing will be executed`);
console.log(separator);
console.log(` Project: ${projectName}`);
console.log(` Channel: ${project.channel}`);
console.log(` Repo: ${project.repo}`);
console.log(` GitHub: ${project.github}`);
console.log(` Task ID: ${origTaskId}`);
console.log(` Title: ${title}`);
console.log(` Mode: ${mode}${modeOverride ? ' (override)' : ' (auto-detected)'}`);
console.log(` Iterations: ${iterations}`);
console.log(` Attempts: ${attempts}`);
console.log(` Branch: ${branchName}`);
if (attempts > 1) {
console.log(` Run branches: ${branchName}, ${branchName}-run2`);
if (attempts > 2) console.log(` ... through ${branchName}-run${attempts}`);
}
console.log(separator);
console.log(` Claude prompt preview (first 500 chars):`);
console.log();
console.log(` ${prompt.slice(0, 500).split('\n').join('\n ')}`);
if (prompt.length > 500) console.log(` ... (${prompt.length - 500} more chars)`);
console.log(`\n${separator}`);
console.log(` Would create: ATS run task on channel "${project.channel}:run-1"`);
console.log(` Would run: Claude Code (${mode}, up to ${iterations} iteration${iterations > 1 ? 's' : ''})`);
console.log(` Would open: PR on ${project.github} from ${branchName}`);
console.log(separator + '\n');
}

// === Status command ===
function statusCommand() {
const projects = Object.entries(PROJECTS);

if (projects.length === 0) {
console.log('No projects configured.');
return;
}

// Table header
const col = { name: 20, channel: 22, repo: 45, github: 35, pending: 9 };
const pad = (s, w) => String(s).padEnd(w).slice(0, w);
const separator = '─'.repeat(col.name + col.channel + col.repo + col.github + col.pending + 8);

console.log(`\nats-project-runner v${VERSION} — Project Status\n`);
console.log(
` ${pad('Project', col.name)} ${pad('Channel', col.channel)} ${pad('Repo Path', col.repo)} ${pad('GitHub', col.github)} ${pad('Pending', col.pending)}`
);
console.log(` ${separator}`);

for (const [name, proj] of projects) {
let pendingCount = '?';
try {
const raw = ats('list', '--channel', proj.channel, '--status', 'pending', '-f', 'json');
// Try to parse as JSON array or count objects
const match = raw.match(/\[[\s\S]*\]/);
if (match) {
const tasks = JSON.parse(match[0]);
pendingCount = String(tasks.length);
} else {
// Count individual task JSON objects
const taskMatches = raw.match(/\{[\s\S]*?\}/g);
pendingCount = taskMatches ? String(taskMatches.length) : '0';
}
} catch {
// ats list may return non-zero if no tasks — treat as 0
pendingCount = '0';
Comment thread
mikkel marked this conversation as resolved.
Outdated
}

console.log(
` ${pad(name, col.name)} ${pad(proj.channel, col.channel)} ${pad(proj.repo, col.repo)} ${pad(proj.github, col.github)} ${pad(pendingCount, col.pending)}`
);
}
console.log();
}

// === CLI ===
function usage() {
console.error(`Usage:
Expand All @@ -804,7 +903,9 @@ function usage() {
node index.js run <task-id> --mode oneshot Force one-shot mode
node index.js run <task-id> --mode iterative Force iterative mode
node index.js run <task-id> --iterations 12 Set max iterations (implies iterative)
node index.js run <task-id> --dry-run Preview what would happen without executing
node index.js watch Watch all configured channels for new tasks
node index.js status Show all projects and pending task counts
`);
process.exit(1);
}
Expand All @@ -820,6 +921,11 @@ async function main() {
return;
}

if (command === 'status') {
statusCommand();
return;
}

if (command !== 'run') usage();

const taskIdArg = args[1];
Expand All @@ -840,10 +946,13 @@ async function main() {
attempts: { type: 'string', short: 'a' },
mode: { type: 'string', short: 'm' },
iterations: { type: 'string', short: 'i' },
'dry-run': { type: 'boolean' },
},
strict: false,
});

const dryRun = values['dry-run'] || false;

if (values.attempts) {
attempts = parseInt(values.attempts, 10);
if (isNaN(attempts) || attempts < 1) {
Expand Down Expand Up @@ -900,6 +1009,12 @@ async function main() {
const { name: projectName, project } = match;
log('info', 'Matched project', { projectName, repo: project.repo, github: project.github });

// Dry-run mode: preview what would happen without executing
if (dryRun) {
dryRunPreview(task, project, projectName, attempts, modeOverride);
return;
Comment thread
mikkel marked this conversation as resolved.
Outdated
}

// Run all attempts
const results = await runAllAttempts(task, project, projectName, attempts, modeOverride);

Expand Down