session-end
One survey, one preview, one confirmation — then run only the
end-of-session passes that actually qualify. This is the orchestrator
over three capture skills that used to compete for the wind-down moment
(design decisions D3/D4, docs/archive/session-plugin-workflow.md):
| Pass | Skill | Captures |
|---|---|---|
| Wrap | session-plugin:session-wrap |
Loose threads → taskwarrior, optional journal, GitHub issues, upstream issue/PR candidates |
| Distill | session-plugin:session-distill |
Durable learnings → rules, skill updates, justfile recipes, process/methodology (script+recipe or project-local .claude/skills/) |
| Feedback | feedback-plugin:feedback-session |
Notable plugin/skill interactions → GitHub issues on claude-plugins |
| Taskwarrior sync | (inline, no sub-skill) | Close done tasks, update statuses, add follow-ups no open PR/issue already tracks; uses stable UUIDs |
| Blueprint tracker-sync | blueprint-plugin:blueprint-feature-tracker-sync |
Drain closed WO-linked tasks from tracker tasks.pending → tasks.completed (--drain-wave) |
When to Use This Skill
| Use this skill when... | Use alternative when... |
|---|---|
| User winds down ("wrap up", "done for today") and more than one pass may apply | Only loose threads to capture → session-plugin:session-wrap directly |
| The Stop-hook nudge offered this skill and the user confirmed | Only learnings to codify → session-plugin:session-distill directly |
User invokes /session-end |
Mid-session single-task close → taskwarrior-plugin:task-done |
Execution
Execute this orchestration. Not fully automatic by design: filing
GitHub issues and writing a journal are not git restore-able, so the
single confirmation gate below is mandatory.
Step 1: Survey once
One shared decision pass — do not let each sub-skill re-survey. Run the
shared collector (the same one the wrap/spinup skills and the nudge hook
use); it emits detection, git state, PRs, taskwarrior tasks with stable
UUIDs, and recent commits in one parallel-safe pass. Note two scoping
keys in its TASKWARRIOR section: TASK_SCOPE (project /
remote-name / ancestor-name / all-projects-fallback / unknown /
none) names where OPEN_TASKS was actually counted, and
PROJECT_CONFIDENCE (high / low) says whether that slug can be
trusted — the detected project is a directory-basename guess and can be
wrong (chezmoi source dirs, worktrees, portfolio checkouts, renamed
clones). A third pair, PROJECT_AMBIGUOUS / PROJECT_AMBIGUOUS_TASKS,
appears only when the detected slug owns zero tasks while a named
ancestor slug owns some. A fourth, PROJECT_PREFIX_SIBLINGS /
PROJECT_PREFIX_SIBLING_TASKS, appears only when other slugs share the
detected slug's prefix — the split taskwarrior's own CLI filter
(task project:<slug>) hides, which is how a wrong slug gets "verified"
and follow-ups land in a near-empty sibling. PROJECT_EXACT_TASKS is
always present: the slug alone, without its . subprojects.
bash "${CLAUDE_SKILL_DIR}/../../scripts/session-survey.sh" --with-commits --with-blueprint --with-dedup
Pass --project <name> to override the detected project. Hand the digest
to the confirmed passes in Step 4 so they don't re-survey. --with-dedup
is what populates the GITHUB_DRIFT section that Step 4's taskwarrior-sync
redundancy test reads — without it the section is always empty and the
guard silently runs against nothing. That section also carries GH_READY:
false means it is present but unqueried, the same "not clean, just
not asked" signal as PROJECT_CONFIDENCE=low above — don't treat an empty
GITHUB_DRIFT under GH_READY=false as evidence there's nothing to dedup
against. Plus the conversation: what finished, what's hanging, what was
learned, what plugin/skill friction or wins occurred.
Remediating GH_READY=false. It always ships with GH_FAIL_REASON=,
which says why GitHub went unqueried — the six causes want different
responses, so act on the reason rather than treating every false alike.
Never re-run for auth, no-cli, or no-remote. In every case the
GitHub-derived counts stay unqueried, not zero — so the taskwarrior-sync
redundancy test in Step 4 must not use them as evidence a follow-up is
untracked. See REFERENCE.md for the per-reason table.
For the Distill qualify gate (Step 2), also run the distill collector's
coarse summary — the mechanical half of the Distill signal (recipe candidates,
hot files, process groupings). It degrades to TRANSCRIPT_AVAILABLE=false when
no transcript is reachable, so treat a SKIP as "no mechanical signal, judge
conceptually only":
bash "${CLAUDE_SKILL_DIR}/../../scripts/distill-survey.sh" --session-id "${CLAUDE_SESSION_ID}" --summary
Step 2: Qualify each pass
Apply each skill's own signal filter strictly; silently skip passes that don't qualify — offering an empty pass is the drown-in-signals failure mode.
| Pass | Qualifies when |
|---|---|
| Wrap | ≥1 genuine loose thread per session-wrap's LOG IT filter |
| Distill | A durable, generalizable learning emerged AND the repo has a distillable surface (.claude/rules/ or a justfile). Corroborate the mechanical half with the distill collector's --summary (below): RECIPE_CANDIDATE_COUNT / HOT_FILE_COUNT / PROCESS_SIGNAL > 0 means recipes/hot-files/process are worth a pass even if no conceptual rule emerged |
| Feedback | A plugin/skill behaved notably well or badly — bug, enhancement, or positive worth filing |
| Taskwarrior sync | TASK_AVAILABLE=true AND (OPEN_TASKS ≥ 1 OR RECENT_TASK_COUNT ≥ 1 OR PROJECT_AMBIGUOUS_TASKS ≥ 1) in the Step 1 digest. When PROJECT_CONFIDENCE=low, name the scope actually used (TASK_SCOPE, plus PROJECT_RESOLVED when set) in the Step 3 preview and offer --project <slug> — a low-confidence zero is an unqueried project, never a clean queue. When PROJECT_AMBIGUOUS is set, render the preview as 0 here, N under <slug> and offer --project <slug>; this fires even at PROJECT_CONFIDENCE=high, because a user-asserted --project and a repo declaration both deliberately keep high — so the low-confidence escape below does not cover it. When PROJECT_PREFIX_SIBLINGS is present, name it in the preview as N under <slugs> and confirm the slug before filing anything: those slugs are what a task project:<slug> CLI check would have swept in, so a slug verified that way can be the wrong one and the follow-ups land in a sibling nobody reads |
| Blueprint tracker-sync | UNDRAINED_COUNT ≥ 1 in the Step 1 digest's BLUEPRINT section. Non-blueprint / tracker-missing repos auto-disqualify (count is 0) → silent skip. If blueprint-plugin isn't installed, note it and skip (as with Feedback) |
Blueprint auto-drain (ADR-0020 level 1): when the qualifying repo's
docs/blueprint/manifest.json enables and opts the feature-tracker-sync task
into auto-running at autonomy level ≥ 1, that pass is auto-confirmed —
leave it out of the Step 3 question, run it in Step 4 order, and report a
one-line receipt in Step 5. All other passes still go through the Step 3
confirmation. The gate requires all three fields (issue #2358) — run it as
written, auto ⇒ auto-confirm, anything else ⇒ ask:
jq -r 'if ((.automation.autonomy_level // 0) >= 1) and (.task_registry["feature-tracker-sync"].enabled == true) and (.task_registry["feature-tracker-sync"].auto_run == true) then "auto" else "ask" end' docs/blueprint/manifest.json 2>/dev/null
For why all three are required and the safe default for a missing enabled
key, see REFERENCE.md.
If nothing qualifies, say so in one line and end — no preview, no question.
Step 3: One preview, one confirmation
Present a single compact preview: each qualifying pass with a one-line
reason and its concrete payload (the wrap items; the distill proposal
sketch; the feedback finding; the open taskwarrior items with their UUIDs;
for blueprint: Blueprint tracker-sync — N closed WO task(s) not drained from the feature tracker: WO-…).
Then one AskUserQuestion (multiSelect) listing the qualifying passes
as options, qualifying ones described with their reasons. The user picks
any subset; "Other" covers adjustments.
Never end the turn on a freeform "y/n" text question — ending the turn fires Stop hooks mid-confirmation (the race that motivated this orchestrator). AskUserQuestion keeps the turn open.
Step 4: Sequence the confirmed passes
Run in this order, each via the Skill tool (or inline for taskwarrior sync), passing along the Step 1 survey so they don't re-do it:
Taskwarrior sync (if confirmed) — run inline before Wrap so Wrap sees the updated queue state. For each open/active task: ask the user (via AskUserQuestion) whether to mark done, update, or leave. Address tasks by stable UUID (
task +LATEST uuidsafter creation;task <uuid> done/task <uuid> modifyfor existing tasks). Never use volatile numeric IDs — they shift when other tasks complete. Any follow-up added here obeys Wrap's redundancy test: an open PR or assigned issue in the Step 1 digest'sPRS/GITHUB_DRIFTsections is already its own tracker — annotate, never mirror it into a task.session-plugin:session-wrap— closes/annotates/adds tasks so later passes see the final queue state. Any upstream issue/PR candidates it surfaces appear in the Step 3 preview and route per-candidate in Wrap's own Step 4 (track-for-later vs verify-then-file) under this mandatory confirmation gate — filing an upstream issue is notgit restore-able, exactly what the gate exists for. A File now candidate always routes throughworkflow-orchestration-plugin:workflow-verify-before-filing→agent-patterns-plugin:cold-read-gate→ file; never blindBlueprint tracker-sync (if confirmed) — runs after passes 1–2 because both mutate the taskwarrior queue and may close more WO-linked tasks after the Step 1 survey. Re-derive the wave inline right before delegating (never reuse the survey's
UNDRAINED_WOSas the drain list):task bpid.any: status:completed export 2>/dev/null | jq -r --slurpfile t docs/blueprint/feature-tracker.json '([.[] | .bpid // empty] | unique) as $closed | (($t[0].tasks.pending // []) | map(.id)) as $pending | [$closed[] | select(. as $w | $pending | index($w))] | join(",")'Then invoke
/blueprint:blueprint-feature-tracker-sync --drain-wave <list>with no evidence flags — the sync skill sources evidence from taskwarrior annotations itself (its priority order: files → inline → annotation → ask). If the re-derived list is empty, report "already drained" and move on. Cross-plugin: ifblueprint-pluginisn't installed, note it and skipsession-plugin:session-distill— apply mode per its own flow; the user already confirmed the pass, so skip a second blanket prompt but keep distill's per-category destructive-change promptsfeedback-plugin:feedback-session— cross-plugin; if the feedback plugin isn't installed, note it and skip
Step 5: Report
One short block: what each executed pass wrote (tasks touched / closed, files edited, issues filed) and which passes were skipped as not qualifying.
Seam: distill vs feedback
"Discovered a better flag / a skill suggested something subtly wrong" → feedback (issue on claude-plugins). "Found a reusable project pattern, rule, or recipe" → distill (artifact in this repo). When both apply, both run — they write to different places.
Auto-surfacing
A Stop hook (hooks/session-end-nudge.sh) offers this skill at most
once per session when the user's own messages carry a wind-down phrase.
It is offer-only and stays silent when this skill (or wrap/distill) is
already in the transcript. Pre-silence:
touch ~/.cache/claude-session-end-nudge/<session_id>.
Agentic Optimizations
| Context | Command |
|---|---|
| One-pass survey (detection + git + PRs + tasks-with-UUIDs + commits + blueprint tracker state + GitHub-drift dedup) | bash "${CLAUDE_SKILL_DIR}/../../scripts/session-survey.sh" --with-commits --with-blueprint --with-dedup |
| Trust the task count? | TASK_SCOPE= + PROJECT_CONFIDENCE= in the TASKWARRIOR section (low ⇒ re-run with --project <slug> before treating 0 as clean) |
| Which repo do the branch, dirt and PR rows describe? | GIT_SCOPE= + GIT_CONFIDENCE= in the GIT section (PRS_SCOPE= / PRS_CONFIDENCE= mirror them). repo ⇒ the checkout at PROJECT_DIR. workspace-root ⇒ an undeclared outer repo contains that checkout and the rows describe it — name GIT_ROOT= when reporting them, and say the cwd sits inside GIT_NESTED_REPO=. Never re-run from the workspace root: the collector already did |
| Distill qualify signal (recipe/hot-file/process counts) | bash "${CLAUDE_SKILL_DIR}/../../scripts/distill-survey.sh" --session-id "${CLAUDE_SESSION_ID}" --summary |
| Re-derive the drain wave before delegating | task bpid.any: status:completed export | jq … intersected with tracker tasks.pending (Step 4.3) |
| Stable UUID for latest task | task +LATEST uuids |
| Mark task done by UUID | task <uuid> done |
| Distillable surface check | find . -maxdepth 2 -path '*/.claude/rules' -o -maxdepth 1 -name 'justfile' -o -maxdepth 1 -name 'Justfile' |
For the GH_FAIL_REASON remediation table and the Blueprint auto-drain gate
details, see REFERENCE.md.