---
name: r-save
description: >
Use at the end of a session to sync state — plan progress, memory, and the session log.
---
# /rdf:r-save — Session State Sync
End-of-session state sync. Updates tracking files to reflect what
actually happened, persists a session summary for the next agent.
Run this before ending a session or swapping context. The counterpart
to `/rdf:r-start` — save writes the journal, start reads it.
## Arguments
`$ARGUMENTS` — optional flags:
- No args: full save (all sections)
- `--dry-run`: show what would change without writing
- `--plan-only`: sync PLAN.md only (quick save)
## Progress Tracking
See [reference/progress-tracking.md](../reference/progress-tracking.md).
Create all 6 tasks in ONE batch before starting.
Tasks: `Session diff` · `Plan sync` · `Memory sync` · `Audit resolve` ·
`Session log` · `Session insight`
Fallback checklist (Gemini CLI, Codex):
`- [ ] Session diff` · `- [ ] Plan sync` · `- [ ] Memory sync` ·
`- [ ] Audit resolve` · `- [ ] Session log` · `- [ ] Insight`
(`[-]` for skipped phases)
## Protocol
Run all phases sequentially on the main thread. Batch git commands
where possible — run `git log`, `git diff`, `git status` in a single
shell invocation to minimize round-trips. All phases execute silently;
only the final report (section 8) produces visible output.
### 1. Compute Session Diff
A SessionEnd hook may have precomputed this session's deterministic state.
**Cache selection rule:** if `$RDF_SESSION_ID` is set, look for
`.rdf/work-output/session-end-${RDF_SESSION_ID}.json`; otherwise glob
`.rdf/work-output/session-end-*.json` and take the newest one NOT ending in
`.consumed`. On a hit, parse it, SKIP the `rdf-state.sh --full` re-run, and
rename the file to `*.consumed`. If no cache is found, run:
```bash
bash ~/.rdf/state/rdf-state.sh --full .
```
*If the `~/.rdf/state/` helpers are absent (helpers not yet bootstrapped —
fresh plugin install before its first session restart), `/rdf:r-save` cannot sync
session state — tell the user to restart the session (plugin) or run `rdf
deploy claude-code` (checkout) to deliver them.*
Read `.diff_categories` from the state JSON and format the top-3 summary from
it — do NOT re-classify files by hand; that classification is now
deterministic in `rdf-state.sh`. (The SessionEnd cache carries only the git
snapshot, not `diff_categories` — a cache hit still requires this one
`rdf-state.sh` call to obtain them.)
This returns JSON with: HEAD, branch, dirty count + file names,
recent commits, unpushed count, plan phases, pipeline position,
session log last entry, and insights. Parse the JSON.
If `rdf-state.sh` is not found, fall back to individual commands:
```bash
echo "HEAD=$(git rev-parse --short HEAD)" && \
echo "BRANCH=$(git branch --show-current)" && \
echo "DIRTY=$(git status --porcelain | wc -l)" && \
echo "UNPUSHED=$(git rev-list --count HEAD...@{u} 2>/dev/null || echo 0)" && \
git log --oneline -20
```
From the state JSON or fallback, identify:
- New commits since the session started (compare HEAD against
`session_last` field's `head_after`, or MEMORY.md HEAD, or
fall back to the last 10 commits)
- Uncommitted changes (staged + unstaged)
- Files modified across all new commits
- Upstream status (unpushed count)
**Diff characterization:** The `.diff_categories` object in the state JSON
already classifies changed files by path prefix — keys `commands`, `agents`,
`scripts`, `cli`, `adapters`, `specs`, `docs`, `other`. Read those counts
directly; do NOT re-classify files by hand (the classification is
deterministic in `rdf-state.sh`).
Produce a one-line summary: `"3 commands, 1 spec, 2 docs"` (ordered
by count descending, top 3 categories, remainders grouped as "other").
**Dirty file names:** If dirty count > 0, collect the actual file
names (max 5) for the report.
**Pipeline position:** Determine where in the spec→plan→build→ship
arc the project is:
- `docs/specs/` has files + no active plan → `spec` (design complete, plan next)
- Active plan exists with pending phases → `plan` (plan ready, build next)
- Active plan exists with in-progress phases → `build` (building phase N)
- Active plan exists with all phases complete → `ship` (ready to ship)
- None of the above → `idle`
Record all values for the report.
### 2. Sync the Plan with Git
Source `~/.rdf/state/rdf-bus.sh`; `rdf_session_init`. Resolve the plan:
`plan_path="$(rdf_active_plan_path)"`. If empty, skip.
For each phase in `$plan_path`:
**Detect completion:**
- Cross-reference `git log --oneline` against the phase description
and file list
- If commits exist that implement the phase's work AND the phase's
acceptance criteria files are present, mark the phase `complete`
with the commit hash as evidence
**Record spec path:**
- Source `~/.rdf/state/rdf-bus.sh`; `rdf_session_init`. If
`.rdf/work-output/spec-progress-${RDF_SESSION_ID}.md` exists,
read the `SPEC_PATH`. If not, fall back to glob
`.rdf/work-output/spec-progress-*.md` (most recent by mtime);
if neither found, fall back to legacy `.rdf/work-output/spec-progress.md` (pre-3.1.0).
If `SPEC_PATH` is present and non-empty, record it for the session log
`spec_path` field.
**Detect in-progress work:**
- If files listed in a pending phase have uncommitted changes
(`git diff --name-only` matches), mark the phase `in-progress`
- If a phase was previously in-progress and its files have new
commits, check whether the work is complete or still partial
**Rules:**
- Do NOT mark anything complete without verifying the commit exists
- Do NOT reorder phases or change descriptions
- Do NOT change phases that are already marked complete (idempotent)
- Update the progress summary at the top of `$plan_path` if present
Record for report: phases newly completed, phases in-progress,
total phase count.
### 3. Sync MEMORY.md
Locate the project's MEMORY.md in the .rdf/memory/ directory:
`.rdf/memory/MEMORY.md`
If it does not exist, create it with the standard index format.
**Update these fields (preserve everything else):**
- **HEAD hash**: update to current `git rev-parse --short HEAD`
- **Branch**: update to current `git branch --show-current`
- **Version**: grep from source (project-specific — check CLAUDE.md
for the version grep pattern)
- **Pushed**: check `git status` for ahead/behind upstream
**CRITICAL**: Never forward-copy values from prior MEMORY.md entries.
Always grep from source or git for current values.
**Size guard (auto-act)**: After updating, count total lines. If >=180,
invoke `/rdf:r-util-mem-compact` in preview mode and carry its proposed
reduction into the report (§8) as an action line — not a passive warning.
Record for report: whether HEAD changed, commit count recorded,
push status, any warnings.
### 4. Resolve AUDIT.md
If `AUDIT.md` exists in the project root:
- Read the findings/remediation section
- For each unresolved finding, cross-reference against `git log`
and current source
- If a finding's recommended fix was committed (commit exists AND
code change verified), annotate: `RESOLVED (hash)`
- Update executive summary counts if findings were resolved
- Do NOT delete findings — only annotate resolution status
Record for report: findings resolved count, findings remaining.
If no `AUDIT.md`: skip silently.
### 5. Write Session Log
Append a structured entry to `.rdf/work-output/session-log.jsonl`:
```json
{
"timestamp": "{ISO 8601 UTC}",
"project": "{project name}",
"branch": "{branch}",
"head_before": "{hash at session start}",
"head_after": "{current HEAD hash}",
"commits": {N},
"files_changed": {N},
"diff_summary": "{categorized one-line: 3 commands, 1 spec}",
"pipeline": "{idle|spec|plan|build|ship}",
"spec_path": "{path to spec file, or null if none}",
"plan_phases_completed": [{list of phase numbers}],
"plan_phases_in_progress": [{list of phase numbers}],
"dirty_files": {N},
"unpushed": {N},
"insight": "{punchline text, or null if skipped}",
"tokens": {tokens summary object, or null}
}
```
Append the entry as ONE compact JSON line (no pretty-printing);
`head_after` is the short hash (`git rev-parse --short HEAD`).
`tokens` is this session's token usage summary from the state helper, or
`null` when it is unavailable:
```bash
tok="$(bash ~/.rdf/state/rdf-tokens.sh --session "${CLAUDE_CODE_SESSION_ID:-}" --summary 2>/dev/null)" || tok="" # helper absent / no id / no transcript → record null
[ -n "$tok" ] || tok=null
```
Create `.rdf/work-output/` directory if it does not exist.
The `head_before` value comes from: the last session-log entry's
`head_after`, or MEMORY.md's HEAD field, or the oldest of the
session's commits. If no commits were made, `head_before` equals
`head_after`.
### 6. Generate Session Insight
**Zero-change sessions:** If the session produced 0 commits and 0
dirty files, skip insight generation entirely — there is no session
to reflect on.
Reflect on the full session — what was built, how it was built, what
went smoothly, what caused friction or rework. Distill a single
punchline insight: sage advice for how to better operate with the
harness or get better output from the model in future sessions.
**The insight must be:**
- One sentence, max two — a punchline, not a paragraph
- Actionable — the reader should be able to DO something different
- About the harness/workflow/model interaction, not about the code
itself — "name commands consistently from the start" not "fix the
bug in parser.sh"
- Novel — do not repeat an insight already in the last 5 entries
**Read the last 5 entries** from `~/.rdf/insights.jsonl` to check
for duplicates. If the file does not exist, create it. Create the
`~/.rdf/` directory if it does not exist.
**Append one entry:**
```json
{
"timestamp": "{ISO 8601 UTC}",
"project": "{project name from cwd basename}",
"tool": "{claude-code|gemini-cli|codex}",
"insight": "{the punchline}",
"tags": ["{category}", "{category}"]
}
```
Tags are 1-3 categories from: `git`, `naming`, `testing`, `commands`,
`context`, `planning`, `review`, `performance`, `workflow`, `scope`.
**Cap at 30 entries.** If the file exceeds 30 lines after appending,
trim the oldest entries from the top in-place. Use `sed -i` — NOT
`tail -30 file > tmp && mv tmp file` — because interactive shells
commonly alias `mv` to `mv -i`, which stalls waiting for overwrite
confirmation. In-place editing sidesteps the alias entirely:
```bash
lines=$(wc -l < ~/.rdf/insights.jsonl)
[ "$lines" -gt 30 ] && sed -i "1,$((lines - 30))d" ~/.rdf/insights.jsonl
```
### 7. Lessons Learned Prompt
**Check auto-commit setting:** Read `~/.rdf/config.json` (if it
exists) for the `auto_commit_insights` key.
- If `true`: automatically append the insight to
`~/.rdf/lessons-learned.md` under the appropriate category heading.
Note `(auto-committed)` in the report. Skip the prompt.
- If `false` or config does not exist: include the prompt in the
report (see section 8).
**Appending to lessons-learned.md:**
When committing (either auto or user-confirmed), append the insight
as a bullet under the matching category heading in
`~/.rdf/lessons-learned.md`. If the file does not exist, create it
with the standard header (see below). If the category heading does
not exist, add it.
Standard header for new file:
```markdown
# Lessons Learned
Cross-session operational wisdom, promoted from session insights.
Referenced by all AI tools via project CLAUDE.md / GEMINI.md.
Max 50 entries — run `/rdf:r-util-mem-compact` to prune.
```
Category headings map from insight tags:
- `git` → `## Git`
- `naming` → `## Naming`
- `testing` → `## Testing`
- `commands` → `## Commands`
- `context` → `## Context Management`
- `planning` → `## Planning`
- `review` → `## Review`
- `performance` → `## Performance`
- `workflow` → `## Workflow`
- `scope` → `## Scope`
Use the first tag as the category. Bullet format:
`- {insight text}`
**Cap at 50 entries.** If the file exceeds 50 bullets after
appending, note it as a warning in the report.
**Rebuild the lessons index (single writer, F7).** After appending
(y or auto), run:
```bash
bash ~/.rdf/state/rdf-lessons.sh index
```
This rebuilds `~/.rdf/lessons-index.md` AND backfills any missing
`<!-- id:X -->` markers on lessons bullets (idempotent). This is the
ONLY place `lessons-learned.md` / `lessons-index.md` are written
programmatically — the SessionStart inject hook is strictly read-only.
`rdf-lessons.sh index` takes an flock on `lessons-learned.md`, so a
concurrent `/rdf:r-util-mem-compact` consolidation cannot interleave.
### 8. Output Report
This is the ONLY visible text output from the entire save operation.
Everything above executes silently — this section consolidates all
results into one compact block.
**Target: under 20 lines of visible output.**
```
## Save: {Project} v{version} · `{branch}` · {pipeline_stage}
`{start_hash}` → `{end_hash}` · {N} commits · {diff_summary}
- [x] **Plan** — {summary}
- [x] **Memory** — HEAD updated, {N} commits recorded
- [x] **Log** — appended
- [ ] **Audit** — *not present*
> **Insight**: {the punchline text}
>
> Commit as lesson learned? **y** / **n** / **auto**
> **Next** — {pipeline-aware hint}
```
**The heading line** packs project identity, branch, and pipeline
stage into one line. Pipeline stage shows where you are in the arc:
- `idle` → no spec or plan
- `spec` → spec exists, plan next
- `plan` → plan ready, `/rdf:r-build` next
- `build phase 3/7` → actively building
- `ship` → all phases done, `/rdf:r-ship` next
**The diff line** replaces the old 4-column table with a single
dense line: hash range, commit count, and categorized diff summary.
More informative in less space.
**Adaptation rules:**
- Only show action lines for sections that ran — omit skipped
- Audit shows as `[ ]` with *not present* (signals known absence)
- If auto-commit enabled, replace prompt with:
`> **Insight** *(auto-committed)*: {text}`
- If zero-change session, omit insight block entirely
- If `--dry-run`, prefix heading with `[dry-run]`
**Warnings** — append to the Next blockquote only when triggered:
```
> **Next** — `/rdf:r-start` to resume.
> ⚠ {N} unpushed commits — `git push` before switching machines
> ⚠ {N} dirty files: `{file1}`, `{file2}`, `{file3}`
> ▶ MEMORY.md {N}/200 — previewed compaction saves {M} lines; apply? y/n
> ▶ Context ~{N}% — start a fresh session or `/half-clone` now
```
Warning thresholds:
- Unpushed: any (>0 is always shown — this is safety-critical)
- Dirty files: >0 (show up to 5 filenames)
- Memory: >=180 lines → auto-run `/rdf:r-util-mem-compact` preview, show reduction
- Context: >60% estimated (rough heuristic: count conversation
turns × ~2000 tokens, compare to model context limit) → directive
(fresh session / `/half-clone`), not a passive warning
**Pipeline-aware Next hint:**
- `idle` → `Spec a feature with /rdf:r-spec, or plan directly with /rdf:r-plan`
- `spec` → `Spec ready — /rdf:r-plan to create implementation plan`
- `plan` → `/rdf:r-build to start phase 1`
- `build phase N/M` → `/rdf:r-build {N+1} to continue` or `/rdf:r-ship if complete`
- `ship` → `All phases done — /rdf:r-ship to release`
**After displaying the report:** If the prompt is shown (not
auto-commit), wait for the user's response:
- **y**: Append insight to `~/.rdf/lessons-learned.md` per section 7.
Confirm: `Saved to lessons-learned.md`
- **n**: Do nothing. Insight stays in rolling pool only.
- **auto**: Write `{"auto_commit_insights": true}` to
`~/.rdf/config.json` (merge if file exists). Append this insight.
Confirm: `Auto-commit enabled. Saved to lessons-learned.md`
## Rules
- Execute sections 1-7 silently — the report is the only output
- Read before writing — never overwrite content you haven't read
- Preserve all existing content structure and formatting
- Only update facts that are verifiably stale (confirmed via git)
- NEVER use values from prior MEMORY.md entries as source — always
grep from source files or git for current values
- Do NOT stage, commit, or push — this is a save operation only
- Do NOT modify CLAUDE.md — it contains conventions, not volatile
state
- Do NOT run tests or lint — this is a state sync, not verification
- Idempotent: running /rdf:r-save twice with no intervening work should
produce no changes on the second run