Skip to content

fix: auto-stash creates empty stash objects from untracked noise files, no cleanup #19

@stackbilt-admin

Description

@stackbilt-admin

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:

  1. --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.

  2. 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

  1. Clone the taskrunner into a repo that uses charter CLI (writes telemetry to .charter/telemetry/events.ndjson).
  2. Let the taskrunner execute 10 tasks.
  3. Run git stash list in the target repo.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions