ask
CommandAsk a single expert agent a question - like a Slack ping to a colleague
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
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]"
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.
One settlement core (the default flow); the flags below are separate paths, NOT modifiers of it:
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.
**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
A development workflow plugin for Claude Code Stop babysitting your AI. Start shipping with it.
Ask a single expert agent a question - like a Slack ping to a colleague
Backlog review and cleanup - multi-agent backlog review, quick no-agent pulse (--fast), or interactive triage cleanup
Quick sanity check - code (inline 6-perspective) or plan (--plan).
Execute implementation plan from /lets:plan - inline in native plan mode, or delegated to named implementer subagents you review…
GitHub PR review lifecycle - analyze, discuss, post inline comments, follow-up, respond, approve
Implement tasks incrementally — build, test, verify, commit. Add "auto" to run the whole plan in one approved pass.