Bug
The auto-stash preflight in taskrunner.sh (and plugin/taskrunner.sh) uses:
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
fi
Two problems compound:
-
--include-untracked captures noise files such as .charter/telemetry/events.ndjson, build artifacts, or IDE lock files that shouldn't trigger a stash. The condition treats any status --porcelain output as a signal to stash, including untracked-only state.
-
Empty stashes are never cleaned up. When the runner finishes, it pops the stash only if stashed=true AND the task completed cleanly. If the stash was empty (or had only noise), the stash entry stays around. Over many runs, stashes accumulate indefinitely.
Reproduction
- Clone the taskrunner into a repo that uses charter CLI (writes telemetry to
.charter/telemetry/events.ndjson).
- Let the taskrunner execute 10 tasks.
- Run
git stash list in the target repo.
- Observe: 10 accumulated auto-stash entries, most of which are empty or contain only telemetry noise.
Real-world evidence (from a fork running this logic):
stash@{0}: On main: cc-taskrunner: auto-stash before task 16c2374a-...
stash@{1}: On main: cc-taskrunner: auto-stash before task 3a6c2503-...
... (10 entries total)
Inspection:
stash@{0} contained 36 lines of .charter/telemetry/events.ndjson — telemetry noise.
stash@{1} through stash@{9} had empty diffs against parent — git stash show --stat returned nothing. They were created but captured no actual content.
Fix
Two changes:
1. Only stash when there's real tracked dirty state. Don't use status --porcelain as the check — it includes untracked. Use git diff --quiet and git diff --cached --quiet which only detect tracked changes:
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
# stash only tracked changes (no -u)
git stash push -m "cc-taskrunner:${task_id:0:8}" 2>/dev/null && stashed=true
fi
2. Verify the stash captured content, drop if empty. Belt-and-suspenders for races:
if $stashed; then
if git diff stash@{0}^1 stash@{0} --quiet 2>/dev/null; then
# Stash is empty (race or edge case) — drop it immediately
git stash drop stash@{0} 2>/dev/null || true
stashed=false
fi
fi
Why this matters for autonomous systems
Empty stash accumulation is a silent memory leak for autonomous runners. A month of unattended operation produces hundreds of stashes, obscures any real stashed work, and pollutes git stash list as a debugging surface. Operators can't tell which stash entries matter.
Downstream already patched
A fork running the same pattern has already landed the fix:
- Stashes contain real tracked diffs, verified post-push
- Untracked-only state is left alone (let the task deal with it, or let the worktree cleanup handle it)
- Empty stashes are auto-dropped
- Verified against 5 scenarios: clean tree, untracked-only, tracked modified, pop recovery, staged-only
Happy to submit a PR if useful.
Related
- Charter CLI writes telemetry to
.charter/telemetry/events.ndjson which is the most common source of the untracked noise. That file is gitignored at the repo root only — if your project has nested **/.charter/ paths (e.g., a monorepo with web/.charter/), the gitignore doesn't match and it shows up as untracked.
Bug
The auto-stash preflight in
taskrunner.sh(andplugin/taskrunner.sh) uses:Two problems compound:
--include-untrackedcaptures noise files such as.charter/telemetry/events.ndjson, build artifacts, or IDE lock files that shouldn't trigger a stash. The condition treats anystatus --porcelainoutput as a signal to stash, including untracked-only state.Empty stashes are never cleaned up. When the runner finishes, it pops the stash only if
stashed=trueAND the task completed cleanly. If the stash was empty (or had only noise), the stash entry stays around. Over many runs, stashes accumulate indefinitely.Reproduction
.charter/telemetry/events.ndjson).git stash listin the target repo.Real-world evidence (from a fork running this logic):
Inspection:
stash@{0}contained 36 lines of.charter/telemetry/events.ndjson— telemetry noise.stash@{1}throughstash@{9}had empty diffs against parent —git stash show --statreturned nothing. They were created but captured no actual content.Fix
Two changes:
1. Only stash when there's real tracked dirty state. Don't use
status --porcelainas the check — it includes untracked. Usegit diff --quietandgit diff --cached --quietwhich only detect tracked changes:2. Verify the stash captured content, drop if empty. Belt-and-suspenders for races:
Why this matters for autonomous systems
Empty stash accumulation is a silent memory leak for autonomous runners. A month of unattended operation produces hundreds of stashes, obscures any real stashed work, and pollutes
git stash listas a debugging surface. Operators can't tell which stash entries matter.Downstream already patched
A fork running the same pattern has already landed the fix:
Happy to submit a PR if useful.
Related
.charter/telemetry/events.ndjsonwhich is the most common source of the untracked noise. That file is gitignored at the repo root only — if your project has nested**/.charter/paths (e.g., a monorepo withweb/.charter/), the gitignore doesn't match and it shows up as untracked.