/end
End a work session - a settlement pass that reconciles uncommitted / unpushed work + session context into git, the tracker, and a session snapshot file. --session (aliases --snapshot, --pre-compact, --compact) skips settlement and only writes the shared snapshot, keeping the
> /plugin marketplace add restarter/lets-workflow > /plugin install lets@lets-workflow
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. --session (aliases --snapshot, --pre-compact, --compact) skips settlement and only writes the shared snapshot, keeping the
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. --session (aliases --snapshot, --pre-compact, --compact) skips settlement and only writes the shared snapshot, keeping the session going.
argument-hint: "[--session]"
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 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.
- **`--session`** (aliases `--snapshot`, `--pre-compact`, `--compact` - one path, four spellings) - a session record on request, **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=session`) and the session continues. Use it to bank the state of a long session, before a `/compact` or not. **Identical to `/lets:note --session`.** See the early-exit at the top of Step 1.
There is deliberately no second flag for the pre-compaction case. The record it writes is permanent and `/lets:start` reads it back, so the snapshot must describe what HAPPENED, not what the caller intended to do next - and "written mid-session, the session continued" is true whether or not a `/compact` follows. Two flags asserting two intents was the wrong shape for that problem: the honest sentence fixes it, a choice at the call site does not.
- **`--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)
**Snapshot-only early exit (runs BEFORE any settlement detection):** if invoked with `--session`, `--snapshot`, `--pre-compact` or `--compact`, do NOT run the settlement detect/settle steps. Delegate straight to the snapshot primitive - `Skill(skill: "lets:session-snapshot", args: "kind=session pointer=auto")` - then show the `--session` Output and STOP. Byte-identical to `/lets:note --session`. 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, **only if detect-task returned a task**, read its STATUS - S2 and S3 both gate on it, and detect-task's own liveness probe runs only on `$LETS_MERGE_BRANCH`, so off the merge-branch nothing here knows the status yet. No task means no read and no id to substitute; skip straight to the state block below:
show task=<task-id> # returns {id,title,status}; read status`/lets:end` runs once per session, so one tracker round-trip is affordable here - unlike the hot paths detect-task deliberately guards. If `show` is unsupported (`LETS_TRACKER=none`) or fails, say so in ONE line and treat the status as UNKNOWN: S2 and S3 both stand down rather than guess a task's state. A settlement offered on a guessed status is the same class of error as a range reported from an unvalidated boundary.
Then resolve the session boundary through the shared reader - `Skill(skill: "lets:session-boundary")`. It echoes `SESSION_BOUNDARY` / `SESSION_TRUST` / `SESSION_COMMITS` / `SESSION_RANGE_DESC`; read them verbatim and surface its stderr NOTEs. Do NOT re-derive any of it here: the validation ladder lives in that skill alone, because a second copy is a second answer, which is how a stale boundary once became a confident "260 commits" for a 6-commit session (lets-370mx). `RANGE_DESC` below IS its `SESSION_RANGE_DESC`.
Then one bash block for the rest of the state:
# Echoed, not just assigned: a bash var dies with the fence, and three consumers downstream need
# the branch - the Output line, Step 4's worktree hint, and S5's label, whose whole job is to name
# the concrete push target (informed consent).
BRANCH=$(git branch --show-current); echo "BRANCH=$BRANCH"
git status --short # DIRTY if non-empty (S1)
# 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 the active task is `in_progress` AND `SESSION_COMMITS > 0`; S3 if the active task's status is `open` or `in_progress` AND this session touched it (`SESSION_COMMITS > 0` OR DIRTY OR `AHEAD > 0`); S5 if `A
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. --session (aliases --snapshot, --pre-compact, --compact) skips settlement and only writes the shared snapshot, keeping the session going. argument-hint: "[--session]"
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 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.
- **`--session`** (aliases `--snapshot`, `--pre-compact`, `--compact` - one path, four spellings) - a session record on request, **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=session`) and the session continues. Use it to bank the state of a long session, before a `/compact` or not. **Identical to `/lets:note --session`.** See the early-exit at the top of Step 1.
There is deliberately no second flag for the pre-compaction case. The record it writes is permanent and `/lets:start` reads it back, so the snapshot must describe what HAPPENED, not what the caller intended to do next - and "written mid-session, the session continued" is true whether or not a `/compact` follows. Two flags asserting two intents was the wrong shape for that problem: the honest sentence fixes it, a choice at the call site does not.
- **`--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)
**Snapshot-only early exit (runs BEFORE any settlement detection):** if invoked with `--session`, `--snapshot`, `--pre-compact` or `--compact`, do NOT run the settlement detect/settle steps. Delegate straight to the snapshot primitive - `Skill(skill: "lets:session-snapshot", args: "kind=session pointer=auto")` - then show the `--session` Output and STOP. Byte-identical to `/lets:note --session`. 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, **only if detect-task returned a task**, read its STATUS - S2 and S3 both gate on it, and detect-task's own liveness probe runs only on `$LETS_MERGE_BRANCH`, so off the merge-branch nothing here knows the status yet. No task means no read and no id to substitute; skip straight to the state block below:
show task=<task-id> # returns {id,title,status}; read status`/lets:end` runs once per session, so one tracker round-trip is affordable here - unlike the hot paths detect-task deliberately guards. If `show` is unsupported (`LETS_TRACKER=none`) or fails, say so in ONE line and treat the status as UNKNOWN: S2 and S3 both stand down rather than guess a task's state. A settlement offered on a guessed status is the same class of error as a range reported from an unvalidated boundary.
Then resolve the session boundary through the shared reader - `Skill(skill: "lets:session-boundary")`. It echoes `SESSION_BOUNDARY` / `SESSION_TRUST` / `SESSION_COMMITS` / `SESSION_RANGE_DESC`; read them verbatim and surface its stderr NOTEs. Do NOT re-derive any of it here: the validation ladder lives in that skill alone, because a second copy is a second answer, which is how a stale boundary once became a confident "260 commits" for a 6-commit session (lets-370mx). `RANGE_DESC` below IS its `SESSION_RANGE_DESC`.
Then one bash block for the rest of the state:
# Echoed, not just assigned: a bash var dies with the fence, and three consumers downstream need
# the branch - the Output line, Step 4's worktree hint, and S5's label, whose whole job is to name
# the concrete push target (informed consent).
BRANCH=$(git branch --show-current); echo "BRANCH=$BRANCH"
git status --short # DIRTY if non-empty (S1)
# 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 the active task is `in_progress` AND `SESSION_COMMITS > 0`; S3 if the active task's status is `open` or `in_progress` AND this session touched it (`SESSION_COMMITS > 0` OR DIRTY OR `AHEAD > 0`); S5 if `A
A development workflow plugin for Claude Code Stop babysitting your AI. Start shipping with it.
Repo: restarter/lets-workflow
Other commands on lets-workflow.
backlog
Backlog review and cleanup - multi-agent backlog review, quick no-agent pulse (--fast), or interactive triage cleanup
execute
Execute implementation plan from /lets:plan - load plan and enter native plan mode
github-pr
GitHub PR review lifecycle - analyze, discuss, post inline comments, follow-up, respond, approve

