Session End Skill
SkillDev toolsUse this skill when performing a full session close-out: verifies all planned work against the agreed plan, creates issues for gaps, runs quality gates, commits cleanly, mirrors to GitHub, and produces a session summary. Triggered by /close command.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Session End Skill skill
What this skill tells your AI
The instructions your AI receives, as published by kanevry/session-orchestrator in skills/session-end/SKILL.md and read by ahel’s review.
Platform Note: State files (STATE.md, wave-scope.json) live in the platform's native directory:
.claude/(Claude Code),.codex/(Codex CLI),.cursor/(Cursor IDE), or.pi/(Pi). All references to.claude/below should use the platform's state directory. Shared metrics live in.orchestrator/metrics/. Seeskills/_shared/platform-tools.md.
Project-instruction file:
CLAUDE.mdandAGENTS.md(Codex CLI) are transparent aliases — see skills/_shared/instruction-file-resolution.md. All references toCLAUDE.mdin this skill resolve via that precedence rule.
Phase 0: Bootstrap Gate
Read skills/_shared/bootstrap-gate.md and execute the gate check. If the gate is CLOSED, invoke skills/bootstrap/SKILL.md and wait for completion before proceeding. If the gate is OPEN, continue to Phase 1.
Phase 0.5: Parallel-Aware Preamble
Skip silently when
persistence: falsein Session Config.
Before Phase 1, run the parallel-aware preamble per skills/_shared/parallel-aware-preamble.md. The preamble detects other active sessions in the worktree-family via findPeers(repoRoot, { mySessionId }), classifies the caller's mode via classifyMode(callerMode) against the exclusivity-matrix, and fires the appropriate AUQ on conflict.
Outcome handling:
PASS_THROUGH→ continue to Phase 1EXCLUSIVE_BLOCKED→ exit Phase 0 cleanly per the AUQ outcomePROMOTION_OFFER→ user picks Worktree-Promotion (seeparallel-aware-auq.mdoutcome-handling — callsenterWorktree()), in-place + Deviation, or Abbrechen
For session-end specifically: the preamble is DETECTION-ONLY. The lock-release path in later phases keeps its current behavior — releasing the OWN session's lock requires no matrix consultation.
Implementation reference: skills/_shared/parallel-aware-preamble.md § Implementation.
AUQ reference: skills/_shared/parallel-aware-auq.md.
Phase 0.6: Skill-Invocation Self-Report (#724, C4)
Emit an L1 skill-invocation record for
session-enditself. The PreToolUseSkill-matcher hook only captures skills dispatched via theSkilltool — a prose-invoked skill like this one is invisible to it (verified gap: zerosession-endrows inskill-invocations.jsonldespite many closed sessions). This self-report closes that gap so L2/L3 skill-health has asession-endselection signal. Best-effort, try/catch-silent — it never blocks the close.
try {
const { appendSkillInvocation, DEFAULT_SKILL_INVOCATIONS_PATH } =
await import('${PLUGIN_ROOT}/scripts/lib/skill-invocations-schema.mjs');
const nodePath = await import('node:path');
await appendSkillInvocation(nodePath.join(process.cwd(), DEFAULT_SKILL_INVOCATIONS_PATH), {
timestamp: new Date().toISOString(),
event: 'selected',
skill: 'session-orchestrator:session-end',
session_id: sessionId ?? null, // from session.lock `session_id`, when available
phase: null,
});
} catch { /* self-report is advisory — never block the close */ }
Phase 1: Plan Verification
Always runs, first. Reads back the agreed plan and dispositions every item: 1.1 Done (verify with evidence) + 1.1a File-Level Grounding, 1.2 / 1.3 / 1.4 which COLLECT carryover candidates and file nothing (#769), 1.5 Discovery Scan, 1.6 Safety Review + 1.6.6 "What Not To Retry", then 1.65 Handover Alignment Gate — the only place
[Carryover]filing is authorized to originate, fail-open to "everything carries" — followed by 1.7 Metrics Collection, 1.8 Session Review, and 1.9 / 1.10 Mission-Status classification + breakdown. Full procedure:plan-verification.md.
Phase 2: Quality Gate
Always runs and is BLOCKING — do NOT commit broken code. Runs every check in
verification-checklist.md, then Phase 2.0a Echo-Stub Detection (GH #42 — astubbed: {}entry fromgate-full.mjsis a FAILED gate, never a pass), 2.1 Vault Validation, 2.2 CLAUDE.md (or AGENTS.md) Drift Check and 2.3 Vault Staleness Check — each gated by its own Session Config key. Full procedure incl. the per-mode routing matrices:references/phase-2-quality-gate.md.
Phase 2.5: Custom Phases (#637)
Opt-in. Skip this phase entirely if
custom-phasesin$CONFIGis absent or[](the default).
Repos declare deterministic close/housekeeping phases as a contract (not the freeform special: convention): each phase runs a command with exit-code gating and Final-Report reporting. The block is parsed by scripts/lib/config/custom-phases.mjs; each record is { name, when, command, mode, review } (already validated — unsafe records were dropped at parse time).
Step 1 — Read + filter by when
Read custom-phases from $CONFIG and the session-type from STATE.md frontmatter (feature | deep | housekeeping | none):
- If
session-type === 'housekeeping': keep phases withwhen ∈ {housekeeping, both}. - Otherwise (
feature/deep/any other): keep phases withwhen ∈ {session-end, both}.
If no phases remain after filtering, skip to Phase 3.
Step 2 — Run each phase in declaration order
For each kept phase:
mode === 'off'⇒ skip silently (do not run the command).- Otherwise run
commandvia Bash. Capture the exit code and the last ~10 lines of stdout (these become the report summary — do NOT inline the full output). - If
reviewis set, read that file after the command as the review step and note its path in the report.
Step 3 — Route by mode
mode === 'warn'(default): record the result (name, exit code, summary) for the Phase 6 Final Report "Custom Phases" line. Never block the close — even on a non-zero exit.mode === 'hard':- exit code
0⇒ continue; record<name>: pass (mode=hard). - exit code
≠ 0⇒ BLOCK the close using the same routing pattern as Phase 2.3 strict-mode.mode: hardhere is an operator-declared repo contract (the repo deliberately chosemode: hard), so the block semantics are preserved — but the AUQ now ALSO offers a warn + carryover escape hatch. Present the phase name + captured summary and offer:- On Claude Code: AskUserQuestion with options:
- "Fix and retry Phase 2.5" (Recommended) — exit close, let the user investigate.
- "Warn + carryover and close" — file a carryover issue (labels
carryover,priority::high) titled[Carryover] custom-phase '<name>' (mode=hard) exited <code>capturing the phase name + captured summary for a follow-up session, log the Deviation entry, then continue the close. - "Override and close" — proceed, log a Deviation entry in STATE.md
## Deviations:- [<ISO timestamp>] Phase 2.5: custom-phase '<name>' (mode=hard) exited <code>, overridden by user.In addition to the Deviation entry, emit an override-ratio event so the override feeds theoverride_ratiometric (#730/H5):node scripts/emit-event.mjs --type orchestrator.finding.overridden --payload '{"phase":"2.5","kind":"custom-phase-hard","count":N}'. - "Abort close" — exit close without writing.
- On Codex CLI / Cursor IDE: same options as a numbered Markdown list.
- On Claude Code: AskUserQuestion with options:
- exit code
A hard-fail (whether overridden or not) ALWAYS appends its result line to STATE.md ## Deviations; warn-mode results do not.
Step 4 — Surface to closing report
Pass each phase result (name, mode, exitCode, summary, review?) forward to the Phase 6 Final Report "Custom Phases" line (see Phase 6 below).
Phase 2.6: Broken-Window Budget (#730/H5)
Opt-in via
broken-window-budget.enabledin Session Config (defaultfalse). Skip silently when disabled.
Assemble the in-memory "knowingly-broken shipment" list from THIS session's already-computed results — no new detection logic, only aggregation:
- Phase 2.0a stub findings (
result.stubbed) that shipped anyway underenforcement: warn. - Phase 2.3 / 2.5 "Override and close" choices (reuse each entry's Deviation-log payload verbatim).
- Phase 1.8 MED/LOW findings routed to "Unresolved Review Findings" (#617).
- Wave-level reviewer findings overridden without a fix task (
## Deviationsentries matchingreviewer finding overridden— written by wave-executor §5/5a).
For EACH item: file a hard-terminated closure issue via createBrokenWindowIssue()
from scripts/lib/spiral-carryover.mjs — labels broken-window + priority::high,
due-date = today + broken-window-budget.due-days (default 7; glab native
--due-date, gh fallback: Due: <date> as first body line — GitHub has no
native due-date field). Idempotent per task-hash — re-running a close never
duplicates issues.
Emit ONE event per filed issue (note: event-name segments use underscores, never hyphens):
node scripts/emit-event.mjs --type orchestrator.broken_window.filed --payload \
"$(node -e "process.stdout.write(JSON.stringify({source:'<2.0a|2.3|2.5|1.8|wave-override>', issue:<IID>, due:'<YYYY-MM-DD>'}))")"
Non-blocking: a filing failure is a WARN, never blocks the close (same fail-open
discipline as createSpiralCarryoverIssue).
Phase 3: Documentation Updates
Always runs. Refreshes the session-lock heartbeat at entry (
updateHeartbeat, #590-3 — skip whenpersistence: false) so a long close-out cannot lapse the 4h TTL, then walks 3.0 Defensive Cleanup → 3.1 SSOT files → 3.2 Docs Verification → 3.2a Session Handover → 3.3 Claude-rules freshness → 3.4/3.4a STATE.md write + coordinator-snapshot cleanup → 3.45 Telemetry Flush → 3.5/3.5a/3.6.x session memory, learning extraction and the mechanical skip-plan tail → 3.7/3.7a/3.7b/3.7c/3.7d metrics write, recommendations, durable commit (#490), vault board → closed, session-eval. Full procedure:references/phase-3-documentation-updates.md.
Phase 3.8: Session Lock Release (#330)
Gate: Only run if
persistenceistruein Session Config. Skip silently otherwise.
After STATE.md is finalized with status: completed (Phase 3.4) and Recommendations are written (Phase 3.7a), release the distributed session-lock so the next session can acquire it cleanly:
import { release } from 'scripts/lib/session-lock.mjs';
// sessionId is the physical raw value established by session-start Phase 1.2
// and stored in .orchestrator/session.lock `session_id`. It is not STATE.md
// `session:` or `semantic_session_id`, both of which are attribution labels.
const rawSessionId = sessionId;
const result = release({ sessionId: rawSessionId, repoRoot: process.cwd() });
// result.ok is always true unless a filesystem error occurred.
// result.deleted === true → lock file removed successfully.
// result.deleted === false → lock was absent or had a different raw session_id.
If result.deleted === false, log info: session-lock not released — already absent or raw session_id mismatch and continue. An active lock whose raw id differs is ambiguous: do not retry release with an equal semantic_session_id, STATE.md session, or owner proof. Leave that live lock for its TTL/Reaper lifecycle.
If result.ok === false (rare filesystem error), log ⚠ session-lock: release failed — <result.reason> and continue. Do NOT block the close for a lock-release failure — the TTL provides automatic expiry for the next session.
The lock is released here — AFTER all STATE.md writes are complete and BEFORE the commit is staged in Phase 4.1. This ordering ensures a clean handover when the current raw owner releases it: the lock file is absent from the working tree when the commit is assembled, so it is not accidentally staged.
Phase 4: Commit & Push
4.1 Stage Changes
- Stage files individually:
git add <file>— NEVERgit add .orgit add -A - Always stage these session artifacts (if modified):
.orchestrator/metrics/sessions.jsonl(session summary from Phase 3.7).orchestrator/metrics/learnings.jsonl(learnings from Phase 3.6).orchestrator/metrics/eval.jsonl(eval record from Phase 3.7d, if modified — note: in repos where metrics are gitignored this is a no-op)<state-dir>/STATE.md(session state, if persistence enabled)- Any files created or modified by wave agents
- Review staged changes:
git diff --cached— verify every change is from THIS session - If you see changes you did NOT make, ask the user (parallel session awareness)
4.2 Commit
Use Conventional Commits format:
type(scope): description
- [bullet points of what changed]
- Closes #IID1, #IID2 (if applicable)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For sessions with many changes, prefer ONE commit per logical unit (not one mega-commit).
4.3 Push
git push origin HEAD
4.4 GitHub Mirror (if configured in Session Config)
Three states, three DISTINGUISHABLE outcomes. The predecessor of this block
(git remote get-url github 2>/dev/null && git push github HEAD 2>/dev/null || echo "GitHub mirror: not configured")
collapsed a failed push into GitHub mirror: not configured and exited 0 — git's real
error went to /dev/null, so a broken mirror was indistinguishable from an unconfigured one
(.claude/rules/bash-harness-pitfalls.md — "Silence is not success"). That matters more once
anything is wired to the mirror (e.g. a Vercel Git deploy): a silently-failing push means the
downstream artifact never updates and nobody is told.
Run it verbatim — tests/skills/session-end/github-mirror-push.test.mjs extracts the block
between the markers and executes it, so no second copy of this command may exist.
# --- github-mirror-push:begin ---
# Only attempt if 'mirror: github' is in Session Config.
# State 0: not a git repository at all → loud WARN, exit 1. This state was MISSED
# in the first version and is the reason it is listed first now: outside
# a repo, `git remote get-url` fails with "fatal: not a git repository",
# which is indistinguishable from "no such remote" by exit code alone.
# The block then announced "no 'github' remote configured — skipping
# (not an error)" and exited 0 — fail-open, in the very fix written to
# close a fail-open. Found by an adversarial reviewer, not by the author.
# State 1: no 'github' remote → informational, exit 0 (legitimate for consumer repos)
# State 2: push succeeded → confirmation WITH the pushed SHA, exit 0
# State 3: push FAILED → loud WARN on stderr WITH git's real output, exit 1
if ! git_dir=$(git rev-parse --git-dir 2>&1); then
echo "WARN GitHub mirror: not a git repository — cannot mirror anything." >&2
echo " git said: ${git_dir}" >&2
exit 1
elif ! mirror_url=$(git remote get-url github 2>&1); then
echo "GitHub mirror: no 'github' remote configured — skipping (not an error)."
echo " git said: ${mirror_url}" >&2
elif push_out=$(git push github HEAD 2>&1); then
echo "GitHub mirror: pushed $(git rev-parse HEAD) -> ${mirror_url}"
else
echo "WARN GitHub mirror PUSH FAILED: $(git rev-parse HEAD) is NOT on ${mirror_url}" >&2
echo "${push_out}" >&2
echo "WARN Mirror is stale — anything wired to it (site deploy) will not update." >&2
exit 1
fi
# --- github-mirror-push:end ---
State 3 exits non-zero on purpose: it is the only machine-readable signal that the mirror is
behind. Report it to the operator in the session summary; do not retry silently and do not
swallow it with || true.
Phase 4a: Auto-Promoted Worktree Cleanup (#575 P3.2)
Skip when
persistence: false; skip silently unless the CURRENT worktree is an auto-promoted sibling —detectAutoPromotedWorktree()fromscripts/lib/session-end/worktree-cleanup.mjs(marker.orchestrator/promoted-from.jsonfirst, legacy basename match as fallback, #1069). Runs AFTER the Phase 4 commit+push, never before (#490 durableCommit ordering). Clean worktree → auto-remove with WARN; dirty → the 3-option AUQ (Behalten/Löschen/Manuell) before any destructive action, per PSA-003. Full procedure:references/phase-4a-worktree-cleanup.md.
Phase 4b: Worktree-Orphan Sweep (#831/B5)
Skip when
persistence: false; skip silently unlessworktree-orphans.enabled: true(opt-in, defaultfalse). Runs AFTER the Phase 4 commit+push (same #490 invariant as 4a).checkWorktreeOrphans()fromscripts/lib/session-end/worktree-orphan-sweep.mjsPROPOSES with a read-only argv set; the coordinator DISPOSES via the AUQ it renders itself. Full procedure:references/phase-4b-worktree-orphan-sweep.md.
Phase 5: Issue Cleanup
Always runs. Closes resolved issues (stripping
status:*labels first viastripStatusLabels, #308), updates partially-done issues, and in Step 3 FILES the Phase 1.65 gate's carry-list — the deferredcreateSpiralCarryoverIssuecall for SPIRAL/FAILED items and themarkOpenQuestionAnsweredOnDiskwrite live here, not in Phase 1.65 (atomicity). Step 3b folds non-exempt over-cap creations into one[Backlog-Sammel]collector under theissue-budgetcap; discovery findings from Phase 1.5 are filed at the end. Full procedure:references/phase-5-issue-cleanup.md.
Phase 6: Final Report
Present to the user the Session Summary: Completed / Carried Over / Dropped at Handover Gate (#769) / New Issues Created / Unresolved Review Findings (MED-LOW, #617) / Metrics — including the Docs Health line rendered from the Phase 2.3 result and the Custom Phases line from Phase 2.5 — / Next Session Recommendations. Full template plus the Test-delta and Documentation-Coverage anchors: references/session-summary-template.md.
Sub-File Reference
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 50
- Forks
- 7
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
session-end-kanevry- Source
- github.com/kanevry/session-orchestrator