wrap-up — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wrap-up (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Produce a tidy end-of-day snapshot so the next session can resume from a paste, even if this one was mostly admin/planning and the worktree is about to be discarded. Pair with /landscape (which orients at the start of a session) — /wrap-up is its bookend.
~/.agents/handoffs/YYYY-MM-DD-{slug}.md; prompt only on collision/overwrite or when choosing a different name./handoffs-tidy for a separately requested archive review; do not invoke it.Follow /name-session's client-selection convention. Read name-session for detection only; do not invoke its scope lookup during wrap-up.
/quit. Never recommend /exit in Pi./exit./quit; Claude Code: /exit — rather than guessing.The skill cannot execute either client's built-in exit command. After the summary renders, the user exits manually.
It also cannot rename the session for you. Only you typing the command triggers a rename. Step 4 prints /name {slug} for Pi or /rename {slug} for Claude Code, but the handoff file remains the durable artifact you grep for tomorrow.
MUST re-fetch on every invocation. Each run executes the helper scripts from scratch. Never reuse output from a prior run; state has changed since.
>
MUST use the dedicated helper scripts. Never construct ad-hocgit/gh/bdpipelines inline — those bypass the per-script permission allowlist and produce noisy permission prompts. Specifically: §0 must go through~/.agents/skills/wrap-up/scripts/header.sh, §1 must go through~/.agents/skills/wrap-up/scripts/activity.sh, §3 must go through~/.agents/skills/landscape/scripts/working-copy.sh(reused — landscape and wrap-up share the same hygiene probe), and §3b must go through~/.agents/skills/wrap-up/scripts/multirepo.sh.
Render the sections below in order. The helper scripts in §0, §1, §3, §3b, and §3d can run in parallel.
~/.agents/skills/wrap-up/scripts/header.shIt emits delimited sections:
---DATE--- — date '+%A %Y-%m-%d %H:%M' output.---CWD--- — pwd.---BRANCH--- — current branch (empty if not in a git repo).---GIT-COMMON-DIR--- / ---GIT-DIR--- — used for worktree detection (see below).---REPO-ROOT--- — canonical repo root (parent of the realpath of --git-common-dir); empty if not in a git repo.---DEFAULT-BRANCH--- — short name of the repo's default branch (main/master), empty if undetermined.---WORKTREES--- — one {path}|{branch} line per worktree of this repo (branch (detached) when applicable). §4 uses it to map a feature branch back to the worktree that holds it.---SETTINGS-DRIFT--- — one {worktree}|{file}|{count} line per worktree settings file holding permission entries the canonical (main-worktree) copy lacks (parse-error instead of a count when the file isn't valid JSON). §3c renders these.If ---GIT-COMMON-DIR--- and ---GIT-DIR--- differ, the cwd is a linked worktree (not the main checkout). Note this — it affects the warnings in §3. (String inequality is sufficient: the main checkout returns the same value for both — typically .git — while a linked worktree returns /path/to/main/.git for common-dir vs /path/to/main/.git/worktrees/{name} for git-dir.)
Capture ---REPO-ROOT--- as {repo-root} for the resume block in §4. It's the stable identity /handoffs uses to group sessions per project — independent of which worktree wrote the handoff, and resilient to the worktree being pruned later.
Default to the current cwd/branch, including trunk-based repositories with retained worktrees. Other worktrees do not prove this session belongs elsewhere. §4 considers an alternative only when this conversation identifies unfinished work there; a completed temporary branch is not a resume target.
Render:
## Wrap-up — {Weekday} {YYYY-MM-DD} {HH:MM}
**Where:** `{pwd}` on `{branch}`{worktree-suffix}{worktree-suffix} is _(linked worktree)_ when applicable, else empty.
Run the helper:
~/.agents/skills/wrap-up/scripts/activity.shIt emits delimited sections:
---STATUS--- — OK or NO_GIT. If NO_GIT, skip the Commits sub-section.---DATE--- — today's YYYY-MM-DD (the script's window).---WINDOW-START--- / ---WINDOW-END--- — the local, exclusive-end interval bounding activity.---AUTHOR--- — git config user.email for the commit filter.---COMMITS--- — one pipe-delimited line per commit: {worktree_basename}|{branch}|{sha}|{subject}|{when}. Empty if no commits today.---GH-STATUS--- — OK, UNAVAILABLE (gh missing or not authenticated), or ERROR (one or more searches failed).---GH-DIAGNOSTICS--- — bounded names of failed PR searches; empty for OK and UNAVAILABLE.---PRS-CREATED--- / ---PRS-MERGED--- / ---PRS-CLOSED-UNMERGED--- — JSON arrays (always [] when empty) from gh search prs --author=@me.---BEADS-STATUS--- — OK / NO_BD / NO_BEADS_IN_REPO.---BEADS-IN-PROGRESS--- — output of bd list --status=in_progress (state being left for tomorrow).---BEADS-STALE-DAYS--- — the idle grace period in days (WRAP_UP_STALE_DAYS, default 7). §3a names it in the prompt.---BEADS-STALE-CANDIDATES--- — aged tracker claims selected by the helper's update-time cutoff.These are §3a's report-only candidates, not proof of inactivity; session activity is unverified.
---BEADS-CREATED-TODAY--- — open Beads created inside the emitted local window; the helper queries with equivalent UTC bounds and excludes closed-same-day rows so they appear only in BEADS-CLOSED.---BEADS-CLOSED--- — closed Beads whose closure timestamp falls inside the same UTC-normalized window.activity.sh --workspace is the reusable, read-only catch-up contract used by /today. It preserves this default no-argument contract, resolves members through multirepo.sh --members-only, and repository-qualifies commits and JSON Beads lists. Workspace mode also emits the generic BEADS-CREATED marker with every issue created in the window, including issues now closed; the compatibility BEADS-CREATED-TODAY marker retains the existing open-only behavior. --workspace --previous-workday selects Friday on Monday and the preceding calendar day otherwise for /yesterday. The helper converts the emitted local WINDOW-START/WINDOW-END interval to UTC for timestamp filtering and requires Python 3.10+ for date and JSON processing. Do not parse workspace configuration or reproduce date-window logic in consumers.
#### Commits
Render as a table only if ---COMMITS--- is non-empty:
### 🧾 Commits today
| Worktree | Branch | SHA | Subject | When |
|----------|--------|-----|---------|------|If empty: _No commits today._ (Still useful — confirms admin-only session.)
#### PRs created / merged / closed today
If ---GH-STATUS--- is UNAVAILABLE, render _GitHub CLI not authenticated — PR roundup skipped._ and skip the sub-section. If it is ERROR, retain rows from successful searches and render _GitHub query failed — PR roundup may be incomplete._ plus the bounded ---GH-DIAGNOSTICS--- entries.
For OK or ERROR, parse the three JSON arrays. De-duplicate (a PR may appear in created + merged on the same day — merged wins). Render a single table if anything remains:
### 🔀 PRs today
| Event | PR | Repo | Title |
|-------|----|------|-------|[#{number}]({url}).Skip the section entirely if all three arrays are empty.
#### Jira tickets you touched today
Query for tickets the current user updated since midnight (status transitions, comments, edits):
mcp__jira__jira_get
path: /rest/api/3/search/jql
queryParams:
jql: updatedBy = currentUser() AND updated >= startOfDay() ORDER BY updated DESC
fields: summary,status,issuetype,updated
maxResults: 20
jq: issues[*].{key: key, summary: fields.summary, status: fields.status.name, type: fields.issuetype.name, updated: fields.updated}Render as a table only if non-empty:
### 📋 Jira touched today
| Key | Type | Status | Summary |
|-----|------|--------|---------|[KEY](https://.../browse/KEY)).Skip the section entirely if the result is empty. If the Jira MCP errors or isn't configured, render _Jira unavailable — skipped._ and move on (do not fail the skill).
Note: updatedBy catches the obvious cases (status changes, comments, field edits). It won't catch tickets you only read during the session — that's intentional. The conversation chat in §2 captures what you thought about but didn't change.
#### Beads — in-progress, created today, closed today
If ---BEADS-STATUS--- is NO_BD or NO_BEADS_IN_REPO, skip the whole sub-section silently.
Render each of the three lists only when non-empty. Combine into compact tables (the same column shape works for all three):
### 🎯 Beads
**In progress ({count})**
| ID | Type | Pri | Title |
|----|------|-----|-------|
**Created today ({count})**
| ID | Type | Pri | Title |
|----|------|-----|-------|
**Closed today ({count})**
| ID | Type | Pri | Title |
|----|------|-----|-------|Notes:
bd list output already filters out closed beads, so BEADS-CREATED-TODAY naturally excludes ones that were closed the same day. Those appear under "Closed today" instead — no de-duplication needed.do not start a new tracker investigation to complete the handoff. Mark unknown progress unknown.
### 🎯 Beads heading. If only some are non-empty, render just the populated tables and skip the empty ones (don't show _No created today._ placeholders — silence is shorter).This is the qualitative part — what git log cannot tell you. Looking at the current session's actual conversation history, summarise in 3–6 bullets:
Keep each bullet to one sentence. If the session was purely mechanical (e.g. just ran /landscape and /pr-status), say so in one line and skip the bullets — there's nothing worth restating.
Reuse landscape's helper (same probe — don't duplicate it here):
~/.agents/skills/landscape/scripts/working-copy.shParse its ---BRANCH---, ---STATUS---, ---AHEAD-BEHIND---, ---STASHES-ON-BRANCH---, and ---OTHER-WORKTREES-UNSAFE--- sections.
Render a small status block + appropriate warning:
### 📍 Working copy
- Branch: `{branch}`{worktree-suffix}
- Uncommitted: {clean | N modified / M untracked}
- Unpushed: {0 | N commits ahead of @{u} | no upstream}
- Stashes on this branch: {0 | N}Then exactly one of the warnings below (pick the first matching rule):
⚠️ Uncommitted work — the resume block does not preserve file diffs. Review in a separate preservation task before removing this checkout.⚠️ {N} unpushed commit(s) remain local. A handoff does not publish them or authorize a push.ℹ️ Linked worktree with no code to preserve. Conversation recovery still depends on a verified handoff save — check the save result below before leaving or pruning.Also surface other worktrees with unsaved work from ---OTHER-WORKTREES-UNSAFE--- as a footnote if any exist — easy to forget those after closing the session.
Unattributed changes. Report changes not attributable to this session as ownership unknown, not automatically another session's work. Never commit, stash, discard, push, or repair any changes inside wrap-up. A commit author or tracker status does not establish session ownership.
The §3 probe only inspects the cwd repo. In a multi-repo workspace (mgit services or git submodules) that silently misses unpushed/uncommitted state in sibling repos — the single most common wrap-up blind spot. Run the roll-up:
~/.agents/skills/wrap-up/scripts/multirepo.shIt emits ---MARKER--- (mgit | submodules | none), ---ROOT---, and ---REPOS--- lines: {name}|{branch}|{ahead}|{behind}|{upstream}|{modified}|{untracked} (ahead/behind are - with no upstream; the root repo appears as its own row).
### 🗂️ Other repos in this workspace
| Repo | Branch | Unpushed | Behind | Uncommitted |
|------|--------|----------|--------|-------------|{ahead}, or no upstream when upstream=no (local-only, never pushed).⚠️ diverged — N ahead / M behind.{modified} modified / {untracked} untracked (omit zero parts).Load-bearing for resume: a service repo left ahead-but-unpushed, or a deploy/config repo left diverged, is exactly what tomorrow forgets. Feed any unpushed/diverged members into the resume block's Open threads or Blocked on you field (§4).
Skip this section silently if §0's ---SETTINGS-DRIFT--- was empty (not a git repo, no linked worktrees, no drift — or no python3, in which case /tidy-settings remains the manual path).
Each line is {worktree}|{file}|{count}: {count} permission entries that live only in that worktree's .claude/{file} and not in the canonical main-worktree copy. For settings.local.json (gitignored, per-worktree) those entries are deleted forever when the worktree is pruned — that's the high-stakes case, and exactly the loss this check exists to prevent. For settings.json (git-tracked) the drift is an uncommitted edit; committing it is the fix, and §3's hygiene warnings already cover uncommitted files.
Render one line per drifting file:
### ⚙️ Worktree settings drift
⚙️ {count} permission(s) in `{worktree}/.claude/{file}` exist only in that worktree — pruning it loses them.(For a parse-error count, render ` {file} does not parse — /tidy-settings will diagnose ` instead.)
Add an Open-threads bullet: {count} worktree-only permission(s) in {worktree} — separately run /tidy-settings before pruning. This is report-only: do not invoke a repair skill or copy settings.
From the current cwd repo/worktree, run the authoritative local-only, read-only helper (Python 3.10+, Git and Gitleaks required):
~/.agents/skills/artifact-hygiene/scripts/artifact_hygiene.py --prettyCapture stdout and exit status, including nonzero exits. Render coverage before findings using artifact-hygiene's Report contract: exact status/verdict, each source's status and safe error codes, normalized redacted findings, and suppressed counts with clone-local allowances disclosed. Use only normalized evidence; partial is never clean. Never recover raw evidence from reported files, commits, scanner output, or configuration. Remediation is a separate explicitly approved task; do not edit artifacts, rewrite history, change allowances, install tooling, or copy detectors during wrap-up.
Validate artifact-hygiene/v2. Exit 0 alone is not clearance: complete reports may have verdict clean, advisory, or block. Show advisory findings without describing them as harmless. Exit 2 is partial and exit 3 is failed; both grade block. Missing tools or malformed output are unavailable, not clean. Check the verdict against the findings' policy.grade values and coverage; inconsistent reports are unavailable rather than recorded as valid grades. Audit failure never blocks saving the handoff: render available coverage and continue to §§4–5.
Add this separate line to the working-copy risks and the saved resume block, even when clean: Artifact hygiene ({audited-cwd}): {status}/{verdict}; {summary counts including suppressed}. Preserve helper status/verdict exactly; if no valid report exists, write unavailable — no valid report instead. Keep finding locations/evidence in the visible report, not the saved handoff. For findings or incomplete coverage, add an Open-threads bullet for a separately approved review before publication.
The audit covers only this cwd repo, not the whole workspace or the handoff being written afterward. Other repositories in §3b remain unaudited unless the helper is separately run with their verified worktree path as its repository argument. Label every audited location; never inherit a root's clean verdict for members or switch to the main checkout when wrapping a linked worktree.
Use ---BEADS-STALE-CANDIDATES--- and ---BEADS-STALE-DAYS---; skip when the tracker is absent or no candidates were returned. List the candidate IDs with their owning repository when known. Say: Tracker claims not updated for {stale-days}+ days; session activity is unverified. Absence from commits, branches or PRs is not proof of abandonment; private bead IDs need not appear in any of them. Keep current status in the handoff and name /backlog-groom as a separate review when relevant. Wrap-up does not change tracker status, close work, or claim a task.
A self-contained paste that, dropped into a fresh session tomorrow, lets the next instance pick up without re-discovering context. Fenced as markdown so it copies cleanly.
Before rendering the block, surface the topic slug and a paste-ready rename so the user sees what this session is being filed under and can name the session to match before exiting:
**Topic slug:** `{slug}` — used as the Resume title and the handoff filename.
**Rename this session to match:**
/name {slug}
(Paste it into Pi's command input and press Enter. In Claude Code, use `/rename {slug}` instead. Optional; skip if the session is already named for this slug.)This is the explicit "what should we call this session" cue — without it, the slug is buried inside the fenced block and easy to miss.
#### Reconcile the branch when parked on the trunk
Gate — skip this whole sub-section unless the repo has linked worktrees (more than one ---WORKTREES--- entry from §0). With only the main checkout there is no other-worktree feature branch to recover, so just record the current branch as-is (even the trunk). The reconciliation below is worktree-specific machinery and pure noise for projects that don't use worktrees — don't run it for them.The resume block's {branch} and {cwd} default to §0's current cwd and branch. Preserve that location in trunk-based work. Only reconcile when this session has identified unfinished work in another checkout; worktree count, commit authorship and matching branch names do not prove ownership.
For that identified unfinished work, locate a possible resume target in the existing evidence:
---WORKTREES---.---WORKTREES--- whose name contains the topic slug's ticket (e.g. slug ab-1505-… → branch fix/AB-1505-…).Then:
note when applicable. Never substitute an unrelated or already-integrated branch. Surface the change:
> Wrapped from {cwd-on-trunk} (on {default-branch}), but today's work is on {feature-branch} in {feature-worktree} — recording that as the resume location.
AskUserQuestion which branch/worktree the next session should resume in. Options: each candidate {branch} — {worktree basename}, plus Stay on {default-branch}.{repo-root} is unaffected — it's the same repo regardless of which worktree you record.
### Resume block
Where to pick up: {cwd} on branch {branch}{worktree-note} Repo root: {repo-root} Jira: {jira-field} Beads: {beads-field} Deliverable: {deliverable-field} PRs: {prs-field}
Context:
Decisions so far:
Working-copy risks:
Open threads:
Blocked on you (manual ops):
Suggested next step:
Pointers:
Guidance for the model when filling this in:
{YYYY-MM-DD} and {HH:MM} both come from the header script's ---DATE--- field (date '+%A %Y-%m-%d %H:%M'). The time disambiguates several same-day handoffs in the /handoffs picker — list.sh reads it back from this header line (falling back to file mtime for older handoffs), so keep the {YYYY-MM-DD} {HH:MM} shape on the # Resume: line intact.make …-sync, pushes the permission classifier reserved, granting access. Keep these out of "Suggested next step" (which is the agent's next move). Omit the field entirely when there are none — most sessions have none.{topic-slug} is kebab-case, ≤4 words. Pick the most specific noun phrase — ab-1107-cta-event beats cta-stuff.{worktree-note} is (worktree at {path}) for linked worktrees, else empty.{cwd} should be an absolute path when possible — not a relative path like packages/web/. The /handoffs skill matches handoffs to repos via the recorded location, and relative paths can't be resolved later.{repo-root} is the value captured in §0 (canonical repo root — parent dir of the realpath of --git-common-dir). Omit the line entirely (don't render an empty value) if the session wasn't in a git repo.#### Header fields: Jira / Beads / PRs
These are structured top-level fields (one per line, right under Repo root:) so future tooling can grep them cleanly. Multiple values per field are common — use comma-separated backtick-quoted tokens. Use — when there are none for that field. Never omit a field; if empty, render —.
Auto-populate from the data already gathered in §§1–2; do not ask the user. Sources, in priority order:
fix/AB-649-… → AB-649) + any keys the chat referenced. De-duplicate. Format: ` AB-649, AB-651 `. bd-123, bd-124 `. Don't list every closed bead from §1 if there were many — prefer the in-progress set as the primary signal. This is the full context list — own work and recurring "in-progress elsewhere" / parent-epic beads.Beads that is this session's own work — the bead(s) whose closure means this handoff is finished. Derive it from the beads referenced in today's commits in this repo (§1's activity) and/or the bead the session actively worked, in_progress or just closed. Exclude context beads (recurring "in-progress elsewhere", another session's WIP) and parent epics — they appear in Beads only. Format: ` bd-123 . This is what /handoffs and /handoffs-tidy key their "done" check on for trunk repos (where everything commits to master, so there's no branch/PR signal). **When unsure, include rather than omit** — an extra bead only delays the handoff being marked done (safe); omitting an own-work bead can make it look done while live work remains. Use —` when the session delivered nothing of its own (pure context/triage/discussion) — then the handoff stays live until something else marks it done. #42, #43 — keep the markdown link form so the next session can click through. Use just #42 ` if no URL is available.If a field has more than ~4 items, keep the most relevant 4 and add (+N more) after the last item rather than wrapping to a second line. (For Deliverable, a (+N more) truncation makes the bead-closure check un-verifiable — so keep that field complete; it's own-work only and should rarely exceed a couple of beads.)
Persisting the resume block is the point of /wrap-up: /handoffs, /landscape, and launchers such as cl and pl consume files under ~/.agents/handoffs/. The deterministic helper owns path construction, no-clobber creation, writing, and read-back verification. Frontmatter is not a portable permission sandbox; each client's actual permission policy still applies.
Pass only the exact unfenced resume summary already rendered in §4, never raw transcripts, credentials, .env contents, or unrelated session text. Content sanitization remains the model's responsibility; the save operation is not a secret scanner.
#### Pi: use the dedicated tool
Call save_handoff with date, time, slug, and content (the complete unfenced block). This fixed-destination operation is available in every session-mode guard state, including plan, conflict and lost, without /implement or /grant-file. It does not grant repository, native-file, or shell-write authority. It runs the reviewed helper without a shell and verifies its receipt. New saves need no prompt; the tool uses only a short-lived target-file lease, never a directory or session-long grant. Distinct filenames can save concurrently; a contended target remains blocked.
If the tool is absent, disabled or fails, report the handoff unsaved and keep the recovery block. Do not fall back to Bash, native file tools, a different directory, or /grant-file in Pi. Activation/reload of a compatible pi-session-mode package is a separate owner-approved operation. A helper-digest mismatch needs a reviewed adapter update, not a bypass.
#### Other clients: use the helper under their own permission policy
The following shell invocation is not a Pi fallback. When a shell heredoc is the stdin mechanism, use a quoted delimiter absent from the block so backticks, $(), backslashes, and other content are not expanded:
~/.agents/skills/wrap-up/scripts/save-handoff.py {YYYY-MM-DD} {HH:MM} {slug} <<'WRAP_UP_HANDOFF_EOF'
# Resume: {slug} — {YYYY-MM-DD} {HH:MM}
...
WRAP_UP_HANDOFF_EOFThe helper requires Python 3.10+, accepts at most 64 KiB of UTF-8, and returns one JSON object with schemaVersion: "wrap-up-save/v1". It rejects malformed identity arguments, empty input, missing/out-of-order/empty required fields, NUL bytes, a mismatching first-line header, symlink/non-regular targets, and stale overwrite approval. Structure checks catch common truncation, not semantically incomplete prose; byte verification binds the submitted block, not the conversation transcript. It writes mode 0600 through a same-directory temporary file, installs a new target without clobbering, then reopens it without following symlinks and verifies the exact bytes and file identity. The target path is always $HOME/.agents/handoffs/{date}-{slug}.md; do not supply or substitute another path. The helper creates only that handoffs directory (mode 0700) beneath an existing, non-symlinked ~/.agents. While the retired ~/.claude/handoffs/ still exists it returns failure with reason legacy-storage-present and writes nothing: report that handoff storage must be migrated first (a separately approved whole-directory move), and never fall back to the legacy path or a shell write.
#### Verified new save
The normal no-collision call needs no user prompt. Treat it as saved only when that same Pi tool invocation succeeds (or the other-client helper exits 0) and returns valid v1 JSON containing all of:
"status": "saved"pathmode: "new"sha256bytes countOnly after those postconditions pass may you print:
Saved to `{path}` (verified `{sha256-prefix}`).Never print Saved to, a cat command, or an equivalent persistence claim based on path selection, an earlier file, a malformed response, or intended tool use. If you omit the save call, report unsaved rather than infer success. This is an instruction to the model, not host-enforced output validation: the helper cannot prevent an uncalled or disobeyed workflow from inventing a success claim. Client-side output guards are outside this skill's scope.
#### Collision — prompt before replacement
A regular file already at the target returns status: "collision", its existingSha256, and a first-free suggestedSlug (the standalone helper exits 3); it never counts as saved, even if its contents look valid. Prompt:
~/.agents/handoffs/{YYYY-MM-DD}-{slug}.md already exists. Save this handoff how?Options:
save_handoff with the same complete block and overwriteSha256: existingSha256; the tool requires a fresh native TUI confirmation bound to the existing and replacement hashes. Headless/RPC overwrite is refused; a hash or chat approval cannot substitute for the dialog. Other clients require explicit approval before the helper's --overwrite-sha256 {existingSha256} call. Success still requires the verified-save response above with mode: "overwrite"; a changed, missing, symlinked, or non-regular target fails.The -N collision suffix remains a first-class convention: list.sh uses header time for normal same-day ordering, then the suffix to order an established collision family.
#### Unsaved or failed
Any non-collision error, status: "cancelled", invalid/missing response, unexpected path/mode, or failed postcondition means the save is not verified. Do not assume the file is absent: an I/O or concurrent-change failure may leave data needing recovery. Re-render the complete resume block, then report the attempted path plus only the helper's bounded reason/detail:
⚠️ Failed to save handoff to `{attempted-path}`: {reason} — {bounded-detail}Omit Saved to and every cat command. On overwrite verification or directory-sync failure, the helper attempts to restore the hash-approved prior file only when it is safe to replace the failed write. If rollback is unsafe or fails, backupPath identifies the retained recovery copy: show that path and stop rather than deleting it or retrying blindly. Temporary recovery copies are not launcher handoffs. Overwrite confirmation is still a user decision; a hash is not proof of consent. The helper targets a user-owned local directory, not transactional isolation against hostile concurrent writers.
Missing Python, denied execution, or unsupported filesystem operations also mean unverified. Do not broaden permissions, change the user's handoff directory, or fall back to an unchecked write. The visible resume block remains the manual recovery path.
/handoffs-tidyArchiving belongs to a separately requested /handoffs-tidy run. After saving, mention that handoff if older context may need review; do not invoke it or inspect archive candidates here. Wrap-up does not touch ~/.agents/handoffs/archive/.
Select exactly one footer using the client selection from Important — client-specific commands.
Pi footer:
---
**Next:** run `/quit` to close this session. Resume tomorrow with `cat ~/.agents/handoffs/{file}.md` (or paste the block above).Claude Code footer:
---
**Next:** run `/exit` to close this session. Resume tomorrow with `cat ~/.agents/handoffs/{file}.md` (or paste the block above).Unknown-client footer:
---
**Next:** close this session manually — Pi: `/quit`; Claude Code: `/exit`. Resume tomorrow with `cat ~/.agents/handoffs/{file}.md` (or paste the block above).Include the cat half only for the exact path returned by a valid status: "saved" response in this wrap-up run. Otherwise drop it and keep just the paste reminder.
Each section is independent — fail soft, don't block the rest.
_GitHub CLI not authenticated — PR roundup skipped._discard established context or invent fetched evidence.
/tidy-settings run by hand covers the same ground.in working-copy risks and continue saving the handoff; never infer clean from missing findings.
cat command, and continue to the footer. The generated block is still the recovery artifact even if durable verification failed.Run make test-wrap-up test-session-boundaries in the owning repository. The Pi adapter's cross-repository checks live in pi-session-mode: set HANDOFF_SAVE_HELPER to this skill's absolute scripts/save-handoff.py path when running its npm run check and npm run verify:git-install. Its reviewed helper digest must match; these checks do not activate the installed extension. To include both external launcher discovery paths without starting either client, explicitly select the shared ai-tools collector:
HANDOFF_CONTEXT_GATHER=/absolute/ai-tools/shared/launcher/context-gather make test-wrap-upThat integration test uses synthetic handoffs, an isolated HOME, and a stubbed GitHub CLI. Without the explicit path it is reported as skipped, not assumed covered. These tests do not establish client permission pre-approval or enforce model output.
WRAP_UP_STALE_DAYS defaults to 7; a non-numeric value falls back to 7.This changes report filtering only, never the meaning of a claim or permission to demote it.
git stash as a way to "preserve" work for tomorrow without committing — stashes evaporate from memory faster than commits, and worktree pruning takes them with it. Prefer a WIP commit on a throwaway branch if there's something to save.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.