name: wrap
description: This skill should be used when the user says /vibe-wrap (or /vibe-wrap:wrap) and wants a session-end handoff doc that reads the breadcrumb trail sibling vibe plugins already left, surfaces what shipped + what's uncommitted + what's unpushed, and gates commit + push interactively. Multi-repo aware (read wide, mutate narrow): "What shipped" + "Still unpushed" span every sibling repo that had commits in the session window, while the commit/push gates stay scoped to the current repo. Reads breadcrumbs, sibling session-logs / friction / wins, git state across repos, and the active decision-log backend. Writes a markdown wrap doc to docs/session-wraps/<ts>.md (fallback .vibe-wrap/wraps/<ts>.md) and prints inline. Bumper-lanes invariant — every gate defaults to no-action and has a clear skip path. Flags: --inline-only, --bridge, --session-window <hours>, --repos <p1,p2,...>, --repo-roots <dir>, --no-multi-repo.
wrap — Read the trail, render the handoff, gate the rest
The user-facing session wrap. Read the breadcrumb trail the toolkit already left, surface what shipped + what's uncommitted + what's unpushed, render a durable handoff doc, then surface the commit / push / decision-log / bridge gates interactively. The doc always lands. The gates are bumpers, never walls.
Read ../guide/SKILL.md first for shared behavior — voice rules, the bumper-lanes invariant, persona adaptation, friction-trigger contract, namespace isolation, and ecosystem-aware composition.
Before you start
- Session logging. Call
session-logger.start("wrap", <cwd>)at command start,session-logger.end(<entry>)at command end. Per../session-logger/SKILL.md. - Persona + mode. Read
shared.preferences.personaandshared.preferences.modefrom the unified profile per the guide. Persona shapes the prose voice; mode shapes pacing. The bumper-lanes invariant never changes. - Hard dependency: Python 3.11+. If the runtime is older or absent, fail at command start with a clear next step. Git CLI is a soft dependency — if absent, the wrap still renders from breadcrumbs + sibling reads + decision-log, with a one-line note that git state is unavailable.
- The division of labor.
scripts/render-wrap.pydoes the deterministic read + render + gate-state computation. It performs no mutations. This SKILL drives the interactive gestures (commit, push, decision-log append, bridge) based on the gate state the script reports. Keep that boundary — the script is reproducible; the gates are conversational.
How the user invokes
/vibe-wrap
/vibe-wrap:wrap
Flags:
| Flag | Default | Purpose |
|---|---|---|
--inline-only |
off | Skip the file write. The wrap still prints inline; gates still surface. |
--bridge |
off | Force the dashboard-bridge gate to register eligible regardless of threshold (still MCP-only). |
--session-window <hours> |
4 | Fallback window when ${CLAUDE_SESSION_ID} is unavailable or yields no breadcrumbs. |
--repos <p1,p2,...> |
(discovery) | Explicit repo set for the read-wide pass — bypasses sibling discovery. Each path is read (filtered to git repos), plus the current repo. |
--repo-roots <dir> |
parent of cwd | Override the discovery scan root for sibling repos. |
--no-multi-repo |
off | Disable multi-repo discovery; read the current repo only (the v0.1.0 shape). |
Step 1 — Decision-log first-run (interactive, once)
Before rendering, resolve the decision-log backend. The render script is read-only and will report backend: "pending" if no config exists — it never triggers the interactive picker. This SKILL owns the first-run gesture.
At command start, import the dispatcher and call active_backend(cwd). If it returns None (first-run pending) AND the session is interactive:
- Run the first-run picker per
references/decision-log-backends.md§ First-run UX. Four options, recommended default labeled,[skip — disabled]as the fourth choice. - Persist the user's choice to the chosen config path.
- Then proceed to render.
If active_backend(cwd) returns a real backend, skip the picker — config already exists.
Step 2 — Render (deterministic)
Run the render core, passing the resolved session UUID:
python ${CLAUDE_PLUGIN_ROOT}/skills/wrap/scripts/render-wrap.py \
--session-id ${CLAUDE_SESSION_ID} \
[--session-window <hours>] \
[--inline-only] \
[--bridge] \
[--repos <p1,p2,...>] \
[--repo-roots <dir>] \
[--no-multi-repo]
The script:
- Resolves the session window (earliest breadcrumb ts, else
now − window-hours). - Runs the reader scripts. Git state is read WIDE via
multi-repo-state.py, which discovers sibling git repos under the scan root (default = parent of cwd) and reports per-repo state for every repo that had ≥1 commit in the window —git-state.pyremains the single-repo core it calls.read-breadcrumbs.pyandread-sibling-state.pyrun as before. - Reads decisions via the decision-log dispatcher (read-only), scoped to the current repo.
- Assembles the markdown wrap from
assets/wrap-template.md. - Writes to
docs/session-wraps/<YYYY-MM-DD-HHmm>.md(fallback.vibe-wrap/wraps/<ts>.mdif nodocs/) in the current repo only, unless--inline-only. - Emits the rendered doc, then a fenced JSON block tagged
```VIBE-WRAP-GATE-STATEdescribing which gates are eligible, the multi-repo discovery summary, and sibling repos' read-only state undergates.other_repos.
Print the rendered wrap doc inline to the user. Then parse the gate-state JSON block — do not show that block to the user; it's the SKILL's control channel.
The wrap doc has six sections: What shipped, Decisions logged, Friction signals captured, Still uncommitted, Still unpushed, Session bounds. Each renders an empty-state line when there's nothing to report — never a silent gap.
Multi-repo shape (v0.2.0 — read wide, mutate narrow):
- What shipped leads with
Across N repos this session (M commits total): a, b, c.followed by a per-repo subsection (repo name, commit count, recent commits — capped at 10/repo with a truncation note). The current repo is flagged(current). - Still unpushed reports the current repo's push status actionably, then lists any sibling repo ahead of its remote as informational only — vibe-wrap never offers to push another repo.
- Still uncommitted stays scoped to the current repo (it owns the commit gate). Sibling dirty state is surfaced read-only under
gates.other_reposin the gate-state JSON.
See references/gate-design.md § Read wide, mutate narrow for the boundary contract.
Step 3 — Gates (interactive, default no-action)
Read references/gate-design.md for the full per-gate contract. Surface each gate only when the gate-state block marks it eligible: true. Every gate defaults to no-action. Pressing enter or typing n skips it.
All four gates are scoped to the current repo only (mutate narrow). The gate-state gates.commit / gates.push reflect the current repo's state. gates.other_repos is a read-only array of sibling repos' uncommitted/unpushed state — surface it informationally if it helps the user, but never offer to commit or push a sibling repo. There is no gate for another repo.
Gate 1 — Commit
When gates.commit.eligible is true:
- List the uncommitted files (already in the wrap doc's "Still uncommitted" section).
- If
gates.commit.suppressed_reasonis set (detached HEAD / mid-rebase), do NOT offer the commit gate — surface the reason as state to resolve manually and skip to the next eligible gate. - Ask
commit these? [y/N]. DefaultN. - On
y: draft a commit message from the wrap summary. Askaccept message? [y/N/edit]. Onystage the listed files and commit; oneditopen$EDITOR; onNskip. - Secret-pattern path: for each entry in
gates.commit.secret_matches, fire an additionalcommit despite secret-pattern match? [y/N]perreferences/secret-patterns.md. DefaultNexcludes the matched file. Record any override in the wrap doc's notes. - Friction wiring (rows from
../guide/references/friction-triggers.md§ /vibe-wrap):- Declined (
n/enter) with uncommitted files present AND a SessionEnd nudge fired earlier this session →default_overridden/ low:echo '{"sessionUUID":"<uuid>","command":"wrap","friction_type":"default_overridden","confidence":"low","symptom":"declined commit gate; <N> file(s) left uncommitted"}' | python ${CLAUDE_PLUGIN_ROOT}/skills/friction-logger/scripts/log.py - Commit message rewritten (
edit, and >50% of the message body changed between the draft and the accepted version) →artifact_rewritten/ medium, withsymptomquoting the draft's and the accepted message's first lines.
- Declined (
Never git add -A. Never --no-verify. Never amend. Never empty-commit.
Gate 2 — Push
When gates.push.eligible is true (local ahead of a tracked remote, no unusual git state):
- Name the remote from
gates.push.remoteand the ahead count fromgates.push.ahead. - Ask
push to <remote>? [y/N]. DefaultN. - Multi-remote: after the answer, offer one follow-up
push to a different remote? [y/N]pergate-design.md§ Multi-remote path. List remotes excluding the upstream just handled; let the user pick. - Friction wiring: if the user declines (
n) while ahead-of-remote was non-zero →default_overridden/ low:echo '{"sessionUUID":"<uuid>","command":"wrap","friction_type":"default_overridden","confidence":"low","symptom":"declined push gate; <ahead> commit(s) ahead of <remote>"}' | python ${CLAUDE_PLUGIN_ROOT}/skills/friction-logger/scripts/log.py
Force-push is never offered. Diverged branches surface as state to resolve manually.
Gate 3 — Decision-log write
When gates.decision_log.eligible is true (active backend is not disabled / pending / unknown):
- Ask the backend-specific prompt per
gate-design.md§ Gate 3 (e.g.,append a session-end decision to your Markdown decision log? [y/N]). - On
y: build the decision dict — title from the wrap summary, body excerpt,link= wrap doc path,project_tag= bound 626Labs project ID (MCP) or repo name (file backends) ornull(unbound). Call the dispatcher'sappend(decision, cwd). - On append failure (
{"ok": False}), surface the error string and continue. The wrap doc still stands.
Gate 4 — Dashboard bridge
When gates.bridge.eligible is true (backend is 626labs-mcp AND threshold met):
- Name the signals from
gates.bridge.threshold_signals(decisions logged, commits in window, bridge flag). - Ask
bridge strategic context to the dashboard's Architect AI? [y/N]. DefaultN. - On
y: call the auto-detected decision-log MCP's bridge tool — the recognized one is the 626Labs dashboard (mcp__626labs-cloud__bridge_context_to_architect) — with the wrap summary as context. If the MCP is unreachable at gate time, surface a one-line note and skip silently. The MCP is optional; absence is never an error. - Friction wiring: if the user declines (
n) when the threshold fired organically (threshold_signals.decisions_logged >= 1orcommits_in_window > 2) →complement_rejected/ high, withcomplement_involvedset to the bridge tool. Strong signal — the threshold may be too loose, or the prompt copy doesn't land the stakes:
If eligibility came only fromecho '{"sessionUUID":"<uuid>","command":"wrap","friction_type":"complement_rejected","confidence":"high","complement_involved":"mcp__626labs-cloud__bridge_context_to_architect","symptom":"declined bridge gate after threshold fired"}' | python ${CLAUDE_PLUGIN_ROOT}/skills/friction-logger/scripts/log.py--bridge(threshold_signals.bridge_flagtrue, neither organic signal present) and the user declines, logdefault_overridden/ low instead — the flag is a valid override path; aggregate use signals the threshold may be too tight.
Bridge is opt-in per gesture even when the threshold fires. Never autonomous.
Step 4 — Write back gate outcomes, then close
Gate outcomes write-back
The render at Step 2 captured the session as of render time. The gates resolve minutes later — a decision logs at gate time, a commit lands, a push fires. Without a write-back the durable artifact is permanently wrong about its own session: it says No decisions logged this session. even when Gate 3 just logged one. The wrap doc is what a future session reads; it has to tell the truth about the run that produced it.
So after every gate in Step 3 has resolved (accepted, declined, or skipped), and before calling session-logger.end(), append a ## Gate outcomes block to the wrap doc this run wrote.
- Skip entirely when
--inline-only— no file was written, so there's nothing to append to. - Target the wrap-doc path from the gate-state JSON's
wrap_doc_path. If it'snull(the earlier write failed), skip silently — never recreate the file. - Pure append. Open the file, append the block below. Never rewrite the rendered sections above it — the render half stays reproducible; this is the one conversational addition.
Emit one row per gate that was eligible this run (the gate-state block marks each eligible). A gate that never surfaced gets no row — don't pad with "not eligible" lines. Use the real resolved values, not placeholders:
## Gate outcomes
- Commit: committed N file(s) as `<sha>` — or — declined, left uncommitted — or — skipped (detached HEAD / mid-rebase)
- Push: pushed N commit(s) to `<remote>` — or — declined, left unpushed
- Decision log: logged to `<backend>` (ref `<id>`) — or — declined — or — append failed: `<error>`
- Bridge: bridged strategic context to the dashboard Architect — or — declined
The decision-log row carries the backend name and, on a successful append, the ref ID the dispatcher returned (e.g., the 626Labs dashboard decision ID). The commit row carries the short SHA of the commit the gate produced. These are the facts a future reader needs and the next /vibe-wrap:evolve-wrap cycle parses.
Close
End with a one-line handoff:
- If the doc was written:
Wrap saved to <path>.plus a note on any gates the user declined (still uncommitted / still unpushed). - If
--inline-only:Wrap printed inline (no file written). - Call
session-logger.end()withcomplements_invokedlisting each sibling whose state the wrap pulled from, plus the gates the user accepted.
Edge cases (all six PRD cases handled by the script + this flow)
- Empty session — zero breadcrumbs, zero git activity. The doc says so in each section's empty-state line and exits without error.
- No git remote —
gates.push.eligibleis false; the "Still unpushed" section notes local-only state. No push prompt. - Detached HEAD —
gate-state.unusual_git_state.detached_headis true; commit/push gates are suppressed with a reason. The doc still renders. - Mid-rebase — same suppression path via
mid_rebase. - Multiple remotes — the push gate names the upstream first, then offers the different-remote follow-up.
- Secret-pattern match — surfaced in "Still uncommitted" with a
WARNINGmarker; the commit gate fires the extra confirmation per match.
Performance budget (spec Open Issue #5)
Target <10s for the non-interactive portion: trail reader ≤4s, decision-log read ≤2s, render ≤1s. Gates are interactive and not budgeted. If a subsystem ships over budget against a real session, flag it for /evolve-wrap.
Friction triggers
The per-gate triggers are wired inline at their call sites in Step 3 — commit decline + commit-message rewrite (Gate 1), push decline (Gate 2), bridge decline (Gate 4). That's where the observable behavior happens, so that's where friction-logger.log() fires. The rows below cover the triggers that don't sit at a gate:
- First-run picker (Step 1): user picks a non-default decision-log backend →
default_overridden/ medium (symptomnames the picked backend). First-run only — never fires once config is persisted. - Command-start complement decline: user declines a Pattern #13 complement offer →
complement_rejected/ high (complement_involved= the sibling SKILL). --inline-onlyoverride when the file-write was the recommended path →sequence_revised/ low.- Re-wrap within 10 minutes of a prior
/vibe-wrapfor the same session →sequence_revised/ medium (symptomcarries both invocation timestamps). - Universal:
repeat_question/rephrase_requestedper the guide's universal triggers — only with a quoted prior insymptom.
Full contract: ../guide/references/friction-triggers.md § /vibe-wrap. Confidence is fixed per trigger; never override at log time. The invocation shape is identical everywhere — pipe a partial entry JSON (sessionUUID, command, friction_type, confidence, symptom, optional complement_involved) to python ${CLAUDE_PLUGIN_ROOT}/skills/friction-logger/scripts/log.py. The sessionUUID is the one session-logger.start() returned at command start.
Reference
scripts/render-wrap.py— the render core (read + render + gate state).assets/wrap-template.md— the markdown template.scripts/read-breadcrumbs.py·scripts/read-sibling-state.py·scripts/git-state.py— trail readers.scripts/multi-repo-state.py— multi-repo git-state aggregator (read wide). Discovers sibling repos + reports per-repo state; callsgit-state.py's single-repo core per repo.scripts/decision-log/__init__.py— decision-log dispatcher.references/gate-design.md— bumper-lanes invariant per gate.references/secret-patterns.md— the patterns that trigger the secrets warning.references/decision-log-backends.md— backend contract, config precedence, first-run UX.references/breadcrumb-contract.md— schema + plant contract for sibling authors.../guide/SKILL.md— shared behavior, voice, persona adaptation.