From 1d6f26e6394ddfd1abab7feeeea6b2a0a7986800 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Sun, 15 Feb 2026 21:02:57 -0700 Subject: [PATCH 1/6] task/453: Add dry-run mode and task status dashboard --- index.js | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/index.js b/index.js index 6887a88..602bbed 100644 --- a/index.js +++ b/index.js @@ -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'; + } + + 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: @@ -804,7 +903,9 @@ function usage() { node index.js run --mode oneshot Force one-shot mode node index.js run --mode iterative Force iterative mode node index.js run --iterations 12 Set max iterations (implies iterative) + node index.js run --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); } @@ -820,6 +921,11 @@ async function main() { return; } + if (command === 'status') { + statusCommand(); + return; + } + if (command !== 'run') usage(); const taskIdArg = args[1]; @@ -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) { @@ -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; + } + // Run all attempts const results = await runAllAttempts(task, project, projectName, attempts, modeOverride); From 111f73de86c06dafd846cc0d820b2dc78ccd407d Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Sun, 15 Feb 2026 21:34:48 -0700 Subject: [PATCH 2/6] Fix dry-run preflight and status error handling 1. Dry-run now skips Claude binary validation in preflight, so --dry-run works in environments where only ATS is available. 2. Status command catch block now logs ATS query failures and shows 'ERR' instead of silently reporting '0' pending tasks, which was producing false health signals. Fixes Codex review comments on PR #2. Co-Authored-By: Claude Opus 4.6 --- index.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 602bbed..9161887 100644 --- a/index.js +++ b/index.js @@ -503,8 +503,10 @@ async function runAllAttempts(task, project, projectName, attempts, modeOverride } // === Preflight === -function preflight() { - for (const check of [{ name: 'ats', bin: ATS_BIN }, { name: 'claude', bin: CLAUDE_BIN }]) { +function preflight({ skipClaude = false } = {}) { + const checks = [{ name: 'ats', bin: ATS_BIN }]; + if (!skipClaude) checks.push({ name: 'claude', bin: CLAUDE_BIN }); + for (const check of checks) { try { const version = execSync(`'${check.bin}' --version`, { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], @@ -883,9 +885,9 @@ function statusCommand() { 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'; + } catch (err) { + log('warn', 'Failed to query pending tasks', { project: name, channel: proj.channel, error: err.message }); + pendingCount = 'ERR'; } console.log( @@ -982,7 +984,7 @@ async function main() { projects: Object.keys(PROJECTS), }); - preflight(); + preflight({ skipClaude: dryRun }); // Fetch the original task (read-only) let task; From 89cfaf1454b52de315fc7b4bdf67b7d631a7316d Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Sun, 15 Feb 2026 21:40:39 -0700 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20address=20Codex=20review=20=E2=80=94?= =?UTF-8?q?=20Move=20dry-run=20branch=20before=20Claude=20preflight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 9161887..6b4eaa0 100644 --- a/index.js +++ b/index.js @@ -984,7 +984,34 @@ async function main() { projects: Object.keys(PROJECTS), }); - preflight({ skipClaude: dryRun }); + // Dry-run mode: only needs ATS preflight (no Claude required) + if (dryRun) { + preflight({ skipClaude: true }); + + let task; + try { + task = getTask(taskId); + } catch (err) { + log('error', 'Failed to fetch task', { taskId, error: err.message }); + process.exit(1); + } + if (!task) { + log('error', 'Task not found', { taskId }); + process.exit(1); + } + + const match = findProjectByChannel(task.channel); + if (!match) { + log('error', 'Task channel not found in config', { taskId, channel: task.channel, configured: Object.values(PROJECTS).map(p => p.channel) }); + process.exit(1); + } + + const { name: projectName, project } = match; + dryRunPreview(task, project, projectName, attempts, modeOverride); + return; + } + + preflight(); // Fetch the original task (read-only) let task; @@ -1011,12 +1038,6 @@ 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; - } - // Run all attempts const results = await runAllAttempts(task, project, projectName, attempts, modeOverride); From e9558cf5f0f7c55c34f537ad4a522a372dd42d3d Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Sun, 15 Feb 2026 21:46:05 -0700 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20address=20Codex=20review=20=E2=80=94?= =?UTF-8?q?=20Move=20dry-run=20branch=20before=20Claude=20preflight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/index.js b/index.js index 6b4eaa0..626da21 100644 --- a/index.js +++ b/index.js @@ -984,34 +984,8 @@ async function main() { projects: Object.keys(PROJECTS), }); - // Dry-run mode: only needs ATS preflight (no Claude required) - if (dryRun) { - preflight({ skipClaude: true }); - - let task; - try { - task = getTask(taskId); - } catch (err) { - log('error', 'Failed to fetch task', { taskId, error: err.message }); - process.exit(1); - } - if (!task) { - log('error', 'Task not found', { taskId }); - process.exit(1); - } - - const match = findProjectByChannel(task.channel); - if (!match) { - log('error', 'Task channel not found in config', { taskId, channel: task.channel, configured: Object.values(PROJECTS).map(p => p.channel) }); - process.exit(1); - } - - const { name: projectName, project } = match; - dryRunPreview(task, project, projectName, attempts, modeOverride); - return; - } - - preflight(); + // Dry-run skips Claude preflight (only validates ATS connectivity) + preflight({ skipClaude: dryRun }); // Fetch the original task (read-only) let task; @@ -1038,6 +1012,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; + } + // Run all attempts const results = await runAllAttempts(task, project, projectName, attempts, modeOverride); From 0d616b5d82fce84101b27900e5b6919de6d86d4b Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Sun, 15 Feb 2026 21:47:09 -0700 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20address=20Codex=20review=20=E2=80=94?= =?UTF-8?q?=20Do=20not=20report=20ATS=20query=20failures=20as=20zero=20pen?= =?UTF-8?q?ding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/index.js b/index.js index 626da21..44b79b4 100644 --- a/index.js +++ b/index.js @@ -871,6 +871,7 @@ function statusCommand() { ); console.log(` ${separator}`); + let hasErrors = false; for (const [name, proj] of projects) { let pendingCount = '?'; try { @@ -888,6 +889,7 @@ function statusCommand() { } catch (err) { log('warn', 'Failed to query pending tasks', { project: name, channel: proj.channel, error: err.message }); pendingCount = 'ERR'; + hasErrors = true; } console.log( @@ -895,6 +897,10 @@ function statusCommand() { ); } console.log(); + if (hasErrors) { + console.error('Warning: one or more channels could not be queried (see ERR above).'); + process.exit(1); + } } // === CLI === From 2e2d69b4f094f45520e757c67416beb8014e97cf Mon Sep 17 00:00:00 2001 From: "Ada (DiffLab AI)" Date: Sun, 22 Mar 2026 01:14:54 -0600 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20Move?= =?UTF-8?q?=20dry-run=20branch=20before=20Claude=20preflight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 44b79b4..df1fbeb 100644 --- a/index.js +++ b/index.js @@ -990,8 +990,39 @@ async function main() { projects: Object.keys(PROJECTS), }); - // Dry-run skips Claude preflight (only validates ATS connectivity) - preflight({ skipClaude: dryRun }); + // Dry-run mode: only validate ATS connectivity, fetch task, and preview + if (dryRun) { + preflight({ skipClaude: true }); + + let task; + try { + task = getTask(taskId); + } catch (err) { + log('error', 'Failed to fetch task', { taskId, error: err.message }); + process.exit(1); + } + if (!task) { + log('error', 'Task not found', { taskId }); + process.exit(1); + } + + log('info', 'Fetched original task', { taskId, title: task.title, channel: task.channel, status: task.status }); + + const match = findProjectByChannel(task.channel); + if (!match) { + log('error', 'Task channel not found in config', { taskId, channel: task.channel, configured: Object.values(PROJECTS).map(p => p.channel) }); + process.exit(1); + } + + const { name: projectName, project } = match; + log('info', 'Matched project', { projectName, repo: project.repo, github: project.github }); + + dryRunPreview(task, project, projectName, attempts, modeOverride); + return; + } + + // Full preflight (includes Claude binary validation) + preflight(); // Fetch the original task (read-only) let task; @@ -1018,12 +1049,6 @@ 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; - } - // Run all attempts const results = await runAllAttempts(task, project, projectName, attempts, modeOverride);