Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to cc-taskrunner will be documented in this file.

Format follows [Keep a Changelog](https://keepachangelog.com/).

## [1.4.1] — 2026-04-09

### Fixed
- **Empty stash accumulation** (#19) — The auto-stash preflight in `taskrunner.sh` and `plugin/taskrunner.sh` used `[[ -n "$(git status --porcelain)" ]]` as the dirty-state check combined with `git stash push --include-untracked`. That triggered on any dirty state including untracked noise (charter telemetry, build artifacts), which created empty stash objects that piled up indefinitely. Downstream evidence: one operator found 10 accumulated stashes, 9 of which were empty and the 10th contained only 36 lines of telemetry noise.

New logic only stashes when `git diff --quiet` or `git diff --cached --quiet` detect real tracked changes. The `--include-untracked` flag was removed. After the `stash push`, the stash is verified against its parent; empty stashes are dropped immediately as a belt-and-suspenders guard against races.

## [1.4.0] — 2026-04-09

### Added
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,9 @@ main ─────────────────────────
└── auto/i9j0k1l2 ── commit ── commit ── PR → (task 3)
```

Before creating a branch, cc-taskrunner stashes any uncommitted work in the repo. After the task completes (or fails), it returns to main and restores the stash. Your in-progress work is never clobbered.
Before creating a branch, cc-taskrunner checks for **real tracked changes** via `git diff --quiet` and `git diff --cached --quiet`. If either shows uncommitted work, it stashes those tracked changes (staged or unstaged) and proceeds. Untracked files are left alone — they're usually build artifacts, telemetry, or IDE lockfiles that would create empty stash objects if captured. After the task completes (or fails), the runner returns to main and restores the stash. Your in-progress work is never clobbered.

The runner also verifies each stash actually captured content (by diffing against its parent) and drops any empty stash immediately. This prevents `git stash list` from accumulating noise over hundreds of autonomous runs.

If a task produces no commits (e.g., a research/analysis task), the empty branch is cleaned up automatically.

Expand Down
23 changes: 19 additions & 4 deletions plugin/taskrunner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,25 @@ execute_task() {
use_branch=true
branch="auto/${task_id:0:8}"

# Stash uncommitted changes to protect live work
if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
git stash push -m "cc-taskrunner:${task_id:0:8}" --include-untracked 2>/dev/null && stashed=true
log "│ Stashed uncommitted changes"
# Stash uncommitted TRACKED changes to protect live work.
# Only stashes real tracked diffs — untracked files alone are usually
# noise (telemetry, build artifacts) and produce empty stashes that
# accumulate indefinitely. See #19 for full bug report.
local has_tracked_dirty=false
if ! git diff --quiet 2>/dev/null; then has_tracked_dirty=true; fi
if ! git diff --cached --quiet 2>/dev/null; then has_tracked_dirty=true; fi

if $has_tracked_dirty; then
if git stash push -m "cc-taskrunner:${task_id:0:8}" 2>/dev/null; then
# Verify the stash captured content (drop if empty, race guard)
if ! git diff stash@{0}^1 stash@{0} --quiet 2>/dev/null; then
stashed=true
log "│ Stashed uncommitted tracked changes"
else
git stash drop stash@{0} 2>/dev/null || true
log "│ Stash was empty after push — dropped"
fi
fi
fi

# Start from main
Expand Down
31 changes: 27 additions & 4 deletions taskrunner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -386,10 +386,33 @@ execute_task() {
use_branch=true
branch="auto/${task_id:0:8}"

# Stash uncommitted changes to protect live work
if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
git stash push -m "cc-taskrunner:${task_id:0:8}" --include-untracked 2>/dev/null && stashed=true
log "│ Stashed uncommitted changes"
# Stash uncommitted TRACKED changes to protect live work.
#
# Only stash on real tracked diffs — untracked files alone are
# usually noise (charter telemetry, build artifacts, IDE lockfiles)
# and wrapping them in --include-untracked produces empty stash
# objects that pile up indefinitely. See #19 for the full bug
# report and accumulation evidence.
#
# Uses `git diff --quiet` and `git diff --cached --quiet` which
# only detect tracked changes. After the push, we verify the stash
# actually captured content by diffing against its parent; empty
# stashes are dropped immediately as a belt-and-suspenders guard.
local has_tracked_dirty=false
if ! git diff --quiet 2>/dev/null; then has_tracked_dirty=true; fi
if ! git diff --cached --quiet 2>/dev/null; then has_tracked_dirty=true; fi

if $has_tracked_dirty; then
if git stash push -m "cc-taskrunner:${task_id:0:8}" 2>/dev/null; then
# Verify the stash actually captured content (protect against races)
if ! git diff stash@{0}^1 stash@{0} --quiet 2>/dev/null; then
stashed=true
log "│ Stashed uncommitted tracked changes"
else
git stash drop stash@{0} 2>/dev/null || true
log "│ Stash was empty after push — dropped"
fi
fi
fi

# Start from main
Expand Down