PLAN-61: LLM-assisted commit and PR creation for electron loops - #38
Conversation
Replace hardcoded "Symphony: implement plan" commit messages and PR descriptions with LLM-generated content that describes actual changes. - Add attemptLlmCommit() that spawns a Claude session to review the diff, write a descriptive commit, push, and create a PR with a real title/body - Include loop ID and artifact link in PR body metadata footer - Mechanical fallback with contextual info if LLM commit fails - Export sanitizeCommitMessage() for reuse across modules - Pass webAppOrigin through router for artifact link generation - Add test coverage for the execute git operations flow
| import { sanitizeCommitMessage } from "./symphony-interactive.js"; | ||
| import { | ||
| expandHome, | ||
| resolveWorktreeParentDir, |
There was a problem hiding this comment.
[HIGH] Project Rules
[P1] Missing version bump in apps/desktop/package.json
Recommendation: Increment the patch version in apps/desktop/package.json and include it in this commit.
import { closeSync, existsSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";There was a problem hiding this comment.
Addressed — version bumped to 0.8.1.
| const parsed: unknown = JSON.parse(raw); | ||
| if (isExecutionResult(parsed)) { | ||
| loopLog(loopId, `LLM commit wrote execution-result.json, pr=${parsed.prUrl}`); | ||
| resolve(parsed); |
There was a problem hiding this comment.
[MEDIUM] Correctness
[P2] execution-result.json not cleaned up after successful LLM commit, leaks into subsequent commits
Recommendation: After reading and validating execution-result.json, delete it unconditionally.
There was a problem hiding this comment.
Fixed. execution-result.json is now cleaned up unconditionally after reading (success or failure) inside attemptLlmCommit, so it never leaks into subsequent runs.
| // shell escaping issues with special characters (--body-file approach). | ||
| const prBody = `Loop ID: ${loopId}\nCommand: ${command}`; | ||
| const bodyFile = path.join(worktreeDir, ".claude", "work", "pr-body.md"); | ||
| writeFileSync(bodyFile, prBody); |
There was a problem hiding this comment.
[MEDIUM] Correctness
[P2] writeFileSync to .claude/work/pr-body.md may fail if directory does not exist
Recommendation: Add mkdirSync(path.dirname(bodyFile), { recursive: true }) before writeFileSync.
There was a problem hiding this comment.
Fixed. Added mkdirSync(path.dirname(bodyFile), { recursive: true }) before writeFileSync.
| // Clean up LLM artifacts before fallback to prevent them from being committed | ||
| if (!llmResult) { | ||
| try { unlinkSync(path.join(worktreeDir, 'execution-result.json')); } catch { /* file may not exist */ } | ||
| try { unlinkSync(path.join(worktreeDir, 'execution-footer.txt')); } catch { /* file may not exist */ } |
There was a problem hiding this comment.
[HIGH] Correctness
[P1] LLM failure cleanup removes wrong file (execution-footer.txt instead of pr-body.md), leaving pr-body.md to be committed by fallback
Recommendation: Replace unlinkSync for execution-footer.txt with unlinkSync for pr-body.md to match what the LLM actually writes.
There was a problem hiding this comment.
Fixed. The cleanup block now removes pr-body.md (not execution-footer.txt). Additionally, attemptLlmCommit itself now cleans up both scratch files unconditionally after reading the result.
| // For existing PRs this ensures the footer is always present. | ||
| try { | ||
| execSync( | ||
| `gh pr edit ${prNumber} --body-file ${shellEscape(bodyFile)}`, |
There was a problem hiding this comment.
[MEDIUM] Correctness
[P2] gh pr edit --body-file unconditionally replaces entire PR body for existing PRs with a 2-line metadata stub
Recommendation: Fetch the existing PR body before editing and append the metadata, or skip editing for newly-created PRs.
There was a problem hiding this comment.
Fixed. For existing PRs, we now fetch the current body via gh pr view, check if the footer is already present, and only append the metadata if missing — preserving whatever description the user or LLM wrote.
Code Review SummaryStatus: Needs Attention Reviewers: Bug Hunter A, Bug Hunter B, Unified Auditor, Premise Reviewer, Gateway Core Architect Findings
HIGH Issues (should fix)
MEDIUM Issues (consider)
Validation Stats
Recommendation: Address the 2 HIGH issues before merging. The version bump is a hard CI blocker. The |
| webAppOrigin ?? "" | ||
| ); | ||
|
|
||
| // Clean up LLM artifacts before fallback to prevent them from being committed |
There was a problem hiding this comment.
[P1] Move LLM scratch files out of the reused worktree
This cleanup only runs when llmResult is null, and it still misses pr-body.md, even though attemptLlmCommit tells Claude to write both pr-body.md and execution-result.json in the repo root. Because EXECUTE reuses the same worktree for later runs on the same artifact slug, those leftovers will be seen as real changes by the next git add -- . and can get committed into the user's branch; if Claude exits 0 without rewriting execution-result.json, the stale file is also treated as a fresh success and the fallback path is skipped.
There was a problem hiding this comment.
Fixed. Scratch files (execution-result.json and pr-body.md) are now cleaned up unconditionally inside attemptLlmCommit after reading the result — regardless of success or failure. The fallback cleanup block also now removes pr-body.md instead of the wrong execution-footer.txt reference.
| const prTitle = `${commitPrefix}Symphony: ${command} -- loop ${shortId}`; | ||
| const prOutput = execSync( | ||
| `gh pr create --title "Symphony: implement plan" --body "Automated PR from Symphony loop" --base ${shellEscape(baseBranch)}`, | ||
| `gh pr create --title ${shellEscape(prTitle)} --body-file ${shellEscape(bodyFile)} --base ${shellEscape(baseBranch)} --label symphony`, |
There was a problem hiding this comment.
[P1] Make symphony labeling best-effort when creating PRs
On any repo that does not already have a symphony label, gh pr create ... --label symphony fails after the commit and push have already succeeded. Because the whole PR creation block is wrapped in this try, executeGitOperations returns null and the loop completes without a PR URL even though a new commit was pushed. This path used to work on unlabeled repos, so label attachment needs to be conditional or retried without --label.
There was a problem hiding this comment.
Fixed. PR creation now runs without --label, then attaches the symphony label as a separate best-effort step via gh pr edit --add-label. If the label doesn't exist on the repo, the PR still gets created successfully.
|
|
||
| // Try LLM-assisted commit first; fall back to executeGitOperations if it | ||
| // returns null. Never call both. | ||
| const llmResult = await attemptLlmCommit( |
There was a problem hiding this comment.
[P2] Pass committer through to the LLM commit subprocess
When body.committer is present, the new LLM-first flow never forwards that identity into attemptLlmCommit. Any successful LLM-assisted commit is therefore authored with whatever git config happens to exist in the worktree, instead of the requested user, which regresses the previous EXECUTE behavior that set GIT_AUTHOR_*/GIT_COMMITTER_* before committing.
There was a problem hiding this comment.
Fixed. attemptLlmCommit now accepts a committer parameter and sets GIT_AUTHOR_NAME/EMAIL and GIT_COMMITTER_NAME/EMAIL as env vars on the spawned claude process, matching the same pattern used by executeGitOperations.
|
I think this can kill this one too https://app.closedloop.ai/features/FEAT-142 |
- Clean up LLM scratch files (execution-result.json, pr-body.md) unconditionally after reading, so they never leak into subsequent worktree runs - Make symphony label attachment best-effort on PR creation so repos without the label don't fail after commit+push already succeeded - Pass committer identity through to attemptLlmCommit via GIT_AUTHOR_*/ GIT_COMMITTER_* env vars on the spawned process - Add mkdirSync before writing pr-body.md to .claude/work/ directory - For existing PRs, fetch current body and append metadata footer instead of replacing the entire body with a 2-line stub - Remove stale execution-footer.txt reference (should have been pr-body.md) - Bump version to 0.8.1 Testing: typecheck, lint pass. Pre-existing test failures unchanged. Risks: Low — all changes are in the EXECUTE commit/PR flow.
Summary
attemptLlmCommit()— spawns a Claude session post-execution to review the diff, write a real commit message, push, and create a PR with a descriptive title/bodysanitizeCommitMessage()fromsymphony-interactive.tsfor cross-module reusewebAppOriginthrough the router for artifact link generationPlan: https://app.closedloop.ai/implementation-plans/PLAN-61
Test plan
attemptLlmCommit()spawns Claude, reviews diff, commits, pushes, and creates PRsanitizeCommitMessage()strips model names from LLM outputpnpm test— new test file covers execute git operations flow