From dfa6707a8fa20f986e50fc32ccb0267b3d9a8fe3 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Tue, 2 Jun 2026 15:20:31 -0400 Subject: [PATCH] commit per task --- package.json | 2 +- src/executor.ts | 77 +++++++++++++++++++++++++++++++++++++++++++++++-- src/utils.ts | 47 ++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 62c1ede..74b4bb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mikefreno/ralpi", - "version": "0.1.9", + "version": "0.2.0", "description": "Execute tasks from task files/PRD's using DAG-based dependency resolution with persistent progress tracking", "keywords": [ "pi-package", diff --git a/src/executor.ts b/src/executor.ts index d92a043..76a991b 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -11,6 +11,9 @@ import { writeFileSafe, ensureDir, captureGitCommits, + hasUncommittedChanges, + getGitStatusPorcelain, + getGitDiff, formatDuration, } from "./utils"; import { updateTaskInFile } from "./parser"; @@ -673,6 +676,76 @@ async function executeTask( ); if (result.success) { + // ── Auto-Commit: Trigger follow-up agent session for uncommitted changes ── + let finalCommitMessages = result.commitMessages ?? []; + let finalCommitSummary = result.commitSummary ?? ""; + + try { + if (hasUncommittedChanges(projectDir)) { + const status = getGitStatusPorcelain(projectDir); + const diff = getGitDiff(projectDir); + const commitPrompt = [ + `## Auto-Commit for Task ${task.id}: ${task.title}`, + "", + "The previous task is complete. There are uncommitted changes in the repository.", + "", + "First stage all intended changes with `git add -A` (including untracked files), then create a meaningful git commit.", + "Use a descriptive commit message and follow conventional commits format.", + "", + "### Current Changes (git status --porcelain)", + "```text", + status || "(no status output)", + "```", + "", + "### Current Tracked Diff (git diff)", + "```diff", + diff || "(no tracked diff output)", + "```", + ].join("\n"); + + // Use a short timeout for the commit session (60s should be enough) + const commitTimeout = Math.min( + 60_000, + config.execution.timeoutMs, + ); + const commitResult = await runAgentSession( + commitPrompt, + projectDir, + commitTimeout, + undefined, + undefined, + currentModel, + config.thinkingLevel, + ); + + if (commitResult.success) { + // Re-capture commits made during this follow-up session + const newCommits = captureGitCommits(projectDir); + if (newCommits.commitMessages.length > 0) { + finalCommitMessages = [ + ...finalCommitMessages, + ...newCommits.commitMessages, + ]; + finalCommitSummary = finalCommitSummary + ? `${finalCommitSummary}; ${newCommits.commitSummary}` + : newCommits.commitSummary; + } + sendChatMessage?.(`✓ commit for ${task.id} · ${task.title}`); + } else { + sendChatMessage?.( + `~ commit for ${task.id} · ${task.title} — follow-up commit session failed: ${commitResult.error}`, + ); + } + } + } catch (error) { + // Don't fail the task if auto-commit fails + sendChatMessage?.( + `~ commit for ${task.id} · ${task.title} — auto-commit error: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + // Save reflection if (result.reflection) { saveReflectionToFile(projectDir, config, result.reflection); @@ -685,8 +758,8 @@ async function executeTask( result.reflection, result.toolUsage, result.outputPreview, - result.commitMessages, - result.commitSummary, + finalCommitMessages, + finalCommitSummary, ); // Auto-update the PRD source file checkbox try { diff --git a/src/utils.ts b/src/utils.ts index bf27688..9619b4f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -579,6 +579,53 @@ function extractAssistantText(content: unknown): string { // ─── Git Commit Capture ────────────────────────────────────────────────────── +/** + * Check if there are any uncommitted changes in the git repository. + */ +export function hasUncommittedChanges(projectDir: string): boolean { + const { execSync } = require("node:child_process"); + try { + const output = execSync("git status --porcelain", { + cwd: projectDir, + encoding: "utf-8", + }).trim(); + return output.length > 0; + } catch { + return false; + } +} + +/** + * Get the current git status in porcelain format. + * Includes untracked files, which `git diff` alone would miss. + */ +export function getGitStatusPorcelain(projectDir: string): string { + const { execSync } = require("node:child_process"); + try { + return execSync("git status --porcelain", { + cwd: projectDir, + encoding: "utf-8", + }).trim(); + } catch { + return ""; + } +} + +/** + * Get the current git diff for tracked uncommitted changes. + */ +export function getGitDiff(projectDir: string): string { + const { execSync } = require("node:child_process"); + try { + return execSync("git diff", { + cwd: projectDir, + encoding: "utf-8", + }).trim(); + } catch { + return ""; + } +} + /** * Capture recent git commits made during task execution * Returns commit messages and a summary string