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. --session (aliases --snapshot, --pre-compact, --compact) skips settlement and only writes the shared snapshot, keeping the

From plugin
lets-workflow
1726 skills15 agents26 commands
> /plugin marketplace add restarter/lets-workflow
> /plugin install lets@lets-workflow

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. --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 (Step 4) + the orchestrator leftovers line (Step 5) + a one-line terminal prose hint (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:

**Tracker protocol:** `Skill(skill: "lets:protocol-tracker")` before this and any later `lets-tracker` block here, unless its text is in your current context.

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

Read more
@restarter
Ships withlets-workflow

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

1725 skills15 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
17
Star history of restarter/lets-workflow on GitHub, a chart by star-history.com

Other commands on lets-workflow.

backlog

backlog

Command

Backlog review and cleanup - multi-agent backlog review, quick no-agent pulse (--fast), or interactive triage cleanup

execute

execute

Command

Execute implementation plan from /lets:plan - inline in native plan mode, or delegated to named implementer subagents you review…

More commands from other plugins.

build
Auto-invoked

build

Command

Implement tasks incrementally — build, test, verify, commit. Add "auto" to run the whole plan in one approved pass.

@addyosmani@addyosmaniInstall Command