One file with secret-shaped text used to block every push, silently. The
drain now attributes scanner hits to files in one pass over the staged diff
(split on its diff --git headers), holds back each flagged file with the
rest of its coupled group, unstages it, and requires the remaining staged
diff to re-scan clean; anything short of that falls back to a full unstage
and status blocked. Held files stay queued and are re-scanned every drain.
- Status file gains held_count, drainable, last_drain_at, last_push_at and
held[] (path, scanner rule, dependents, both fixes); new status held.
- Coupled groups come from an explicit map (decisions.jsonl and
decisions.active.json): a queued member stages the whole group, a group
stages only when its snapshot matches its log in the index, a skipped
member stops the whole group.
- --unskip-file undoes --skip-file and queues the path again.
- An index lock older than 10 minutes is removed under the drain lock; a
younger one, a failed git add, a failed commit (hooks included) keep the
queue and report status error. A verified empty staged diff is the only
"nothing to commit". git add runs once, batched.
- The scanner reads bytes, so undecodable content no longer crashes it into
a false clean; matched text never reaches the status file.
- An empty-queue run with unpushed sync commits stays push_failed.
- Privacy-held duplicate records collapse to one per path; --status counts
a spool too deep for a shell glob.
- --attention prints skill start's fixed attention lines from the sync
status table in the usage header.
Co-authored-by: v639dragoon <matthewsbrickley@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>