plugins/coordinator/skills/session-end/SKILL.md
Wrap up finished work — capture lessons, update docs
npx skillsauth add oduffy-delphi/coordinator-claude session-endInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Close out a finished vein of work: capture lessons and update documentation to reflect completion. No handoff — this is for work that's done, not being passed forward.
/session-endand/handoffare mutually exclusive — never combined./session-endcaps a workstream;/handoffpasses one on. A session terminates via exactly one of them (or via/workday-complete//merge-to-main/ commit-and-stop). If the work is in-flight and needs a successor, STOP — invoke/handoffinstead and do not run this skill. If you are tempted to run both ("end the session AND write a handoff"), the underlying state is two workstreams: end the finished one here, hand off the in-flight one separately with/handoff, framing which is which.
When invoked, capture lessons and update plan/project documentation to reflect completion status. If work is incomplete and needs to be picked up later, use /handoff instead — not in addition.
Design note: Multiple agents may be running concurrently. This skill closes out ONE agent's session without heavy repo-wide operations that could conflict with other agents.
This skill is mirror-shaped to /handoff: a small set of sequential gates plus a TODO-LIST cluster of independent post-work cleanup steps. Treat them as such — do not ladder-walk the todo-list. Convention: docs/wiki/skill-step-parallelization.md.
Sequential gates (real data-dependency edges — must be in this order):
docs/plans/<feature>.md) must be staged in Step 3 before commit. Step 2.4 is a micro-chain off Step 2 in the todo-list cluster (see below); its output is part of Step 3's fan-in union, identical in shape to the existing Step 2.9-integrator-edits → Step 3 staging edge.Todo-list (execute in any order, batch parallel where two independently read/write different files — with the Step 2→2.4 micro-chain exception noted below):
tasks/lessons.md). The 1→1.2 edge is real; run them sequentially as a unit; the pair parallelizes with the other slots.docs/plans/, tasks/<feature>/todo.md, etc.), then plan-doc reconciliation. The 2→2.4 edge is real: Step 2.4 reconciles the plan doc Step 2 just updated. Run them sequentially as a unit; the pair parallelizes with the other slots. Skip Step 2.4 if no governing plan exists.**Status:** fields)archive/completed/YYYY-MM/; internal chain 2.6.1→2.6.7 is real but isolated to this slot)These six slots touch disjoint surfaces. The Step 2→2.4 micro-chain is internal to the Step 2 slot (Step 2.4 reconciliation consumes Step 2's plan-doc output); similarly the Step 1→1.2 pair is internal to the Step 1 slot. Among peer slots, none consumes another's output. Where two slots are pure disk operations on different paths, run them in the same response via parallel tool calls. Step 2.9 (review) has a soft preference to land with the todo-list cluster so its integrator edits stage with Step 3, but does not consume the 2.x cluster's output.
Step 3 is a fan-in, not a sequence. It stages the union of all files touched by the cluster — peer step ordering relative to each other is irrelevant; only their position before Step 3 matters.
Read tasks/lessons.md (if it exists). If anything was learned this session that isn't already captured, add it — but apply the intake filter first.
Create on first use: tasks/lessons.md is not scaffolded by /project-onboarding (it would be empty — no lessons exist on day 1). If lessons exist to capture AND the file does not exist yet, create it now using the template header:
# Lessons — [Project Name]
Engineering patterns worth internalizing. Bold title + 1-2 sentence rule. Max 3 lines per entry.
<!-- This file is maintained by the EM. See CLAUDE.md § Self-Improvement Loop for conventions. -->
Then append the new entry. If there are no lessons to capture and the file doesn't exist, do not create it.
Feature scope: <feature> is derived from the current work context:
tasks/<feature>/todo.md, use that feature namefeature/<name> branch, use <name>tasks/lessons.md (global)What qualifies:
What doesn't qualify: One-off bug fixes, details specific to a single script/pipeline run, or anything already encoded in the code, CLAUDE.md, or MEMORY.md. Before adding, ask: "Will this save time in the next 4 weeks, or is it just documenting what happened?"
Add new entries in the established format (bold title + 1-2 sentence rule, max 3 lines). Prefer merging with an existing entry over adding a new one. Skip if nothing new.
For each new lesson added in Step 1, ask the tier-1 question: "If a different project type — UE / web / data / research — also used the coordinator pipeline, would this rule apply?" This is autonomous self-classification; no separate review step is needed.
tasks/lessons.md by appending [universal] on the same line as the bold title; (b) append a one-liner to the global queue at ~/.claude/tasks/coordinator-improvement-queue.md:
- YYYY-MM-DD | <source-repo> | <source-file>:<line> | <one-line summary> | proposed target: <coordinator file>
Use the project repo name as <source-repo>, and tasks/lessons.md:<line-number> as <source-file>:<line>. If the same <source-file>:<line> already exists in the queue, skip — the queue is append-only and that pair is the dedup key.Find and update relevant plan/task documentation to reflect what was completed:
tasks/<feature>/todo.md — feature-scoped plans for current worktasks/plans/ — session handoff plans and tactical trackersdocs/plans/ — historical and reference plans~/.claude/plans/ — plans written in plan mode (may need copying to canonical location)tasks/todo.md, tasks/plan.md — legacy flat locations
If a plan exists for the work this session touched, read it and update it. Don't rely on having opened it earlier — sessions that start from handoffs or dive straight into code often never explicitly open the plan.Spec backlink:
archive/specs/2026-05-26-session-end-deviation-reconciliation-gate.md§ Goal, D1–D5.
Governing-plan predicate — fires only when a governing plan/spec exists for this session's work. When this session implemented work governed by a spec, plan, or stub — docs/plans/YYYY-MM-DD-<feature>.md, an RFC, an enriched stub spec, or a handoff body that functions as a live spec — perform the reconciliation below. Negative-spec: if no governing plan exists for this session's work, skip this step entirely. Do NOT invent a plan to reconcile against. No ceremony tax on plan-less sessions: the proportionality is explicit, consistent with docs/wiki/ceremony-calibration.md — skip-on-no-plan is the right shape for organic fixes, doc-touch sessions, and ad-hoc work.
Plan documents contain sections that /distill crystallizes into wiki entries (the ALLOWLIST). When what shipped diverged from the plan's forecast, correct these sections in the plan doc before the Step 3 commit so distill synthesizes shipped reality, not the stale forecast:
SHIPPED: <what-shipped> (was: <plan-forecast>). For decisions that shipped unchanged, no annotation is needed.SHIPPED: <actual-signature> (was: <planned-signature>).ID | Criterion | Test | Binding-Class | Status) are consumed by check-acceptance-oracle.sh. In-place correction of an AC table is scoped to the Status/note columns only (e.g. Status → shipped-differently with a note cell). Free-text mutation of the Criterion or Test cells would corrupt the structured oracle the parser expects. Substantive "what shipped vs forecast" delta routes to the ## Deviations table (below) and to the Decisions Made section — NOT a free-text edit of Criterion/Test cells.The (was: <plan-forecast>) half of each annotation maps to distill's existing [SUPERSEDED] nugget class — it is recorded as superseded provenance, not crystallized as a live decision. What crystallizes into the wiki is the shipped reality.
## Deviations audit sectionAfter correcting ALLOWLIST sections, append the following section to the plan doc (or extend it if already present from an earlier partial session):
## Deviations
| deviation | reason | commit |
|-----------|--------|--------|
| <brief description of what diverged from the plan> | <why it diverged> | <commit sha or "pending"> |
One row per meaningful deviation. This section is provenance-only — it is not crystallized by /distill (it is dropped as [EPHEMERAL]). The essential deviation fact survives in the corrected ALLOWLIST sections' (was: …) annotations; the verbose reasons live here and in git history.
Step 2.9's spec-completion lens (when dispatched) is an input to this step's write-back, not a hard predecessor. Step 2.9 may run concurrently with the todo-list cluster; the dependency is soft: Step 2.4 reconciles what the EM already knows from session context. If Step 2.9 later surfaces additional substrate drift or dropped deliverables that weren't caught here, the EM folds those findings into the plan doc before Step 3 stages the union. Distinguish the two write-back types: Step 2.4 performs forecast→reality correction write-backs (the reconciliation this step introduces); Step 2.9's review-integrator path performs defect-fix write-backs (correcting plan-doc errors the reviewer identifies). Both are complementary and both fan into Step 3's staging union.
End of session is the last chance to ensure status fields match reality. This catches work that completed but whose status wasn't updated — common after compaction or rapid context shifts.
**Status:** fields in docs/active/, docs/plans/, or similar), verify their status reflects what actually happened:
docs/plans/consolidated-execution-tracker.md), verify that chunks worked on this session have accurate status entriesSweep the session's commits for completed work that isn't already in the project tracker (docs/project-tracker.md) or the per-entry completion archive under archive/completed/. This catches bug fixes, ad-hoc requests, and quick tasks that bypassed the spec pipeline.
Skip if no archive/ directory exists and no docs/project-tracker.md exists — the project hasn't adopted unified tracking yet.
git log --oneline for commits since the session started (or since the last /session-end//update-docs). For each substantive commit (skip merge commits, doc-only commits, quick-saves), check if the work is already represented in either the tracker or an existing per-entry file under archive/completed/YYYY-MM/. Check by commit SHA — if the hash already appears in any file under that directory, skip that entry. The archive records what shipped, not every keystroke; group related commits into a single entry.
Before writing any per-entry file, check whether a legacy monolith file exists at archive/completed/YYYY-MM.md (i.e., directly at the root of archive/completed/, NOT under a YYYY-MM/ subdirectory). If found AND COORDINATOR_OVERRIDE_LEGACY_MONOLITH is not set to 1:
git mv archive/completed/YYYY-MM.md archive/completed/legacy/YYYY-MM.md
Create archive/completed/legacy/ if it does not exist. The git mv is idempotent — subsequent runs find no monolith-at-root and skip silently. If COORDINATOR_OVERRIDE_LEGACY_MONOLITH=1, skip the git mv (the EM has already handled migration manually).
(a) If a plan was touched this session (any file under docs/plans/ or tasks/*/todo.md), chain = that plan's filename stem (e.g., 2026-05-19-completion-log-phase1).
(b) Else if a handoff was picked up this session, chain = the handoff's filename stem.
(c) Else if a workstream slug appears in any handoff frontmatter consumed this session, chain = that slug.
(d) Else chain = null (omit from filename; write as archive/completed/YYYY-MM/YYYY-MM-DD-adhoc-<sid6>.md).
Nature is classified automatically — no interactive prompt. Dispatch a small Sonnet sub-call (~1 KB output) with:
git diff --name-only for this session's commits)git log --oneline for this session)Sonnet classifies to one of [roadmap | bugfix | tech-debt | infra] and returns a nature: value + one-sentence rationale. Tag the entry nature_inferred: true.
Interactive override: If COMPLETION_NATURE is set in the environment before invoking /session-end, use that value as nature: directly and write nature_inferred: false. The env var bypasses the Sonnet dispatch entirely.
Why AUTO-INFER not interactive-prompt: session-end fires in autonomous execution chains where no human EM is present to answer. A default-skip mechanism would systematically produce un-tagged entries in autonomous sessions and tagged ones in interactive sessions — a sampling bias that corrupts --where nature=<x> queries that Phase 3 consumers depend on. See plan § Chunk 3 for full rationale.
$em_sid and derive <sid6>$em_sid sourcing (env-var-primary):
$em_sid is set in the environment, use it directly.$CLAUDE_CODE_SESSION_ID — the platform-injected session id (Claude Code ≥ ~2.1.150). Per-session and unclobberable by a sibling session, so it is authoritative..git/coordinator-sessions/.current-session-id (last-writer-wins sentinel — session-init.sh writes it on every SessionStart; only a fallback for old Claude Code, per docs/wiki/claude-code-platform-gotchas.md). If the sentinel read is ambiguous (flips between ids across reads), two sessions are live — do not trust it; the env var in step 2 is the answer.meta.json-based lookup — it is circular (you need $em_sid to find the directory containing meta.json).<sid6> = last 6 characters of the resolved $em_sid. If $em_sid cannot be resolved, generate a 6-char hex fallback from the current timestamp (date +%s | tail -c 7 | head -c 6).
The <sid6> suffix makes the filename deterministically unique per EM session — no existence-check race condition. Two concurrent session-ends in the same chain on the same day produce two distinct files, not a collision.
Behavioral rule (tripwire): Session-end MUST invoke
coordinator-session-loe.sh(oraggregate-chain-loe.shfor chain-terminal) to write per-session LoE into the completion entry. Skipping this step produces an incomplete entry that Phase 3 consumers and workweek-complete cannot query. No override mechanism; theloe:block is always written.
Determine whether this is a single-session or chain-terminal session using the same detection logic as Step 2.9 chain-end detection:
/pickup (no predecessor handoff consumed)./pickup AND is ending via /session-end (not /handoff or /spinoff).Single-session path:
loe_block=$(~/.claude/plugins/coordinator/bin/coordinator-session-loe.sh \
--format yaml-frontmatter 2>/dev/null)
If the script is absent or returns non-zero, degrade gracefully: set loe_block to:
loe:
agent_dispatches: null
opus_dispatches: null
em_tokens: null
tshirt: null
Chain-terminal path:
Resolve the consumed predecessor handoff path (the handoff archived by Step 2.7 this session). Resolution order:
/pickup time.tasks/handoffs/archive/<YYYY-MM>/ for entries with consumed_by: <this session_id>.Then invoke the chain aggregator:
loe_block=$(~/.claude/plugins/coordinator/bin/aggregate-chain-loe.sh \
--terminal-handoff "<resolved-predecessor-path>" \
--format yaml-frontmatter 2>/dev/null)
The chain aggregator walks the predecessor: chain backward from the consumed handoff, reads ## Session Ledger blocks from each handoff (including archived predecessors at tasks/handoffs/archive/**/), sums LoE across all sessions, and recomputes t-shirt size against the shared threshold table. If the script is absent or returns non-zero, degrade the same way as the single-session path.
The resolved $loe_block is embedded into the completion entry frontmatter in Step 2.6.6.
For each untracked completed work item (or one entry covering the session's full scope if work is cohesive), write a single Markdown file at:
archive/completed/YYYY-MM/YYYY-MM-DD-<chain-slug>-<sid6>.md
(where YYYY-MM-DD is today's date; if chain is null, use YYYY-MM-DD-adhoc-<sid6>.md).
Create the archive/completed/YYYY-MM/ directory if it does not exist.
File shape:
---
title: "<Concise past-tense one-line description>"
created: YYYY-MM-DD
nature: <roadmap|bugfix|tech-debt|infra>
nature_inferred: <true|false>
chain: <chain-slug or null>
commits:
- <sha1>
- <sha2>
status: pending-release
chain_terminal: <true|false>
authored_by: <em_sid or null>
loe:
agent_dispatches: <N or null>
opus_dispatches: <N or null>
em_tokens: <N or null>
tshirt: <XS|S|M|L|XL|null>
# chain-terminal only — omit for single-session entries:
# chain_sessions: <N>
# chain_span_days: <N>
# chain_starting_handoff: <path>
---
<One paragraph prose summary: what shipped, key decisions, anything notable.>
Frontmatter field semantics:
nature_inferred: true when AUTO-INFER Sonnet path was used; false when COMPLETION_NATURE env var was set.chain_terminal: true when this session is chain-terminal (opened via /pickup, ending via /session-end); false for single-session work. Phase 1 defaulted this to true; Phase 2 sets it correctly per detection.authored_by: is $em_sid if resolvable; omit field (or write null) if not.status: pending-release is the initial state for all completion log entries.loe: block is populated from Step 2.6.5a. For chain-terminal sessions, the block contains aggregate values across the full predecessor chain plus the additional chain-summary fields (chain_sessions, chain_span_days, chain_starting_handoff) that aggregate-chain-loe.sh emits. For single-session entries, only the four base fields are present.loe.tshirt null means LoE computation was unavailable (script absent or errored) — entry is still valid, just unranked.No separate collision handling needed. The <sid6> suffix makes the filename unique by construction (Step 2.6.5).
Not every commit is a work item. Group related commits into a single archive entry. Skip trivial commits (typo fixes, formatting). If a session produced no substantive commits beyond doc/lesson housekeeping, no archive entry is needed — skip silently.
actionedWhen the session's work resolves a request from a cross-repo memo sitting in this repo's cross-repo/inbox/, the receiver-side lifecycle requires flipping status: open → actioned in the memo file (with an optional decision: line) so the inbox accurately reflects channel state. A receiver that ships the requested fix without flipping the status leaves the memo looking unresolved — a silent drift between "what got done" and "what the inbox says is still pending."
Detection — non-automatable; prompt the EM. No reliable programmatic signal connects a session's commits to a memo's resolution (memo topics need not appear in commit messages; the resolution may span multiple commits or non-commit work). Treat this as a session-end checklist surface:
cross-repo/inbox/*.md in the current repo. Parse YAML frontmatter; filter to status: open (or absent → treat as open).N. <basename> — <title or first heading> (from: <from>, topic: <topic>). Then ask once, plain prose: "Any of these resolved this session? If yes, give me the numbers; I'll flip status: actioned and add a one-line decision: you dictate. (Type none if none.)"status: actioned and append decision: <PM-supplied line> to the frontmatter. Then sweep it out of the inbox: git mv cross-repo/inbox/<file> cross-repo/archive/<file>. The /workday-start inbox scan filters by status ∈ {open, reviewed}, so an actioned memo won't re-surface — but the inbox is the active channel and actioned memos clutter it; the archive is their terminal resting place. Keeping the move coupled to the status flip also makes the channel robust to any future scanner that filters by location rather than status. Both the in-place edit and the move fold into Step 3's commit (no separate commit; this is housekeeping, not a workstream event). Create cross-repo/archive/ if it does not exist.Out of scope here: the sender side (nothing to update — the sender keeps no copy per the single-delivery-copy model); memos that the session created or moved (Chunks A/F-style migration work has its own commit story); memos in cross-repo/archive/ (already closed). Do NOT touch any other repo's cross-repo/.
Why session-end specifically: the receiver-side state flip is small enough that asking inline at the moment of resolution would be ceremony; batching at session-end (when the EM is already reviewing the session's scope) is the cheaper cadence. The PM's framing is the trigger: "if one was resolved in the chain."
Counterpart to Step 2.65, sender-side. If this session (or any earlier session in the chain) ran cross-repo-memo or made a doctrine-seeding direct write into a sibling repo, do NOT list those memos / seeds as "pending PM-relay" or "pending your action" in the Final Summary, in any Flag to PM: block, or in a follow-on /handoff body. The PM was handed the receiver path once at send time — that is the relay. The receiving repo's /workday-start Step 1.45 inbox surfacing is the canonical channel; staleness is already flagged there.
Why: sender-side knowledge of whether the receiving EM has actioned a memo decays fast and silently. Re-surfacing a memo the receiver may already have closed (and just not yet had a session you've seen) wastes PM attention on a hand-rolled status board that the inbox maintains authoritatively. Trust the channel.
Concretely banned phrasings in /session-end output:
The only legitimate sender-side mention is at the moment the CLI runs (Receiver-side path + relay reminder in that turn's output). After that turn, the memo is the receiver's surface, not yours.
→ docs/wiki/cross-repo-communication.md § Don't re-nag the PM about already-sent memos.
When this session was opened with /pickup, the consumed handoff still lives in tasks/handoffs/ (mutation-only at pickup time). If this session is ending via /session-end rather than /handoff, archive the predecessor now.
Detection: scan tasks/handoffs/*.md. For each file, read its frontmatter consumed_by: field.
$CLAUDE_CODE_SESSION_ID env var first (platform-injected, unclobberable); .git/coordinator-sessions/.current-session-id sentinel fallback only when the env var is empty.Action: git mv tasks/handoffs/<file> archive/handoffs/<file>. Create archive/handoffs/ if it does not exist. On git mv failure (file already moved by a concurrent /handoff chain-archival), log to stderr and continue — idempotent treatment of already-moved files.
The move folds into the existing session-end commit at Step 3 — no separate commit for this step.
No claim release call needed here. cs_archive at Step 3.5 carries the entire session directory (including handoff-claims/) into .archive/. The claim is released structurally.
Skip entirely if this session is exiting via /handoff — /handoff chain-archival owns that path. /session-end and /handoff are mutually exclusive session-exit paths.
After archiving the predecessor handoff (Step 2.7), regenerate tasks/handoff-tracker.md so it reflects the post-archive state before the commit lands. This keeps the durable tracker current at every session exit.
node plugins/coordinator/bin/render-handoff-tracker.js
If the script is absent or exits non-zero, skip silently — the tracker is a convenience artifact, not a gate. The generated file (tasks/handoff-tracker.md) is staged with the session's existing scoped commit at Step 3 — no separate commit.
Update the documents that future sessions read for orientation — closing the read-write loop with /session-start and /workday-start.
Orientation cache (tasks/orientation_cache.md): Do not author the cache body. Do not patch sections. Do not re-derive content section-by-section. The cache schema (pipelines/workday-start-internals.md § 5.5) is owned by ceremony writers (/workday-start, /update-docs). /session-end is a mid-session writer with a single, narrowly-scoped capability: pinboard append.
Pinboard rule (the only cache mutation permitted here): if this session surfaced something the next session boot MUST see, and it would otherwise be lost (a transient surface gotcha; a critical blocker context; an environment-specific caveat that fooled this session and will fool the next), write exactly one line to ## Pinboard via the routine:
bash plugins/coordinator/bin/regenerate-orientation-cache.sh \
--invoker session-end \
--pinboard "YYYY-MM-DD <writer-slug>: <one-line note>"
The pinboard is a one-slot escape valve. A second mid-session write overwrites; it does not append. The pinboard is cleared at every ceremony regen (/workday-start, /update-docs). If you find yourself wanting to write more than one line, that's not a cache edit — it's a wiki edit, a handoff body, or a lessons.md entry. Escalate to PM.
If you have nothing pinboard-worthy, do nothing. Counters, workstreams, branch state, doc-freshness, "recent work" prose — none of this is a mid-session concern. The ceremony writers regenerate all of it from disk on the next boot.
If the cache file doesn't exist, skip — the project hasn't run /workday-start yet. Do not claim the cache is absent based on intuition — ls tasks/orientation_cache.md before asserting absence.
Project tracker (docs/project-tracker.md): If it exists and this session completed or progressed tracked items, update their status rows. Only touch rows this session affected — don't re-derive the whole tracker.
Action items (first match: ACTION-ITEMS.md, docs/active/ACTION-ITEMS.md, docs/ACTION-ITEMS.md): If one exists and this session resolved any listed items, check them off or remove them per the file's existing conventions.
Documentation index (docs/README.md): If it exists and this session created new guides, added research files, or completed plan documents, patch the relevant table. Only touch rows this session affected.
Concurrency note: These are targeted patches to specific rows/sections based on this session's work — safe with concurrent agents, as long as agents work on different items (which they should by design).
Assess whether this session's diff warrants a code review pass before committing. EM makes the call using the table below — this step is judgment, not ceremony.
Diff-shape table:
| Session shape | Default scale |
|---|---|
| Doc-only edits, lesson capture, no executor dispatched, no code touched | None |
| Single-file fix <50 LOC, no shared schema touched, no executor | None (but commit message names the change) |
| Any executor dispatched, OR >50 LOC code change, OR shared schema/seam touched | code-reviewer (Sonnet, locked — see agents/code-reviewer.md) |
| Chain-end (started with /pickup, ending without /handoff//spinoff) AND chain diff is non-trivial | code-reviewer on chain diff |
| Chain-end AND chain diff too large for a single reviewer (>500 LOC rough anchor, ≥3 segments, or multi-surface) | Partitioned code-reviewer dispatches — see § Partitioning large surfaces. Named reviewers (the Staff Engineer, personas) are for plans and architecture, not code output. Sonnet code-reviewer is the ceiling at session-end |
Precedence rule: chain-end rows (4, 5) override session-end rows (1, 2, 3) when both apply — the chain diff is the integration-risk artifact.
Anchored-ranges note: the numeric anchors (50 LOC, 500 LOC, ≥3 segments) are decision anchors, not hard thresholds. An EM seeing a 51-LOC change with a clean shape should not feel obliged to escalate; an EM seeing a 49-LOC change touching a public schema seam should not feel released from review.
Partitioning large surfaces across multiple code-reviewer dispatches (row 5):
One Sonnet reviewer over a chain diff that spans a new package + cross-repo edits + a shared-schema change will skim — the surface exceeds what a single dispatch can carry with depth. When the row-5 diff is genuinely too big for one reviewer, the EM SHOULD fan out into multiple parallel code-reviewer dispatches, each over a coherent slice. Partitioning shapes (pick what matches the diff):
src/foo/**, src/bar/**, tests/** as separate slices, with the EM owning the union.This is diff partitioning under capacity limits, not the workweek parallel-orthogonal-lenses pattern — every dispatch is the same agent (code-reviewer) with a narrower scope. It does NOT require the lens-orthogonality manifest or the no-rewrite synthesizer from coordinator:parallel-code-review; it shares only the frozen-diff property from § Review Sequencing's parallel carve-out. Mechanics:
code-reviewer prompt names its slice explicitly (paths or commit subset) and an "out of scope: the rest of the chain diff" line so reviewers don't drift.Agent tool uses).coordinator:review-integrator per slice OR collates findings and dispatches one integrator over the union — EM's call based on overlap.--reviewer code-reviewer (single value); record the partition shape in the wrap-up sentence, not the trail field.code-reviewer the Staff Engineer-escalation criteria below apply to the combined finding set (sum across slices), not each slice independently.There is no upper bound on partition count — if the diff genuinely requires 6 or 8 code-reviewer dispatches, dispatch 6 or 8. The constraint is per-reviewer context fit, not total slice count. The workweek merge-gate ceremony (coordinator:parallel-code-review) is orthogonal: it runs N code-semantics chunk reviewers + 3 mechanical workers at merge time regardless of how many session-end partitions ran.
No named-reviewer escalation from code review. Named reviewers (the Staff Engineer, personas) are for plans and architecture. Code output review at session-end is Sonnet code-reviewer only — partition across slices as needed. If code-reviewer surfaces an architectural finding, capture it in tasks/lessons.md and surface to PM for a plan-shaped decision; do not escalate to a named reviewer within the code-review path.
The weekly /workweek-complete Step 7 parallel-code-review is the merge-gate ceremony — N code-semantics chunk reviewers (Sonnet code-reviewer-weekly) + 3 mechanical workers (security, deps, test-evidence) → no-rewrite synthesizer; the Staff Engineer runs a separate advisory architecture pass at Step 7.5, not the gate. It is a merge-gate, NOT a deferral path — do not skip session-end review and "surface to PM for workweek." Session-end review happens at session-end; the merge gate is a separate, independent ceremony.
Anti-ceremony-bias tripwire (code-reviewer-skip direction — still load-bearing):
"If you're considering skipping
code-reviewerbecause the diff feels small or 'we already reviewed the plan' — run it. Plan-time and post-implementation review catch different defect classes; the marker trail recordsverdict=okin seconds when there's nothing to find.code-revieweris the floor on row-3+ sessions, not a negotiable add-on."
Symmetric anti-ceremony tripwire (row 3+ — code-reviewer floor):
"Plan-time review and post-implementation review catch different defect classes — complementary, not substitutional. Mechanical executor gates (grep/pytest/
bash -n) are correctness floors, not review lenses. 'We've done a lot of review already' is the shape wrap-up pressure takes at session-end. If you're drafting a waiving-with-rationale sentence on a row-3+ session to skipcode-reviewer, the rationale is the tell. EM keeps waive authority on genuinely shallow row-3 diffs; the test is the diff shape, not the row number. Seedocs/wiki/session-end-review.md§ why-post-implementation-review-is-not-redundant for the worked example."
Chain-end detection:
$CLAUDE_CODE_SESSION_ID env var first (platform-injected, unclobberable); .git/coordinator-sessions/.current-session-id sentinel fallback only when the env var is empty./pickup AND ending without /handoff or /spinoff invocation this session.list-review-trail-records.sh (live AND archived) — chain history regularly reaches back across the weekly archival boundary, and a live-only glob systematically misses reviews from prior weeks. A "the Staff Engineer reviewed the plan" note in a predecessor handoff body is plan-level design-intent coverage — it does NOT satisfy the chain-end code-reviewer floor. Plan-level the Staff Engineer reviews (docs/plans/*.review-patrik.md) are not trail records and count as zero code-output coverage. If no trail record exists covering the chain diff's sha-range, the chain diff is unreviewed regardless of what the handoff narrative says.Diff scope:
git log $(git merge-base origin/main HEAD)..HEADgit log $LAST_REVIEW_SHA..HEAD (where $LAST_REVIEW_SHA is derived as: take the most recent trail record across both live and archive via list-review-trail-records.sh | tail -1, read its sha_range head, then apply an ancestry filter — git merge-base --is-ancestor <sha_range_head> HEAD must succeed before assigning to $LAST_REVIEW_SHA. Iterate from most-recent to oldest until one passes. If no archive-aware record passes the ancestry filter, fall back to session-start SHA. This filter prevents a sibling workstream's trail record on an unrelated branch from producing a wrong diff scope.)Dispatch: invoke coordinator:review-code Branch A.2 with the resolved diff scope.
Spec cross-reference (loop closure) — include in dispatch brief when a spec exists:
When this session (or the chain, for chain-end) implemented work governed by a spec, plan, or stub — docs/plans/YYYY-MM-DD-<feature>.md, an RFC, an enriched stub spec, or a handoff body that functions as a live spec — name the spec path in the code-reviewer dispatch brief and instruct it to apply the Spec completion lens (per agents/code-reviewer.md § Spec completion lens). The reviewer reads the spec before the diff and reports on scope completeness, shape adherence, substrate drift, acceptance-criteria coverage, and hedge-shaped deferrals.
This is loop closure, not redundancy: EM dispatch-time scope vetting and TDD cover what the author thought to verify; the reviewer's spec lens is the independent pass that catches silently-dropped deliverables, scope creep marketed as "while I was there", and acceptance criteria where the tests drifted to test the easy thing. Apply on row 3, 4, 5 sessions whenever a spec exists; omit on row 1, 2 (no spec involved by definition). If multiple specs apply (chain of stubs, plan + amendment), name all of them; the reviewer will treat the union as the completion oracle. If partitioning the diff across multiple code-reviewer dispatches (per § Partitioning large surfaces), name the spec slice each reviewer is responsible for — overlap is fine, gaps are findings the EM will own.
Negative-spec: if no spec governs this session (small organic fix, opportunistic refactor, doc-touch session), omit the spec section from the brief — do not invent a spec to satisfy the rule, and do not point at a stale spec. The reviewer's agent prompt is clear that no spec named ⇒ skip the lens entirely.
Findings disposition — fix everything, including nitpicks:
"If a finding is worth surfacing, it is worth fixing now. The diff is fresh, the EM has context, and the cost to fix at session-end is a fraction of what it costs three weeks later in a debugging session. A reviewer verdict of
OKwith three 'below blocking threshold' observations is NOT a license to commit and move on — those observations are the review output, and they get fixed in this session before commit. This applies symmetrically across severities: P0/P1/P2/nitpick/observation/note/'consider' — all fold in viacoordinator:review-integratorbefore the marker-trail write. The only legitimate skip path is a real tradeoff (cost/value, scope/polish, architectural direction) that escalates to PM per § Reviewer findings — apply, don't ratify incoordinator/CLAUDE.md. 'Recorded below blocking threshold' in the EM's wrap-up sentence is the tell that this rule was skipped. Re-open the diff, fold the findings, then write the marker."
After integration, the trail's --verdict field still records the reviewer's original verdict (ok / warn / blocked) — verdict tracks what the reviewer found on the pre-fix diff, not what shipped. Downstream load-shedding consumes the verdict; the trail is not a fix-completion log.
Marker write: after review integration completes, invoke:
~/.claude/plugins/coordinator/bin/coordinator-write-review-trail.sh \
--sha-range <A..B> --reviewer <code-reviewer|patrik|code-reviewer+patrik|waived|ubt-compile> \
--scope <chain|session> --verdict <ok|warn|blocked|waived|pending> --diff-loc <N>
Negative-spec:
--reviewer waived --verdict waived. Greppable as verdict=waived.Staging discipline:
"Any files edited by
coordinator:review-integratorduring this step must be staged via explicit path in Step 3, not absorbed by a post-integrationgit add -A. This preserves the existing concurrent-EM safety property of Step 3."
UBT pending-marker (UE plugin work only): If bin/check-ubt-build-fresh.sh exists in the cwd, invoke it in --mode pending. Captures the build verdict as a deferred record; resolution happens at /workday-complete Step 0c. This step is a no-op for non-UE repos (script absent) — the [ -x bin/<name>.sh ] pattern is the canonical convention for conditional UE-specific steps in coordinator skill bodies; future UE conditionals (clippy, etc.) follow this shape.
[ -x bin/check-ubt-build-fresh.sh ] && \
bin/check-ubt-build-fresh.sh --since "$(git merge-base origin/main HEAD 2>/dev/null || git rev-parse HEAD~1)" --mode pending
Fires when this session (or the chain, for chain-end) is a big workstream — same trigger surface as Step 2.9 rows 3/4/5 (any executor dispatched, OR >50 LOC, OR shared schema/seam touched, OR chain-end with non-trivial chain diff). On row-1/row-2 trivial sessions, skip silently — no ceremony tax on doc-touch or small-fix sessions.
Step 2.9 is the line-level code-review lens. The aftermath lenses are the cross-cutting lenses a large workstream is most likely to have left loose. Run each as a quick self-check (not a reviewer dispatch unless a lens surfaces something needing depth); each maps to an existing doctrine surface so the check is "did this session honor X," not new judgment:
machine-local/, installer scripts, sentinel files, registry entries, env config)? If yes, confirm the clean-install path reproduces it — local green tests are not evidence a fresh machine installs the work. → global CLAUDE.md § Install-surface completeness; docs/wiki/install-surface-completeness.md.docs/README.md, docs/wiki/, and any Atlas/narrative index. Confirm stale references are repointed (not duplicated). → CLAUDE.md § Documentation and Knowledge System.security-audit-worker / dep-cve-auditor reviewer-routed workers per CLAUDE.md § Reviewer-Routed Workers (fuller routing protocol: docs/wiki/reviewer-routed-workers.md) — do not self-assess novel auth/secret surfaces.Output shape: a short checklist in the Step 4 Final Summary — one line per lens, clear / <one-line finding + disposition>. Findings that are tradeoff-free corrections fold in this session (per CLAUDE.md § Reviewer findings — apply, don't ratify); real tradeoffs surface to PM. The lenses are an offer-shaped self-audit, not a blocking gate — but on a big workstream, "all clear" must be an affirmative statement, not a silent skip.
Pre-terminate dirty-tree gate (fail loud on unattributable files). Before the session-end commit, run git status --porcelain and classify every dirty (modified / untracked / partially-staged) path:
scope: block, an active handoff, or a session claim under .git/coordinator-sessions/ accounts for it. The machine-checkable form of "a session claim accounts for it" is a handoff-frontmatter consumed_by: field naming another session's id (sourced from .git/coordinator-sessions/.current-session-id per schemas/handoff.yaml) — grep for that to tie the prose signal to a field an executor can actually check.git add -- <path> && git commit -m "chore: adopt orphaned WT change <path> — unattributed at session-end".git stash push -u -m "orphaned-WT <YYYY-MM-DD> session-end: <path> — left by unknown session" -- <path>. Name the stash so the next session can find and adjudicate it (per CLAUDE.md "Probe edits in git stash push -u / pop").The forbidden outcome is terminating with case-(c) files still dirty and unnamed. Orphan .tmp.<pid>.<nanos> files are a special case (Edit-tool atomic-write crash, per CLAUDE.md § Verifying Executor Output) — diff against target before deleting; do not stash them blind.
Note — this dirty-tree gate is replicated across all three session terminators (session-end, handoff, workday-complete) because the failure is identical across them. Three surfaces, same gate, inline-not-snippet: the three blocks legitimately vary by <terminating action> / <terminator> token (session-end commit vs. handoff commit vs. workday-complete merge/rebase). Snippet-sync is for byte-identical text that must not drift; near-identical-with-intentional-variation is the correct shape here. This is the instance-#3 ceremony moment the ceremony-calibration.md rule names — three terminator surfaces in one plan IS the threshold, and the conscious choice is inline-over-snippet because parameterizing the per-surface variation into a single snippet would make that variation invisible. Trigger for revisiting: a fourth terminator surface appears, OR the three blocks converge to byte-identical.
git add -A. With concurrent EMs active on the same branch, git add -A sweeps up another session's staged/modified files and silently re-attributes them. Instead:
tasks/lessons.md, docs/plans/<feature>.md if Step 2.4 reconciliation ran, archive/completed/YYYY-MM/<entry>.md, archive/completed/legacy/YYYY-MM.md if AUTO-MIGRATE ran, docs/project-tracker.md, action-items file, docs/README.md, tasks/handoff-tracker.md if Step 2.75 ran).git add <path1> <path2> ... — name each path explicitly.git status shows unfamiliar unstaged files you didn't touch, leave them alone — but first run the Step 3.0 dirty-tree gate; "leave alone" is correct ONLY for case (b) named-owner files."session-end quick-save". (The post-commit hook will auto-push on work/feature branches.)git log "origin/$(~/.claude/plugins/coordinator/bin/coordinator-current-branch)..HEAD" 2>/dev/nullgit push origin mainNow that the final commit has landed and pushed, archive this session's claim directory so concurrent sessions don't see stale claims accumulating until the 24h reaper fires. /session-end is one of two session-exit pathways (the other being /handoff); both must clean up claims, otherwise sessions that wind down via /session-end leak claims that force the next concurrent EM into a 24h wait, COORDINATOR_OVERRIDE_SCOPE=1, or hand-archival.
Run:
sid="${CLAUDE_CODE_SESSION_ID:-$(cat "$(git rev-parse --show-toplevel)/.git/coordinator-sessions/.current-session-id" 2>/dev/null)}" && \
source ~/.claude/plugins/coordinator/lib/coordinator-session.sh 2>/dev/null && \
cs_archive "$sid" 2>/dev/null || true
Idempotent — already-archived sessions return 0 silently (verified: a session archived by /handoff and re-archived here is a no-op). Failures are non-fatal (the 24h reaper is the safety net). Skip silently if the session id can't be resolved or the lib is unavailable.
Note on session_id source: Prefer the platform env var $CLAUDE_CODE_SESSION_ID (Claude Code ≥ ~2.1.150) — per-session and unclobberable, so it is the session that actually owns this exit. The .current-session-id sentinel is "last writer wins" across concurrent sessions and is only a fallback for older Claude Code; an ambiguous sentinel read (flips between ids) means two sessions are live — use the env var, don't act on the sentinel.
If this session executed an oracle-bearing plan (one that went through coordinator:review and carries a bindable ## Acceptance Criteria table with gate-bound rows) and any gate-bound ACs remain red or unrun, emit an offer-shaped notice before the final summary:
"You have unresolved acceptance tests in
<plan-path>: <count> red/unrun gate-bound AC(s). Runbash check-acceptance-oracle.sh <plan-path>before merging via/merging-to-main."
Never a hard block. /session-end is not a merge surface — teeth live at coordinator:merging-to-main Step 0. This is advisory only: design-as-offers, not friction. If no oracle-bearing plan was involved this session, or all gate-bound ACs are green, skip silently.
Present a brief end-of-session summary:
## Session Complete
**Work done:** [1-2 sentence summary]
**Lessons captured:** [N new / none]
**Work archived:** [N items written to archive/completed/YYYY-MM/<filename>.md / none needed / project not using unified tracking]
**Docs updated:** [list of updated files]
**Orientation refreshed:** [orientation cache patched / tracker updated / action items checked off / nothing to update / no orientation docs exist]
**Pushed to remote:** [yes — branch name / no — reason]
Flag to PM: Explicitly note the push so they can verify nothing breaks for other consumers.
If $ARGUMENTS is provided, use it as context for what was accomplished this session.
tools
Orient session — preflight, load context, choose work
documentation
Wrap up finished work — capture lessons, update docs
development
Triangulate plan-claim / code-reality / review oracles to classify each plan into DELIVERED+REVIEWED / DELIVERED-UNREVIEWED / PARTIAL / IN-FLIGHT / ABANDONED. Run after any crash or 'did we actually finish what we think we finished?' moment.
testing
Check for a published coordinator update and advise a preserve-by-default migration path — never a blind overwrite.