/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.
$ npx -y skills add restarter/lets-workflow --agent claude-codeHow 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.mddescription: 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
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
A development workflow plugin for Claude Code Stop babysitting your AI. Start shipping with it.
Other commands on lets-workflow.
- /ask
Ask a single expert agent a question - like a Slack ping to a colleague
Open command - /backlog
Backlog review and cleanup - multi-agent backlog review, quick no-agent pulse (--fast), or interactive triage cleanup
Open command - /check
Quick sanity check - code (inline 6-perspective) or plan (--plan).
Open command - /done
Finish a task - document, create PR or merge, close
Open command - /execute
Execute implementation plan from /lets:plan - load plan and enter native plan mode
Open command - /github-pr
GitHub PR review lifecycle - analyze, discuss, post inline comments, follow-up, respond, approve
Open command

