Skip to content
Development
Command

/end

End a work session - a settlement pass that reconciles uncommitted / unpushed work + session context into git, the tracker, and a session snapshot file. --pre-compact skips settlement and only writes the shared snapshot, keeping the session going.

From plugin
lets-workflow
1622 skills15 agents22 commands
Install
$ npx -y skills add restarter/lets-workflow --agent claude-code

How it fires

How this command gets triggered: by you, by Claude, or both.

  • Fires itselfClaude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/end

Context preview

What this command does when you run it.

End a work session - a settlement pass that reconciles uncommitted / unpushed work + session context into git, the tracker, and a session snapshot file. --pre-compact skips settlement and only writes the shared snapshot, keeping the session going.

Command definition

end.md
description: End a work session - a settlement pass that reconciles uncommitted / unpushed work + session context into git, the tracker, and a session snapshot file. --pre-compact skips settlement and only writes the shared snapshot, keeping the session going.
argument-hint: "[--pre-compact]"

Session End

End a work session cleanly. `/lets:end` is a **settlement pass**: it reconciles the volatile session state (uncommitted changes, unpushed commits, in-conversation context) into durable stores (git remote, the tracker, a session-snapshot file) so the window can close - or compact - without losing anything.

**This is NOT task completion.** Use `/lets:done` to finish a TASK; `/lets:end` ends a SESSION.

**Invariant:** `/lets:end` only READS the `.task-<slug>` `session:` boundary - it NEVER writes it. The boundary writers are `take-task` (at `/lets:start`) and the SessionStart hook (refresh on a genuinely new session). End mutating the boundary was the old `--fast` divergence bug; it stays gone.

> **IMPORTANT:** If the spec below invokes any deferred tool (e.g. `AskUserQuestion`), you MUST load and call it as specified. Never skip the call, never substitute a default answer of your own - the tool invocation is part of the contract. This is critical.

Modes

One settlement core (the default flow); the two flags below are separate paths, NOT modifiers of it:

  • **(default)** - full settlement pass (Steps 1-3) + worktree hint + a one-line terminal prose hint (Step 4 / Output). Auto-skip keeps it silent on a tidy session.
  • **`--pre-compact`** (alias `--compact`) - a pre-compaction snapshot, **NOT a session end and NOT a settlement pass**. It runs NO settlement (no commit / push / progress / finish offers) - it ONLY writes the shared session snapshot via `session-snapshot` (`kind=precompact`) and lets the session continue into `/compact`. **Identical to `/lets:note --pre-compact`** - both delegate to the same primitive with the skill owning task detection. See the early-exit at the top of Step 1.
  • **`--fast`** - DEPRECATED. It now runs the default flow (which already stays silent when there is nothing to settle). Emit one line - `--fast is deprecated; running the unified /lets:end (it auto-skips when there's nothing to settle).` - then proceed as default. (Accepted for one release; removal is a follow-up.)

Step 1: Detect (silent)

**`--pre-compact` early exit (runs BEFORE any settlement detection):** if invoked with `--pre-compact` / `--compact`, do NOT run the settlement detect/settle steps. Delegate straight to the snapshot primitive - `Skill(skill: "lets:session-snapshot", args: "kind=precompact pointer=auto")` - then show the `--pre-compact` Output and STOP. This path is byte-identical to `/lets:note --pre-compact`. Steps 1-3 below are the DEFAULT (settlement) flow only.

Read all state ONCE, compute which settlements are actionable, prompt nothing here.

First find the active task: use the **detect-task** skill - `Skill(skill: "lets:detect-task")`. (No task -> S2/S3 below auto-skip; main-mode `/lets:end` is a normal, mostly-silent settle.)

Then one bash block (REUSE dsdmp's hardened `session:` reader verbatim - the staleness NOTE, the `.session-start-ref` back-compat back-fill, and the hex guard are load-bearing; do not re-derive):

LETS_PROJECT_ROOT=$(git rev-parse --show-toplevel)
BRANCH=$(git branch --show-current); BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')

git status --short   # DIRTY if non-empty (S1)

# --- session: boundary (dsdmp canonical reader) ---
TASK_FILE="$LETS_PROJECT_ROOT/.lets/sessions/.task-${BRANCH_SLUG}"
SESSION_LINE=$(sed -n 's/^session: //p' "$TASK_FILE" 2>/dev/null | head -1)
START_REF=$(echo "$SESSION_LINE" | awk '{print $1}')
STORED_SID=$(echo "$SESSION_LINE" | awk '{print $2}')
# Session-id staleness: a sid different from the current session means no /lets:start ran this
# session (no take-task / hook refresh), so the boundary is from a PRIOR session. Degrade loudly.
if [ -n "$STORED_SID" ] && [ "$STORED_SID" != "$CLAUDE_CODE_SESSION_ID" ]; then
  echo "NOTE: session boundary is from a previous session (no /lets:start this session) - commit counts below are best-effort from ${START_REF}." >&2
fi
# Back-compat: the legacy .session-start-ref IS a session boundary, so it may back-fill session:.
[ -z "$START_REF" ] && START_REF=$(cat "$LETS_PROJECT_ROOT/.lets/sessions/.session-start-ref-${BRANCH_SLUG}" 2>/dev/null)
[ -z "$START_REF" ] && START_REF=$(cat "$LETS_PROJECT_ROOT/.lets/sessions/.session-start-ref" 2>/dev/null)  # oldest non-slug, deliberate one-release window
# Defense-in-depth (N1): the boundary is a plugin-written SHA - blank anything non-hex before it
# expands unquoted into the git range, so a hand-edited state file can't inject git option args.
case "$START_REF" in *[!0-9a-f]*) START_REF="" ;; esac

# Commits this session (0 => S2/S3 skip; empty START_REF => unknown, see Step 1 no-boundary rule).
# RANGE_DESC is the human-readable range line the Step 3 templates use - stays tidy when the
# boundary is unknown (no empty "session: ..HEAD ( commits)").
if [ -n "$START_REF" ]; then
  SESSION_COMMITS=$(git rev-list --count ${START_REF}..HEAD)
  RANGE_DESC="session: ${START_REF}..HEAD (${SESSION_COMMITS} commits)"
else
  SESSION_COMMITS=""
  RANGE_DESC="boundary unknown (no /lets:start this session)"
fi
echo "START_REF=${START_REF:-<none>}  SESSION_COMMITS=${SESSION_COMMITS:-<unknown>}  RANGE_DESC=${RANGE_DESC}"

# Unpushed (S5), upstream-aware (mirrors /lets:done Step 8)
if git rev-parse --abbrev-ref @{u} >/dev/null 2>&1; then
  AHEAD=$(git rev-list --count @{u}..HEAD); echo "AHEAD=$AHEAD"
else
  echo "AHEAD=no-upstream ($(git rev-list --count HEAD 2>/dev/null || echo 0) local commits)"
fi

# Worktree (S6)
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null); echo "GIT_DIR=$GIT_DIR"

**Actionable set:** S1 if DIRTY; S2 if active task `in_progress` AND `SESSION_COMMITS > 0`; S3 same gate as S2; S5 if `AHEAD > 0` (or no-upstream

Read more
Ships withlets-workflow

A development workflow plugin for Claude Code Stop babysitting your AI. Start shipping with it.

Get the whole plugin, auto-invoked
Stats
16
Stars
1
Views
3
Forks
Active
Maintenance
Go
Language
MIT
License
3d ago
Last commit
5mo ago
Created

Repo: restarter/lets-workflow