carto
FreeOpen the Session Cartographer Explorer web UI for visual browsing of Claude Code and Codex session history.
The English summary is being prepared. Showing the original description.
Strategic session-end preservation. Captures decisions, discoveries, and state that the automatic hooks miss.
The English summary is being prepared. Showing the original description.
See how to installReview the instructions the agent will receive before you install.
When a runtime script is needed, resolve ROOT from CARTOGRAPHER_ROOT,
CLAUDE_PLUGIN_ROOT, or PLUGIN_ROOT. If none is set, derive the plugin root
from this skill's reported base directory (../.. from skills/wrapup), with
the conventional checkout as a legacy fallback.
Deliberate end-of-session preservation. The hooks capture mechanical facts (files changed, commits made, session ended). This skill captures strategic context — the decisions, discoveries, and unfinished threads that make the next session productive.
Not every session needs an authored synthesis. Transcripts and lifecycle hooks
are the automatic substrate; /wrapup promotes a material session into durable
strategic memory. Use node "$ROOT/scripts/wrapup-coverage.js" to see material
sessions that still lack that synthesis.
Wrapup serves two audiences at once. The log gets a synthesis paragraph that /remember can recall months later. The human gets a digest panel they can verify at a glance — every line traces back to a logged event or to git, so a wrong claim is visible rather than merely plausible.
ROOT="${CARTOGRAPHER_ROOT:-${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-$HOME/Documents/dev/session-cartographer}}}"
node "$ROOT/scripts/session-digest.js"
The script resolves the session from CARTOGRAPHER_SESSION_ID / CLAUDE_CODE_SESSION_ID; pass --session <id> to override. It prints a panel covering span and tempo, commits with type and diff-shape mix, hottest files, research hosts, /remember served-vs-used, and the live dirty/unpushed state of every repo the session touched.
Show the panel to the user verbatim. Do not paraphrase it into prose or re-type its numbers into a bulleted list — the alignment is what makes it scannable, and re-typing is where the numbers drift.
Then read it before writing anything. This step exists so the synthesis is written against the log rather than from your own recollection of the conversation — recollection is exactly where confident, unfalsifiable summaries come from. If the digest disagrees with your memory of the session, the digest is right about what happened; your memory is only better on why.
If the digest exits non-zero (no events logged for this session), say so plainly and write the synthesis from the conversation, noting that it is unverified.
The digest already covers what happened mechanically — do not restate it. Your synthesis adds the layer no hook can infer:
Items 1 and 2 are written twice: woven into the prose paragraph, and again as
structured decisions[] / unresolved[] arrays in Step 1. That is not
redundancy. The prose is what /remember searches; the arrays are what
.carto/profile.md harvests into "Durable decisions". Before 0.5.1 only the
prose existed, so 508 syntheses contributed nothing to the profile and the
section drew all five of its entries from a single project on a single day.
Do not skip the arrays as a shortcut — nothing downstream can recover them from the paragraph. Measured across 508 descriptions, explicit decision markers appear in about 4%, so no later parser can reconstruct what you did not record.
unresolved[] is also the hand-off. The next session to start in this project
gets the latest wrapup's items in its SessionStart note
(hooks/surface-focus-on-start.sh), one per line and cut at 100 characters,
with this event's id so it can --get the rest. Write each item for that
reader, an agent at its first turn that has not seen this conversation:
An empty list says "nothing left open", so leave it empty only when that is true.
Generate a one-paragraph synthesis of the session. Be specific — name the files, the commits, the discoveries. No filler.
Then log it:
DEV="${CARTOGRAPHER_DEV_DIR:-$HOME/Documents/dev}"
CLAUDE_SID="${CLAUDE_SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-}}"
SESSION_ID="${CARTOGRAPHER_SESSION_ID:-${CLAUDE_SID:-${CODEX_SESSION_ID:-unknown}}}"
PROVIDER="${CARTOGRAPHER_PROVIDER:-unknown}"
[ "$PROVIDER" = "unknown" ] && [ -n "$CLAUDE_SID" ] && PROVIDER="claude"
# Fail loudly. A silent "unknown" is how 437 wrapups lost their transcripts.
[ "$SESSION_ID" = "unknown" ] && echo "warning: session id unresolved — this milestone will not link to a transcript" >&2
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EVENT_ID="evt-$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 12)"
# Detect project from cwd. Resolve a worktree to its PARENT repo: a session run
# in repo/.claude/worktrees/<name> whose project is the worktree basename orphans
# every record the moment that worktree is pruned, and agent tools create and
# discard worktrees routinely. cartographer-project.sh is the command-line face
# of cartographer_project() in hooks/common.sh — one definition, two consumers.
ROOT="${CARTOGRAPHER_ROOT:-${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-$HOME/Documents/dev/session-cartographer}}}"
GIT_REPO=$(git rev-parse --show-toplevel 2>/dev/null)
PROJECT=$(bash "$ROOT/scripts/cartographer-project.sh" 2>/dev/null) || PROJECT=""
if [ -z "$PROJECT" ]; then
PROJECT=$(basename "${GIT_REPO:-$(pwd)}")
echo "warning: cartographer-project.sh unavailable — project \"$PROJECT\" is cwd-derived and is wrong inside a worktree" >&2
fi
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "none")
# Find transcript path
TRANSCRIPT=$(find ~/.claude/projects -name "${SESSION_ID}.jsonl" 2>/dev/null | head -1)
if [ -z "$TRANSCRIPT" ] && [ "$SESSION_ID" != "unknown" ]; then
TRANSCRIPT=$(find ~/.codex/archived_sessions ~/.codex/sessions -name "*${SESSION_ID}*.jsonl" 2>/dev/null | head -1)
[ -n "$TRANSCRIPT" ] && PROVIDER="codex"
fi
ENCODED_PATH=$(echo "$TRANSCRIPT" | python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=''))" 2>/dev/null || echo "$TRANSCRIPT")
DEEPLINK=""
[ "$PROVIDER" = "claude" ] && DEEPLINK="claude-history://session/${ENCODED_PATH}"
# Attach the digest's scalars so the milestone stays checkable after the
# transcript hits Claude Code's ~30d TTL. Falls back to null if the digest
# could not run — never block the wrapup on it.
DIGEST=$(node "$ROOT/scripts/session-digest.js" --json --no-git 2>/dev/null \
| jq -c '{duration_minutes, event_count, projects, commit_types, commit_shapes, files_touched: (.files | length), recall}' 2>/dev/null)
[ -z "$DIGEST" ] && DIGEST=null
# Structured outcomes for the profile. One item per argument — jq -R turns each
# line into a JSON string and -s slurps them into an array, so commas, quotes,
# and apostrophes in your text are safe. Replace the placeholder lines; keep the
# select() so an empty list stays [] rather than [""].
DECISIONS=$(printf '%s\n' \
"DECISION_ONE" \
"DECISION_TWO" \
| jq -R . | jq -s 'map(select(length > 0))')
# Each open thread on its own line: the next session's start note lists them
# one per line, so lead with the action and keep each under ~100 characters.
UNRESOLVED=$(printf '%s\n' \
"OPEN_THREAD_ONE" \
| jq -R . | jq -s 'map(select(length > 0))')
KEY_INSIGHT="THE_ONE_THING_WORTH_REMEMBERING"
MILESTONE_EVENT=$(jq -n -c \
--argjson digest "$DIGEST" \
--argjson decisions "$DECISIONS" \
--argjson unresolved "$UNRESOLVED" \
--arg insight "$KEY_INSIGHT" \
--arg eid "$EVENT_ID" \
--arg ts "$TIMESTAMP" \
--arg milestone "session_wrapup" \
--arg description "SESSION_SYNTHESIS_HERE" \
--arg session "$SESSION_ID" \
--arg provider "$PROVIDER" \
--arg transcript "$TRANSCRIPT" \
--arg deeplink "$DEEPLINK" \
--arg project "$PROJECT" \
--arg cwd "$(pwd)" \
--arg event "Wrapup" \
--arg branch "$GIT_BRANCH" \
'{event_id: $eid, timestamp: $ts, milestone: $milestone, provider: $provider, description: $description, session_id: $session, transcript_path: $transcript, deeplink: $deeplink, project: $project, cwd: $cwd, event: $event, git_branch: $branch, digest: $digest,
decisions: $decisions, unresolved: $unresolved, key_insight: $insight}')
[ -z "$MILESTONE_EVENT" ] && { echo "error: milestone JSON not built — nothing written" >&2; exit 1; }
# The helper preserves the JSONL write and semantic-index result as separate
# states, verifies the exact Qdrant event id, and returns one JSON receipt. Never
# append here and then `tail -1`: concurrent sessions share the milestone log.
ROOT="${CARTOGRAPHER_ROOT:-${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-$HOME/Documents/dev/session-cartographer}}}"
WRAPUP_STATUS=0
WRAPUP_RECEIPT=$(printf '%s\n' "$MILESTONE_EVENT" \
| bash "$ROOT/scripts/record-wrapup.sh") || WRAPUP_STATUS=$?
LOG_OUTCOME=$(printf '%s' "$WRAPUP_RECEIPT" | jq -r '.log_outcome // "unknown"')
INDEX_OUTCOME=$(printf '%s' "$WRAPUP_RECEIPT" | jq -r '.index.outcome // "unknown"')
INDEX_STAGE=$(printf '%s' "$WRAPUP_RECEIPT" | jq -r '.index.stage // "unknown"')
case "$LOG_OUTCOME:$INDEX_OUTCOME" in
written:indexed|already_present:indexed)
echo "logged + indexed + verified: $EVENT_ID"
;;
written:*|already_present:*)
echo "logged, not indexed: $EVENT_ID ($INDEX_OUTCOME at $INDEX_STAGE)" >&2
;;
*)
echo "wrapup not logged: $EVENT_ID ($LOG_OUTCOME; $INDEX_STAGE)" >&2
;;
esac
# Preserve this for Step 2 and for an exact diagnostic if anything failed.
printf '%s\n' "$WRAPUP_RECEIPT" | jq .
[ "$WRAPUP_STATUS" -eq 0 ] || echo "index/write status: $WRAPUP_STATUS" >&2
Replace SESSION_SYNTHESIS_HERE with your synthesis paragraph. It must be a single line — the pipeline is TSV/line-based and a literal newline splits the row. record-wrapup.sh flattens any newline or tab left in the synthesis, key_insight or an array item to a space before writing, so a stray one costs formatting, not the row.
INDEX_OUTCOME=$(printf '%s' "$WRAPUP_RECEIPT" | jq -r '.index.outcome // "unknown"')
if [ "$INDEX_OUTCOME" = "indexed" ]; then
POINT_ID=$(printf '%s' "$WRAPUP_RECEIPT" | jq -r '.index.point_id // empty')
QDRANT_URL="${CARTOGRAPHER_QDRANT_URL:-http://localhost:6333}"
COLLECTION="${CARTOGRAPHER_COLLECTION:-session-cartographer}"
# qdrant-curl.sh adds the api-key header when a Qdrant key is configured.
VERIFIED_EVENT_ID=$("$ROOT/scripts/qdrant-curl.sh" -sf "$QDRANT_URL/collections/$COLLECTION/points/$POINT_ID" \
| jq -r '.result.payload.event_id // empty')
if [ "$VERIFIED_EVENT_ID" = "$EVENT_ID" ]; then
echo "verified indexed: $EVENT_ID"
else
echo "verification failed: expected $EVENT_ID, got ${VERIFIED_EVENT_ID:-nothing}" >&2
fi
else
printf '%s' "$WRAPUP_RECEIPT" \
| jq -r '"not indexed: \(.index.outcome) at \(.index.stage)"' >&2
fi
Verification is always by the exact event_id; Qdrant payloads do not need a
milestone field. A failed semantic index never removes the JSONL synthesis.
Check $DEV/.carto/index-errors.jsonl for precondition and service failures or
$DEV/.carto/index-rejects.jsonl for ordinary non-wrapup novelty rejections.
If the session produced a non-obvious discovery, preference, or decision that future sessions need — save it to memory. Most sessions don't warrant a new memory. Don't force it.
What the digest looks like (Step 0 output — show it as-is):
━━ session digest · widget-api ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
session dbc9ac21 · claude · 1242 events
span 2026-05-01 02:36 → 2026-05-05 15:23 UTC · 108h47m
tempo ▂·▁▁▁▁▁▁··█▄▂▅▄·▁▅▄▁▃▄·▃▂▂▁▁▁·▁▆ (3h24m/mark)
projects widget-api 1054 · widget-sdk 77 · docs-site 12
activity 580 edits · 608 bash · 4 searches · 6 compactions · subagents Plan
commits 26 · 15 pushes
15 feature · 8 docs · 2 fix · 1 refactor ▸ 17 construct · 6 surg…
05-05 13:23 d6be69e feat(auth): token refresh with retry bud… +4724 −139
05-03 17:55 7de5c98 docs(api): pagination cursor semantics +12 −1
… 20 more
files 173 touched
src/auth/refresh.ts ×41
recall 3 calls → 22 served · 2 marked used (9%)
evt-7apd8osl9sud evt-wprsakq31ca5
leaving widget-sdk@feat/retry-budget 1 uncommitted
docs-site@main 14 uncommitted
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Good synthesis:
"Trimmed root CLAUDE.md from 20KB to 4KB by moving project map, testing, and library details to per-project CLAUDE.md files. Created CLAUDE.md for widget-api, widget-sdk, and docs-site. Pruned 4 stale memories. Key insight: /focus and /remember make the project map redundant in root context — saves ~4000 tokens per turn."
Bad synthesis:
"Worked on various improvements to the codebase. Made things more efficient. Updated some files."
No reviews yet. Tell others what you thought.
No login needed. Share how it worked for you, ask a question, or note where it did not run.
No comments yet. Be the first to write one.
Open the Session Cartographer Explorer web UI for visual browsing of Claude Code and Codex session history.
The English summary is being prepared. Showing the original description.
Retired. Project orientation moved to the remember skill (`/remember --project <name>` with no query). Use remember instead.
The English summary is being prepared. Showing the original description.
Bring past diagnoses to a bug before fixing it, record the new root-cause hypothesis, and close it as confirmed or refuted once the fix is verified. Use when a bug could come from more than one layer, or looks like something seen before.
The English summary is being prepared. Showing the original description.
Recall past work across Claude Code and Codex sessions. Finds decisions, research, fixes, and conversations by intent; with only a project and no question, orients on that project's recent state.
The English summary is being prepared. Showing the original description.
Diagnose or enable Session Cartographer semantic search in Codex, including least-privilege localhost access to local Qdrant and the embedding server. Use when setup, Qdrant health, localhost reachability, sandbox network denial, or semantic indexing is in question.
The English summary is being prepared. Showing the original description.
Brief on concurrent sessions — who else is working, in which repos, on which files, and what landed underneath you. Use when a commit appears that you did not make, before touching a shared file, or when orienting mid-task in a busy workspace.
The English summary is being prepared. Showing the original description.