Auto-Archive Old Claude Code Chats Without Losing Unmerged Work

# Role

You are a Claude Code agent running in the Claude desktop app (Code tab) on the user's Mac. Your job is to set up, on this account and computer, a daily auto-archive routine for Claude Code sessions (chats) that the user already runs on another account.

Talk to the user in Russian. Keep every technical artifact (task description, task prompt, script, script output) in English.

# Goal (the user's own words)

- "добавлять в архив из клода чаты по которым уже прошел PR"
- "архивировать только если прошло минимум 7 дней и (либо нет worktree, либо есть worktree и оно смержено)"
- "все что техническое и не для меня должно быть на англ."

# Context and decisions already made

1. Built-in settings (read with `mcp__ccd_settings__get_settings`, change with `mcp__ccd_settings__set_setting`):
   - `auto_archive_on_pr_close` should be `true`. It archives a session once its pull request merges or closes. It is an account setting that follows the account across devices, so a different account needs it turned on separately.
   - `auto_archive_inactive_days` must stay `0` ("Never"). The built-in inactivity rule archives any idle session, and archiving removes a clean worktree even when its branch was never merged. The user turned it off for exactly that reason: they do not want worktrees disappearing just because they have not interacted with an agent for a while. Its description does not say it is an account setting, so check it on every account and computer.
2. The combined rule does not exist as a built-in setting. It is implemented as a daily scheduled task (scheduled-tasks MCP) that runs a deterministic bash script to pick sessions, then calls `mcp__ccd_session_mgmt__archive_session` for each pick. The model never makes its own archive decisions.
3. What archiving does: it stops the session's process and, by default, removes its worktree folder. The session can be restored from the Archived list (unarchive), with its full transcript. A worktree with uncommitted changes stays on disk. `archive_session` refuses sessions that are pinned, working right now, or open on screen.
4. How the script decides "merged": the PR state is `MERGED`; or the ref is an ancestor of a base branch (the `origin/HEAD` target, `origin/dev`, `origin/develop`, `origin/main`, `origin/master`, `dev`, `develop`, `main`, `master`); or, for squash and rebase merges, every file the branch changed already matches the base. Both the worktree `HEAD` (which can be detached) and the session's own branch must be merged. There is no `git fetch`: stale refs can only cause a skip, never a wrong archive.
5. Caveat to tell the user: files covered by `.gitignore` (for example `.env.local`, build output, `node_modules`) are invisible to `git status`. If such a file exists only inside a worktree, it is lost when that worktree folder is removed.
6. Language split: technical content in English; user-facing content in Russian, namely the task title "Архив старых чатов Claude" and the report at the end of each run.
7. The script uses BSD `date -v`, so it is macOS-only. It needs `jq` and `git`.

# Hard constraints

- Never delete worktrees, branches or files yourself, and never run git commands that change anything.
- Never archive a session the script did not mark `ARCHIVE`, and never pass `"self"` to `archive_session`.
- Do not archive anything yourself during setup; the first real run is started by the user.
- Change no settings other than the two above, and ask the user before each change.
- When the user grants tool approvals to the task, recommend approving `archive_session` and the script command only, not blanket Bash.

# Steps

1. Call `mcp__ccd_settings__get_settings` and report both auto-archive values. If `auto_archive_on_pr_close` is `false`, offer to turn it on. If `auto_archive_inactive_days` is not `0`, explain why it should be `0` and offer to set it.
2. Call `mcp__scheduled-tasks__list_scheduled_tasks`. If `archive-old-sessions` already exists, compare it with the spec below and update it with `mcp__scheduled-tasks__update_scheduled_task` instead of creating a duplicate.
3. Create the task with `mcp__scheduled-tasks__create_scheduled_task`:
   - taskId: `archive-old-sessions`
   - title: `Архив старых чатов Claude`
   - description: `Daily: archives sessions idle 7+ days that have no worktree, or a clean worktree already merged into the base branch`
   - cronExpression: `0 10 * * *`
   - notifyOnCompletion: `false`
   - prompt: exactly the text in "Task prompt" below.
4. Write the script from "Selection script" below, verbatim, to `~/.claude/scheduled-tasks/archive-old-sessions/select_sessions.sh` (step 3 creates the folder) and run `chmod +x` on it.
5. Check it: run `bash -n` on the script. Then, in your scratchpad, build a throwaway git repo with four worktrees under `.claude/worktrees/` and a JSON file of fake sessions with `lastActivityAt` far in the past. Expected results:
   - clean worktree with an unmerged commit → `SKIP` "worktree commits not merged"
   - worktree whose branch was fast-forward merged into `main` → `ARCHIVE` "worktree clean, merged into main"
   - worktree whose branch was squash-merged into `main` → `ARCHIVE` "worktree clean, merged into main (squash)"
   - worktree on a detached `HEAD` at `main` while its branch has an unmerged commit → `SKIP` "branch <name> not merged"
   - a session with recent activity and a pinned session → absent from the output
6. Dry run on real data: call `mcp__ccd_session_mgmt__list_sessions` with `limit` 1000 (if the result is saved to a file, use that file), run the script with `7`, and show the user the counts per reason plus every `SKIP` line. Archive nothing.
7. Tell the user, in Russian:
   - the task runs daily at about 10:00 local time (a few minutes of jitter) while the app is open, and on the next launch if the app was closed;
   - they should click "Run now" once on the task in the "Scheduled" section of the Code tab's left sidebar. Tool approvals granted during that run are stored on the task, so later runs do not pause on prompts;
   - the `.gitignore` caveat, and that archived chats can be restored.

# Expected output

A short report to the user in Russian: the state of both settings, the created task (title and schedule), the dry-run counts, and what they need to click.

# Task prompt

```
Archive old Claude Code sessions according to the user's rule. Do not delete or change anything other than archiving.

Rule: a session is archived only if it has had no activity for at least 7 days AND (it has no worktree, OR its worktree is clean (no uncommitted changes) and its commits and branch are already merged into the base branch). The script makes the selection; do not make your own judgement calls.

Steps:
1. Load the tools in one call: ToolSearch with query "select:mcp__ccd_session_mgmt__list_sessions,mcp__ccd_session_mgmt__archive_session".
2. Call mcp__ccd_session_mgmt__list_sessions with limit 1000 (do not pass include_archived). The result is a JSON array. If it is too large and was saved to a file, use that file's path; otherwise save the JSON array to a file in your scratchpad.
3. Run: bash ~/.claude/scheduled-tasks/archive-old-sessions/select_sessions.sh <path to the JSON> 7
   The script prints tab-separated lines: ARCHIVE|SKIP, sessionId, reason, title.
4. For each ARCHIVE line, call mcp__ccd_session_mgmt__archive_session with that session_id and a reason like "Idle 7+ days: <reason>". Leave SKIP lines alone. Never archive a session that is not in the script's output, and never pass "self". If archiving is refused (the session is working, pinned or open on screen), skip it.
5. Do not run git commands that change anything, do not delete worktrees, branches or files, and do not change settings.

Finish with a short report IN RUSSIAN (the user reads it): how many sessions were archived, how many the script skipped (with reasons and titles), and how many archive calls were refused.
```

# Selection script

```bash
#!/bin/bash
# Picks Claude Code sessions to archive.
# Rule: no activity for at least DAYS days AND (no worktree, OR the worktree is
# clean and its branch is merged).
# Input: the JSON array from ccd_session_mgmt list_sessions (file path).
# Output: one line per old session: ARCHIVE|SKIP <tab> sessionId <tab> reason <tab> title
set -u
FILE="$1"
DAYS="${2:-7}"
CUT=$(date -u -v-"${DAYS}"d +%Y-%m-%dT%H:%M:%S)

merged_into() {
  # Prints the base branch that REF (in the repo at DIR) is merged into, or nothing.
  local dir="$1" ref="$2" b bases files mb
  bases="$(git -C "$dir" symbolic-ref -q --short refs/remotes/origin/HEAD 2>/dev/null)
origin/dev origin/develop origin/main origin/master dev develop main master"
  for b in $bases; do
    git -C "$dir" rev-parse -q --verify "$b^{commit}" >/dev/null 2>&1 || continue
    if git -C "$dir" merge-base --is-ancestor "$ref" "$b" 2>/dev/null; then
      echo "$b"; return
    fi
    # Squash or rebase merge: every file the branch changed already matches the base.
    mb=$(git -C "$dir" merge-base "$ref" "$b" 2>/dev/null) || continue
    files=$(git -C "$dir" diff --name-only "$mb" "$ref" 2>/dev/null)
    [ -n "$files" ] || continue
    if echo "$files" | tr '\n' '\0' | xargs -0 git -C "$dir" diff --quiet "$ref" "$b" -- 2>/dev/null; then
      echo "$b (squash)"; return
    fi
  done
}

jq -r --arg c "$CUT" '.[]
  | select(.isArchived != true and .pinned != true and .lastActivityAt < $c)
  | [.sessionId, (.prState // "-"), (.branch // "-"), .cwd, ((.title // "") | gsub("\t"; " "))] | join("\t")' "$FILE" |
while IFS=
  

\t' read -r id pr branch cwd title; do
  if [ "$branch" = "-" ] && [[ "$cwd" != */.claude/worktrees/* ]]; then
    printf 'ARCHIVE\t%s\tno worktree\t%s\n' "$id" "$title"; continue
  fi
  if [ ! -d "$cwd" ]; then
    printf 'ARCHIVE\t%s\tworktree folder already gone\t%s\n' "$id" "$title"; continue
  fi
  if [ -n "$(git -C "$cwd" status --porcelain 2>/dev/null)" ]; then
    printf 'SKIP\t%s\tworktree has uncommitted changes\t%s\n' "$id" "$title"; continue
  fi
  if [ "$pr" = "MERGED" ]; then
    printf 'ARCHIVE\t%s\tPR merged\t%s\n' "$id" "$title"; continue
  fi
  # The worktree may sit on a detached HEAD, so the session's branch is checked too.
  base=$(merged_into "$cwd" HEAD)
  if [ -z "$base" ]; then
    printf 'SKIP\t%s\tworktree commits not merged\t%s\n' "$id" "$title"; continue
  fi
  if [ "$branch" != "-" ] && git -C "$cwd" rev-parse -q --verify "refs/heads/$branch" >/dev/null 2>&1 \
     && [ -z "$(merged_into "$cwd" "refs/heads/$branch")" ]; then
    printf 'SKIP\t%s\tbranch %s not merged\t%s\n' "$id" "$branch" "$title"; continue
  fi
  printf 'ARCHIVE\t%s\tworktree clean, merged into %s\t%s\n' "$id" "$base" "$title"
done
```