--- name: context-save preamble-tier: 2 version: 1.0.0 description: | Save working context. Captures git state, decisions made, and remaining work so any future session can pick up without losing a beat. Use when asked to "save progress", "save state", "context save", or "save my work". Pair with /context-restore to resume later. (gstack) allowed-tools: - Bash - Read - Write - Glob - Grep - AskUserQuestion triggers: - save progress - save state - save my work - context save --- {{PREAMBLE}} # /context-save — Save Working Context You are a **Staff Engineer who keeps meticulous session notes**. Your job is to capture the full working context — what's being done, what decisions were made, what's left — so that any future session (even on a different branch or workspace) can resume without losing a beat via `/context-restore`. **HARD GATE:** Do NOT implement code changes. This skill captures state only. --- ## Detect command Parse the user's input to determine the mode: - `/context-save` or `/context-save ` → **Save** - `/context-save list` → **List** If the user provides a title after the command (e.g., `/context-save auth refactor`), use it as the title. Otherwise, infer a title from the current work. If the user types `/context-save resume` or `/context-save restore`, tell them: "Use `/context-restore` to resume — restoring is a separate skill." --- ## Save flow ### Step 1: Gather state ```bash {{SLUG_SETUP}} ``` Collect the current working state: ```bash echo "=== BRANCH ===" git rev-parse --abbrev-ref HEAD 2>/dev/null echo "=== STATUS ===" git status --short 2>/dev/null echo "=== DIFF STAT ===" git diff --stat 2>/dev/null echo "=== STAGED DIFF STAT ===" git diff --cached --stat 2>/dev/null echo "=== RECENT LOG ===" git log --oneline -10 2>/dev/null ``` ### Step 2: Summarize context Using the gathered state plus your conversation history, produce a summary covering: 1. **What's being worked on** — the high-level goal or feature 2. **Decisions made** — architectural choices, trade-offs, approaches chosen and why 3. **Remaining work** — concrete next steps, in priority order, each marked with how this session knows it (see the provenance table below) 4. **Notes** — anything a future session needs to know (gotchas, blocked items, open questions, things that were tried and didn't work) **Remaining Work provenance.** Every item starts with the status `Open.` and never with a completion token such as `[done]` or `[executed]`. Then pick its trailing marker by the first rule that applies: | # | The item is... | Marker | Use when this session... | |---|----------------|--------|--------------------------| | 1 | a writing step (migration, sync, insert, import, a dialog that writes), with or without a concrete path | `(target state checked)` | inspected the target's schema or state (for example the unique key) | | | | `(code read)` | read the writer but not the target | | | | `(path assumed)` | inferred the write without reading either | | 2 | otherwise, names a concrete path: a runnable command, CLI flag or switch, config key or value, or file or directory path | `(path run)` | executed it and observed the outcome; state the outcome in the item (`exit 0`, `failed: <reason>`) | | | | `(path read)` | read the code or file but did not execute it | | | | `(path assumed)` | inferred it without running or reading it, however confident | | 3 | anything else | none | (`Open.` alone) | Writing steps have their own markers because whether a write is safe to repeat depends on the target's state, not on the code that writes. Examples: `Open. Run the suite with CONFIG=staging. (path run) exit 0` and `Open. Switch B reads config Y. (path assumed)`. **Markers stay English literals.** `/context-restore` sorts on the exact strings `(path run)`, `(path read)`, `(path assumed)`, `(target state checked)`, and `(code read)`. Never translate them, even when the rest of the checkpoint is in another language. After drafting Remaining Work, scan each item that should have a marker: if it ends in a parenthesised token that is not one of those five, or has no marker when the table requires one, rewrite that item before writing the file (preferred), or add a Remaining Work item that names the bad line so restore puts it under Verify first. **Decisions Made and file claims.** Provenance above covers Remaining Work only. Any statement in Decisions Made about the *contents* of a concrete file (item counts, line counts, section names) must be backed by a `grep`/`wc`/`Read` in this turn, or rewritten with an honest `(path assumed)` note in Remaining Work that restore will verify first. Do not record an unchecked file claim as fact. If the user provided a title, use it. Otherwise, infer a concise title (3-6 words) from the work being done. ### Step 3: Compute session duration Try to determine how long this session has been active: ```bash if [ -n "$_TEL_START" ]; then START_EPOCH="$_TEL_START" elif [ -n "$PPID" ]; then _LSTART=$(ps -o lstart= -p "$PPID" 2>/dev/null) # BSD date (macOS) parses with -jf; GNU date (Linux) with -d. Never parse an # empty start: GNU date -d "" means today at midnight. [ -n "$_LSTART" ] && START_EPOCH=$(date -jf "%c" "$_LSTART" "+%s" 2>/dev/null || date -d "$_LSTART" "+%s" 2>/dev/null || echo "") fi if [ -n "$START_EPOCH" ]; then NOW=$(date +%s) DURATION=$((NOW - START_EPOCH)) echo "SESSION_DURATION_S=$DURATION" else echo "SESSION_DURATION_S=unknown" fi ``` If the duration cannot be determined, omit the `session_duration_s` field from the saved file. ### Step 4: Write saved-context file Compute the path in bash (NOT in the LLM prompt) so user-supplied titles can't inject shell metacharacters into any subsequent command. The sanitizer is an allowlist: only `a-z 0-9 - .` survive. ```bash {{SLUG_SETUP}} GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints" mkdir -p "$CHECKPOINT_DIR" TIMESTAMP=$(date +%Y%m%d-%H%M%S) # Bash-side title sanitize. Pass the raw title via TITLE_RAW when running this block. # Example: TITLE_RAW="wintermute progress" bash -c '...' RAW="${TITLE_RAW:-untitled}" # Lowercase, collapse whitespace to hyphens, strip to allowlist, cap length. TITLE_SLUG=$(printf '%s' "$RAW" | tr '[:upper:]' '[:lower:]' | tr -s ' \t' '-' | tr -cd 'a-z0-9.-' | cut -c1-60) TITLE_SLUG="${TITLE_SLUG:-untitled}" # Collision-safe filename: if ${TIMESTAMP}-${SLUG}.md already exists (same-second # double save with same title), append a short random suffix. Filenames are # append-only — never overwrite. FILE="${CHECKPOINT_DIR}/${TIMESTAMP}-${TITLE_SLUG}.md" if [ -e "$FILE" ]; then SUFFIX=$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom 2>/dev/null | head -c 4 || printf '%04x' "$$") FILE="${CHECKPOINT_DIR}/${TIMESTAMP}-${TITLE_SLUG}-${SUFFIX}.md" fi echo "CHECKPOINT_DIR=$CHECKPOINT_DIR" echo "TIMESTAMP=$TIMESTAMP" echo "FILE=$FILE" ``` The on-disk directory name is `checkpoints/` (not `contexts/`) — this is a legacy path kept so existing saved files remain loadable. Users never see it. Write the file to the `$FILE` path printed above (use the exact string — do not reconstruct it in the LLM layer). The file format: ```markdown --- status: in-progress branch: {current branch name} timestamp: {ISO-8601 timestamp, e.g. 2026-04-18T14:30:00-07:00} session_duration_s: {computed duration, omit if unknown} files_modified: - path/to/file1 - path/to/file2 --- ## Working on: {title} ### Summary {1-3 sentences describing the high-level goal and current progress} ### Decisions Made {Bulleted list of architectural choices, trade-offs, and reasoning} ### Remaining Work {Numbered list of concrete next steps, in priority order. Each item starts with `Open.` and ends with its provenance marker from Step 2, or no marker under rule 3.} ### Notes {Gotchas, blocked items, open questions, things tried that didn't work} ``` The `files_modified` list comes from `git status --short` (both staged and unstaged modified files). Use relative paths from the repo root. Do not write `remote:`, `project_root:` or `worktree:` yourself; the stamp step below adds them. Then stamp the project identity. Several projects can share one checkpoints directory, so `/context-restore` needs to know which project wrote each file. The helper adds `remote:` (canonical origin: lowercased host, no credentials, port or `.git`), `project_root:` and, in a linked worktree, `worktree:` to the frontmatter. When the session started in another directory that files elsewhere (a non-git parent of this repo), it also leaves a pointer there so `/context-restore` in that directory finds this save: ```bash FILE="<the FILE path printed above>" ~/.claude/skills/gstack/bin/gstack-slug --stamp-checkpoint "$FILE" && echo "STAMPED" [ -z "${CLAUDE_PROJECT_DIR:-}" ] || ~/.claude/skills/gstack/bin/gstack-slug --point-from "$CLAUDE_PROJECT_DIR" "$FILE" 2>/dev/null || true ``` After writing, confirm to the user: ``` CONTEXT SAVED ════════════════════════════════════════ Title: {title} Branch: {branch} File: {path to saved file} Modified: {N} files Duration: {duration or "unknown"} ════════════════════════════════════════ Restore later with /context-restore. ``` --- ## List flow ### Step 1: Gather saved contexts ```bash {{SLUG_SETUP}} GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints" if [ -d "$CHECKPOINT_DIR" ]; then echo "CHECKPOINT_DIR=$CHECKPOINT_DIR" # Use find + sort instead of ls -1t: filename YYYYMMDD-HHMMSS prefix is the # canonical order (stable across copies/rsync; mtime is not), and empty-result # behavior is clean (no files → no output, no "lists cwd" fallback). find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r else echo "NO_CHECKPOINTS" fi ``` ### Step 2: Display table **Default behavior:** Show saved contexts for the **current branch** only. If the user passes `--all` (e.g., `/context-save list --all`), show contexts from **all branches**. Read the frontmatter of each file to extract `status`, `branch`, and `timestamp`. Parse the title from the filename (the part after the timestamp). Present as a table: ``` SAVED CONTEXTS ({branch} branch) ════════════════════════════════════════ # Date Title Status ─ ────────── ─────────────────────── ─────────── 1 2026-04-18 auth-refactor in-progress 2 2026-04-17 api-pagination completed 3 2026-04-15 db-migration-setup in-progress ════════════════════════════════════════ ``` If `--all` is used, add a Branch column: ``` SAVED CONTEXTS (all branches) ════════════════════════════════════════ # Date Title Branch Status ─ ────────── ─────────────────────── ────────────────── ─────────── 1 2026-04-18 auth-refactor feat/auth in-progress 2 2026-04-17 api-pagination main completed 3 2026-04-15 db-migration-setup feat/db-migration in-progress ════════════════════════════════════════ ``` If there are no saved contexts, tell the user: "No saved contexts yet. Run `/context-save` to save your current working state." --- ## Important Rules - **Never modify code.** This skill only reads state and writes the context file. - **Always include the branch name** in frontmatter — critical for cross-branch `/context-restore`. - **Saved files are append-only.** Never overwrite or delete existing files. Each save creates a new file. - **Infer, don't interrogate.** Use git state and conversation context to fill in the file. Only use AskUserQuestion if the title genuinely cannot be inferred. - **This is a gstack skill, not a Claude Code built-in.** When the user types `/context-save`, invoke this skill via the Skill tool.