---
name: pneuma-project-tidy
description: Tidy a Pneuma project's session titles and descriptions. Use in the Project Tidy workspace to replace placeholder metadata with topic-specific names and report progress.
---

# Project Tidy Agent

In script examples, `<SKILL_DIR>` means the actual directory containing this
loaded `SKILL.md`. Substitute its full path and keep shell paths quoted. The
runtime installs it under `.claude/skills` for Claude Code, `.agents/skills` for
Codex, or `.kimi-code/skills` for Kimi; use the path given in your instructions.

You are the Project Tidy Agent. Your one job: sweep this project's **Recent Sessions** list and rewrite every session that's still on a placeholder title (`"WebCraft session"`, `"文档 会话"`, …) so each row says what it's actually *about*.

You don't create anything. You re-label sessions that already exist, using the same `pneuma session refine` mechanism the `pneuma-session` skill runs *inside* a single session — but here you run it across the whole project in one pass.

The user opened this from the project's AI menu and is watching a **live progress report** (the viewer). Your output is two things:

1. The actual refines (applied directly to each session — titles are cheap and reversible).
2. A running `report.json` so the viewer can show progress.

## The loop

```
brief (1 sentence)  →  enumerate  →  write initial report  →  refine each  →  done
```

Work autonomously start-to-finish. Do **not** stop to ask the user which sessions to do or to approve titles — they came here for a finished, tidied list, not a conversation. If they want to steer, they'll interrupt.

### 1. Enumerate

Run the bundled script — never hand-grep the session dirs:

```bash
bun "<SKILL_DIR>/scripts/list-project-sessions.ts"
```

It prints one JSON object: `{ projectRoot, total, needsTidy, sessions: [...] }`. Each session carries:

| Field | Meaning |
|---|---|
| `sessionId` | the id you pass to `refine --target-session` |
| `mode` | which mode the session ran in |
| `displayName` / `description` | current stored title / summary (`null` = still on the default) |
| `sessionName` | the user's **manual** rename — if set, leave it alone |
| `needsTidy` | `true` when there's no manual name **and** no refined title |
| `skipReason` | why it was skipped (only when `needsTidy: false`) |
| `digest` | the first several real user messages (synthetic `<pneuma:…>` tags already dropped) |
| `messageCount` | how many real user messages the session has |

**Only refine sessions where `needsTidy` is `true`.** Skip the rest — a stored `displayName` means it was already tidied, and a `sessionName` is the user's explicit choice that always wins.

### 2. Write the initial report

Create `<sessionDir>/tidy/report.json` listing every `needsTidy` session as `pending`. This is what the viewer renders. Shape:

```json
{
  "schemaVersion": 1,
  "total": 7,
  "sessions": [
    {
      "sessionId": "abc123",
      "mode": "webcraft",
      "status": "pending",
      "before": { "displayName": null }
    }
  ]
}
```

`total` is the count of sessions you intend to refine (the `needsTidy` set). You may also include skipped sessions with `"status": "skipped"` and a short `"skipReason"` so the report is a complete account — but skipped rows don't count toward `total`.

### 3. Refine each session

For each `needsTidy` session, work out a title + one-line summary from its `digest` (read the full `<projectRoot>/.pneuma/sessions/<id>/history.json` only if the digest is too thin to title confidently), then apply it:

```bash
$PNEUMA_CLI session refine --target-session <sessionId> --json '{"displayName": "<≤40 chars>", "description": "<≤280 chars>"}'
```

- Always call through `$PNEUMA_CLI` (not the literal `pneuma`) — it is a single executable path and behaves the same in any shell, quoted or unquoted.
- `--target-session <sessionId>` is what makes this refine land on a **sibling** session instead of this temporary tidy session. Without it you'd re-title the tidy session itself — never do that.
- Before moving to the next session, update that session's entry in `report.json`: set `status` to `"done"`, fill `after.displayName` / `after.description`, and carry `before.displayName` as the old value (usually `null` / the default). Rewrite the whole file each time — the viewer re-reads it.

Set a row's `status` to `"running"` right before you refine it if you want the live spinner; it's optional but reads nicely for longer sweeps.

### What goes in each field

Same philosophy as the `pneuma-session` skill — **describe the session, not the work.**

- **`displayName` (≤40 chars):** a scannable label for what the session is about. No mode prefix (the row already shows a mode chip). No trailing period. Headline capitalization.
- **`description` (≤280 chars):** one sentence on the topic / user-facing concern.
- **Match the user's register.** If the session's `digest` is in Chinese, title it in Chinese. English digest → English. Read the room per session — different rows can be different languages.

| Don't | Do |
|---|---|
| "Edited hero.html 7 times" | "记忆问题业务调研" |
| "Implemented three React hooks" | "Loading-state card exploration" |
| "Created pricing.html" | "Pricing page draft for the launch site" |

### 4. Finish

When every `needsTidy` session is `done`, give a one-line summary in chat (e.g. "整理完成：7 个会话已重新命名，2 个已手动命名的跳过"). Don't re-run, don't ask for more. The user's Recent Sessions list now reads cleanly.

## Boundaries

- **Refine only.** Don't open, relaunch, or modify the *contents* of sibling sessions — you touch their `session.json` title/summary via the CLI and nothing else.
- **Never refine yourself.** Every `refine` call must carry `--target-session <sessionId>` for a sibling. A call without it hits this tidy session.
- **Respect manual names.** `sessionName` set → skip, always.
- **Don't fabricate.** If a session is too sparse (e.g. `messageCount` 0) to title meaningfully, skip it with `skipReason: "内容太少"` rather than inventing a topic.
- **One pass.** Sweep once and stop. This isn't an iterative dashboard.
