Session continuity memory for Claude Desktop
SaferSkills independently audited bro (Agent Skill) and scored it 75/100 (yellow). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 6 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 6 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.
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.
bro holds the middle layer of state that formal artifacts don't: mood, shortcut vocabulary, live threads, decisions, design tokens, debugging hypotheses, working-discipline rules. This layer is ephemeral in assistant attention but persistent if externalized.
v2.1 architecture: logs live per-repo, always visible at {cwd}/bro/ (the previously-supported hidden .claude/bro/ option was removed in v2.1.1 — single canonical location keeps logs inspectable and removes guessing about where to look). Three-tier layout — repo-wide universals (_principles.md), thread-wide stable context ({tag}/_thread.md), daily ephemeral state ({tag}/{date}.md). Tag derives from chat title (manual rename → AI-generated → first message), with a 6-hex postfix from session UUID for guaranteed uniqueness across parallel chats. Rename-aware: when operator renames a chat, bro detects the change and offers to rename the tag folder. /bro migrate performs LLM-driven semantic conversion of any pre-v2.1 logs into the new format with zero data loss.
Check if this skill lives at the canonical path:
test -f ~/.claude/commands/bro.md && echo canonical || echo otherIf file IS at canonical path → proceed to Step 0a.
If file is NOT at canonical path (copied somewhere else, or running inline):
Tell the user:
I notice I'm not installed in the standard location. Claude Desktop / Claude Code auto-discovers skills from~/.claude/commands/— if I live there,/brobecomes a proper slash command available in every new chat, and updates are easier.
>
Install me to the standard path? I can do it myself — just say yes.
If user confirms:
find or ask user)mkdir -p ~/.claude/commandscp <current-path> ~/.claude/commands/bro.md/bro will work from any chat."If user declines → continue from current location (still works, just not as a slash command).
Read ~/.claude/bro-config.json. Check the lastVersionCheck field (ISO timestamp).
If `lastVersionCheck` is missing or older than 7 days ago:
REMOTE_VERSION=$(curl -sfL --max-time 5 \
https://raw.githubusercontent.com/balaka/bro/main/bro.md \
| head -5 | grep "^version:" | awk '{print $2}')bro.md frontmatter the same way (path: ~/.claude/commands/bro.md).bro v{REMOTE} available (you have v{LOCAL}). Update? I can fetch it now — just say yes.
>
Note: if this is a major bump (e.g., v1.x → v2.x), the storage layout changed. v2 stores per-repo (not centralized). v2.1 added LLM-driven migration via /bro migrate that converts any legacy logs into the new format without data loss.If user confirms → run update flow (Section C). If user declines → proceed; re-check in 7 days.
lastVersionCheck in config so we don't re-check for another 7 days.Network failure or curl error → silent, skip check, don't block. Try again next time.
Skip version check entirely if env var BRO_NO_UPDATE_CHECK=1 is set.
setup or config → Section A (configure storage for this repo).list → Section D (list today's threads in this repo).update → Section C (self-update from GitHub).migrate → Section E (LLM-driven migration of legacy logs).tag=<name> or bare <name> (non-keyword) → Section B with <name> as explicit tag (postfix appended automatically).v2.1.1 stores per-repo at a single canonical visible location: {cwd}/bro/. The hidden .claude/bro/ alternative was removed in v2.1.1 — single visible path eliminates guessing about where logs live and prevents invisible-storage data loss.
if [ -d "$(pwd)/bro" ] && [ -f "$(pwd)/bro/.gitignore" ]; then
CURRENT="$(pwd)/bro/"
else
CURRENT="(not configured for this repo)"
fiTell user: "Current storage for this repo: {CURRENT}."
.claude/bro/ if it exists (post-v2.1.0 cleanup)If a .claude/bro/ folder exists from a v2.1.0 install where operator chose hidden:
if [ -d "$(pwd)/.claude/bro" ] && [ -f "$(pwd)/.claude/bro/.gitignore" ]; then
HAS_LEGACY_HIDDEN=1
fiDetected legacy hidden storage at{cwd}/.claude/bro/. v2.1.1 uses single visible location{cwd}/bro/. Move existing files to visible location?
>
y / n
If y:
mkdir -p "$(pwd)/bro"
# Move all files (including .gitignore) preserving structure
rsync -a "$(pwd)/.claude/bro/" "$(pwd)/bro/"
rm -rf "$(pwd)/.claude/bro"if [ -f ~/.claude/bro-config.json ]; then
LEGACY=$(jq -r '.storageDir // empty' ~/.claude/bro-config.json 2>/dev/null)
fiIf LEGACY is non-empty and not pointing to {cwd}/bro/:
Found legacy v1 storage at{LEGACY}. v2.1 stores per-repo at{cwd}/bro/. Run/bro migrateto convert any legacy logs into the new three-tier format (zero data loss; originals preserved in_legacy-pre-v2.1/).
Storage for this repo will be {cwd}/bro/.>
Press Enter to accept, or type a custom path if you have a specific reason (e.g., shared multi-repo workspace).
If user types a path: resolve (~ → $HOME, relative → cwd-relative), use as STORAGE. Otherwise: STORAGE="{cwd}/bro/".
Single canonical visible path is the design goal — it lets you read logs alongside code without remembering which subfolder. Custom paths are a power-user escape hatch, not a default option.
mkdir -p "$STORAGE"
cat > "$STORAGE/.gitignore" <<'EOF'
# bro storage — local session memory, not for commit by default.
# Uncomment a line to share repo-wide or thread-wide context with the team:
# !_principles.md
# !*/_thread.md
*
!.gitignore
EOFIf switching from one location to another within the same repo (e.g., user previously had .claude/bro/ and now picks bro/):
Existing storage at{OLD}has {N} files. Copy_principles.mdand_thread.mdfiles to new location? (Daily logs not copied — they reflect historical state.)
>
y / n
If y: copy _principles.md and */_thread.md to new location (daily files stay at old).
bro will store memory for this repo in{STORAGE}. A.gitignorewas added to keep daily logs local (you can edit it to commit principles/thread files if desired).
>
If you have legacy bro logs (from v1 or v2.0) anywhere on disk, run /bro migrate next to convert them into the new format.Detection (no per-repo config file — checked by existence). Canonical visible-only path:
CWD=$(pwd)
if [ -d "$CWD/bro" ] && [ -f "$CWD/bro/.gitignore" ]; then
STORAGE="$CWD/bro"
elif [ -d "$CWD/.claude/bro" ] && [ -f "$CWD/.claude/bro/.gitignore" ]; then
# Legacy hidden storage from v2.1.0 — prompt to migrate to visible
echo "Hidden .claude/bro/ detected — Section A Step 1a will offer to move it to canonical visible location."
STORAGE="$CWD/.claude/bro" # use temporarily; setup will migrate next /bro setup
else
# No bro storage in this repo yet → first-time setup
go to Step 1a
fiIf .claude/bro/ is detected, suggest running /bro setup to relocate to canonical {cwd}/bro/ (Section A Step 1a).
Run the prompt from Section A Step 3 (asking where to store), then Section A Step 4 (create dir + gitignore). After creation, return here and continue to Step 2.
After resolving storage, check for legacy state:
needs_migration=0
# Storage exists but no _principles.md AND has daily files → likely pre-v2.1
if [ ! -f "$STORAGE/_principles.md" ] && find "$STORAGE" -maxdepth 2 -name "*.md" -type f 2>/dev/null | head -1 | grep -q .; then
needs_migration=1
fi
# OR storage has *.nerve.md files at top level (pre-extraction)
if find "$STORAGE" -maxdepth 2 -name "*.nerve.md" -type f 2>/dev/null | head -1 | grep -q .; then
needs_migration=1
fiIf needs_migration=1:
Detected pre-v2.1 logs in{STORAGE}without_principles.md(or with mixed.nerve.mdfiles at top level).
>
Run/bro migrateto convert into the new three-tier format? It will: - Backup current state - Extract persistent material into_principles.md/_thread.md- Strip persistent from daily logs (keep refs) - Move originals to{tag}/_legacy-pre-v2.1/(never deleted) - Apply UUID postfix to existing tag names
>
y / n / skip-this-time
If y → run Section E, then return here for current chat capture. If n or skip → continue to Step 2 in degraded mode (creates empty _principles.md if absent).
#### 2a. Find current session UUID
Two strategies, in priority order:
Strategy 1 — via PPID (most reliable):
# Claude Desktop writes ~/.claude/sessions/{PPID}.json mapping its pid to sessionUuid
SESSIONS_DIR="$HOME/.claude/sessions"
# Walk up the process tree from current shell to find Claude Desktop's pid
# (the slash command runs as a child of Claude's helper process)
for cand_pid in $PPID $(ps -o ppid= -p $PPID 2>/dev/null) $(ps -o ppid= -p $(ps -o ppid= -p $PPID 2>/dev/null) 2>/dev/null); do
if [ -f "$SESSIONS_DIR/$cand_pid.json" ]; then
SESSION_UUID=$(jq -r '.sessionId // empty' "$SESSIONS_DIR/$cand_pid.json" 2>/dev/null)
SESSION_CWD=$(jq -r '.cwd // empty' "$SESSIONS_DIR/$cand_pid.json" 2>/dev/null)
if [ -n "$SESSION_UUID" ] && [ "$SESSION_CWD" = "$(pwd)" ]; then
break
fi
fi
doneStrategy 2 — via mtime (fallback if PPID lookup fails):
ENCODED_CWD=$(pwd | sed 's| |-|g; s|/|-|g')
PROJ_DIR="$HOME/.claude/projects/$ENCODED_CWD"
SESSION_JSONL=$(ls -t "$PROJ_DIR"/*.jsonl 2>/dev/null | head -1)
SESSION_UUID=$(basename "$SESSION_JSONL" .jsonl 2>/dev/null)If both fail (Claude Desktop without Code, edge case):
SESSION_UUID="$(date +%Y%m%d-%H%M%S)-$RANDOM"chat-{HHMM} for slugSet SESSION_JSONL="$HOME/.claude/projects/$ENCODED_CWD/$SESSION_UUID.jsonl" for next steps.
#### 2b. Resolve title from jsonl (priority chain)
Read jsonl entries, extract title with priority:
# 1. customTitle (manual rename via UI) — HIGHEST PRIORITY
TITLE=$(grep '"type":"custom-title"' "$SESSION_JSONL" 2>/dev/null \
| tail -1 | jq -r '.customTitle // empty' 2>/dev/null)
TITLE_SOURCE="custom"
# 2. aiTitle (auto-generated by Claude) — SECOND
if [ -z "$TITLE" ]; then
TITLE=$(grep '"type":"ai-title"' "$SESSION_JSONL" 2>/dev/null \
| tail -1 | jq -r '.aiTitle // empty' 2>/dev/null)
TITLE_SOURCE="ai"
fi
# 3. First user message — FALLBACK
if [ -z "$TITLE" ]; then
# Skip enqueue events whose content starts with / (avoid recursion if first action is /bro)
TITLE=$(grep '"operation":"enqueue"' "$SESSION_JSONL" 2>/dev/null \
| jq -r '.content // empty' 2>/dev/null \
| grep -v '^/' \
| head -1 \
| head -c 200)
TITLE_SOURCE="first-message"
fi
# 4. Timestamp fallback — LAST RESORT
if [ -z "$TITLE" ] || [ ${#TITLE} -lt 3 ]; then
TITLE="chat-$(date +%H%M)"
TITLE_SOURCE="fallback"
fiMultiple `custom-title` / `ai-title` entries: take the LAST occurrence (most recent rename wins). Use tail -1.
#### 2c. Slugify with stop-word filter (Python preferred for unicode)
Use python3 for reliable Cyrillic handling:
SLUG=$(python3 <<PYEOF
import re
title = """$TITLE"""
STOP_RU = {"давай", "сделаем", "сделать", "сделай", "хочу", "нужно", "надо",
"помоги", "помоги-ка", "посмотри", "проверь", "поправь", "исправь",
"и", "в", "на", "по", "с", "со", "из", "о", "об", "у", "к", "от",
"для", "это", "тут", "там", "этот", "эта", "эти", "тот", "та", "те",
"чтобы", "если", "когда", "как", "что", "так", "же", "ли", "бы"}
STOP_EN = {"hey", "hi", "hello", "let", "lets", "let's", "can", "could", "would",
"please", "help", "look", "check", "fix", "make", "do", # keep these for now: removing 'fix' would hurt; reconsider
"the", "a", "an", "and", "or", "of", "for", "to", "in", "on", "at",
"is", "are", "was", "were", "be", "been", "this", "that", "these",
"those", "it", "its", "they", "them"}
# Lowercase + tokenize (preserve cyrillic + latin + digits)
words = re.findall(r'[a-zа-я0-9]+', title.lower())
# Filter: drop stop-words, drop words shorter than 2 chars
filtered = [w for w in words
if len(w) >= 2
and w not in STOP_RU
and w not in STOP_EN]
# If filtering removed everything, fall back to unfiltered first 5 words
if not filtered:
filtered = words
# Take first 5 words, join with -
slug = '-'.join(filtered[:5])
# Truncate to 40 chars on word boundary
if len(slug) > 40:
truncated = slug[:40]
last_dash = truncated.rfind('-')
if last_dash > 20: # only truncate at word boundary if it's not too aggressive
slug = truncated[:last_dash]
else:
slug = truncated.rstrip('-')
print(slug)
PYEOF
)
# Final fallback if slug is somehow empty
if [ -z "$SLUG" ] || [ ${#SLUG} -lt 3 ]; then
SLUG="chat-$(date +%H%M)"
fiNotes on stop-words:
fix, make, do are kept in EN list because they often start titles like "Fix auth bug" — without them slug becomes just "auth bug". Operator can override per repo by editing local copy of bro.md if desired. (Future improvement: per-repo stop-word config.)#### 2d. Append UUID postfix
# First 6 hex chars of sessionUuid — deterministic uniqueness
POSTFIX="${SESSION_UUID:0:6}"
TAG="${SLUG}-${POSTFIX}"Why this postfix scheme:
#### 2e. Check session marker (continuation logic)
MARKER_FILE="$STORAGE/$TAG/.session.json"
if [ -f "$MARKER_FILE" ]; then
STORED_UUID=$(jq -r '.sessionUuid // empty' "$MARKER_FILE" 2>/dev/null)
STORED_TITLE=$(jq -r '.lastKnownTitle // empty' "$MARKER_FILE" 2>/dev/null)
if [ "$STORED_UUID" = "$SESSION_UUID" ]; then
# Same chat continuing in same tag — proceed to Step 2f (rename detection)
:
else
# COLLISION: tag folder exists but belongs to a different chat
# This shouldn't happen if postfix is correct, but defensive:
# append a second postfix from current UUID
EXTRA="${SESSION_UUID:6:4}"
TAG="${TAG}-${EXTRA}"
MARKER_FILE="$STORAGE/$TAG/.session.json"
fi
fi#### 2f. Rename detection
If marker exists and STORED_TITLE differs from current TITLE:
if [ -n "$STORED_TITLE" ] && [ "$STORED_TITLE" != "$TITLE" ]; then
STORED_SOURCE=$(jq -r '.titleSource // empty' "$MARKER_FILE" 2>/dev/null)
# Only prompt if source priority improved or title genuinely changed
# (e.g., aiTitle → customTitle is a clear upgrade; first-message → aiTitle too)
echo "Title changed:"
echo " was: ${STORED_SOURCE}: \"${STORED_TITLE}\""
echo " now: ${TITLE_SOURCE}: \"${TITLE}\""
echo ""
echo "Rename tag folder to reflect new title?"
echo " 1. Yes — move $TAG → {new-tag} (recommended; default)"
echo " 2. No — keep $TAG, just save new files there"
fiIf user picks 1 (or empty input = default):
${NEW_SLUG}-${POSTFIX}mv "$STORAGE/$TAG" "$STORAGE/$NEW_TAG"lastKnownTitle, titleSource, append to previousTitles[] array(renamed from $TAG since YYYY-MM-DD)TAG="$NEW_TAG" for the rest of this /bro invocationIf user picks 2:
lastKnownTitle to new title (so we don't ask again next /bro)previousTitles[] array#### 2g. Commit tag (create folder + marker)
mkdir -p "$STORAGE/$TAG"
# Write/update session marker
cat > "$STORAGE/$TAG/.session.json" <<JSONEOF
{
"sessionUuid": "$SESSION_UUID",
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"firstMessage": "$(echo "$FIRST_MSG_RAW" | head -c 200 | tr '\n' ' ')",
"lastKnownTitle": "$TITLE",
"titleSource": "$TITLE_SOURCE",
"previousTitles": [...]
}
JSONEOFExplicit-tag override: if operator passed /bro <tag> or /bro tag=<name>:
<name> directly as slug (after normalization: lowercase, spaces→dashes, strip non-alphanumeric)Before writing anything, load context:
These tell us what's already captured, so we don't duplicate.
Review the last 5–7 exchanges. For each piece of new material, run two tests:
Test 1 — temporal:
If a fresh chat opens in this repo tomorrow, would this still be true / relevant?
_principles.md or _thread.md)Test 2 — scope (only for persistent):
Is this universal across all work in this repo, or specific to the current topic / thread?
_principles.md_thread.mdAnti-duplication:
_principles.md or _thread.md → don't repeat in daily, optionally write (see _principles.md) ref.(без изменений с YYYY-MM-DD) note in daily.Classification table (concrete examples):
| Material | Destination |
|---|---|
| "Project on Bash + TS, not MCP" | _thread.md → Architecture (sticky) |
| "Never push without confirmation" | _principles.md → Sticky rules |
| "Tired today" | daily → Operator state |
| "Slice 2 = parsing layer for X" | _thread.md → Roadmap structure |
| "Slice 2 done" (status today) | daily → Roadmap status |
| "Term 'трек' = 3-line headline" | _thread.md → Shortcut vocabulary |
| "Decided: use SQLite, not Postgres" (today) | daily → DECIDED + ASK promote to _thread.md |
| "No Figma" (rejected today, hard) | daily → REJECTED (чтобы не возвращались) |
| "P0: no internal jargon leaks" | _thread.md → Design principles |
| "stripUndefined helper — don't remove" | _thread.md → Known subtleties / gotchas |
| "Не останавливать оператора по соображениям токенов" | _principles.md → Working discipline |
When material looks persistent but it's not 100% clear, ask the operator before cementing it.
One promotion ask per /bro invocation, max. Don't spam. Batch related items.
Format:
Promote to persistent? I noticed candidates:
>
1. "Storage is per-repo, not centralized" — looks like architecture decision →_thread.md? 2. "Auto mode active, minimize interruptions" — looks like working discipline →_principles.md?
>
y / n / pick (e.g., "1 only", "1 to thread, 2 skip")
Don't ask for material that's unambiguously ephemeral (operator state, daily decisions with explicit dates) — write directly to daily.
Don't ask for material that's already in persistent files — just skip.
Token-flow rule: Never stop the operator on token concerns. If operator's _principles.md contains the rule "no token economy in flow" — bypass any habit of warning about response length, depth, or budget.
Write order:
Section editing rules:
_principles.md, _thread.md): append-only for new items; existing items rarely change. Use (supersedes prior, dated YYYY-MM-DD) if a sticky decision is overridden.Sections in daily file are optional — include only when there's material. Empty sections are removed, not left as placeholders. Minimum viable daily file = metadata header + Operator state + Live threads OPEN + Read order at end.
Updated for this /bro:
📜 _principles.md — added: Sticky rules ("no --amend"), Working discipline #4 (token flow)
📂 {tag}/_thread.md — created: Architecture (3 decisions), Vocabulary (4 terms), Roadmap (6 slices)
📝 {tag}/{date}.md — updated: Operator state, Live threads OPEN, REJECTED (чтобы не возвращались)
Tag: bro-migration-fix-9e0988
Source: custom-title — "Bro Migration Fix" (you renamed it from "Fix plugin log storage...")
Postfix: 9e0988 (from session UUID 9e09887a-c861-4916-a49f-9d0fee261393)
Marker: bro/bro-migration-fix-9e0988/.session.jsonIf nothing new worth writing → say so explicitly, skip the writes:
No new persistent or ephemeral material since last update. Nothing written.
Invoked via /bro update or accepted from Step 0a prompt.
bro.md from GitHub: curl -sfL --max-time 10 \
-o ~/.claude/commands/bro.md.new \
https://raw.githubusercontent.com/balaka/bro/main/bro.mdversion: line. If invalid → abort, delete .new file, report error. NEW_VERSION=$(head -5 ~/.claude/commands/bro.md.new | grep "^version:" | awk '{print $2}')
OLD_VERSION=$(head -5 ~/.claude/commands/bro.md | grep "^version:" | awk '{print $2}')Major version bump: v{OLD} → v{NEW}. Storage layout may have changed.
>
After update, run /bro migrate in any repo with legacy logs — it will perform LLM-driven semantic conversion to the new format with zero data loss.>
Continue update? y / n
mv ~/.claude/commands/bro.md.new ~/.claude/commands/bro.md~/.claude/bro-config.json:installedVersion to new versionlastVersionCheck to current ISO timestampUpdated to v{NEW_VERSION}. Restart Claude Desktop (or open a new chat) to load the new version. If you have legacy bro logs anywhere, run /bro migrate in those repos.If curl fails or version check fails → report clearly: "Update failed: {reason}. You're still on v{current}. Try again later or update manually: curl -o ~/.claude/commands/bro.md https://raw.githubusercontent.com/balaka/bro/main/bro.md"
Invoked via /bro list.
[ -f "$STORAGE/_principles.md" ] && stat -f "%Sm" "$STORAGE/_principles.md"
find "$STORAGE" -maxdepth 2 -name "_thread.md" -type f find "$STORAGE" -maxdepth 2 -name "$(date +%Y-%m-%d).md" -type f{tag}/.session.json) for lastKnownTitle, titleSource, previousTitles.bro storage for this repo: {STORAGE}
Persistent:
📜 _principles.md updated 5d ago (8 sections)
📜 fix-auth-bug-a3f1c0/_thread.md updated 2d ago (4 sections)
Today's daily threads:
📝 bro-migration-fix-9e0988 updated 12m ago custom: "Bro Migration Fix"
📝 fix-auth-bug-a3f1c0 updated 2h ago ai: "Fix authentication bug in login flow"
📝 explore-roadmap-7b2ed5 updated 5h ago first-message: "let's explore the roadmap"
Total: 3 daily threads, 1 thread-context file, 1 principles file.
Renamed today:
📝 bro-migration-fix-9e0988 was "fix-plugin-log-storage-..." (renamed 2026-04-29)
Run /bro <tag> to continue a specific thread, /bro to auto-resolve from current chat.If no daily threads today → say "No bro threads created today in this repo. Run /bro to start."
/bro migrate (LLM-driven legacy conversion)Invoked via /bro migrate, or auto-suggested from Step 1b when pre-v2.1 logs detected.
Operating principles (from operator):
{tag}/_legacy-pre-v2.1/.TS=$(date +%Y%m%d-%H%M%S)
BACKUP="$HOME/Desktop/_bro-migrate-backup-$TS.tar.gz"
# Backup the storage in this repo
tar czf "$BACKUP" "$STORAGE" 2>/dev/null
echo "Backup: $BACKUP"If migrate is invoked across multiple legacy locations (Step 3), include all of them in backup.
Run Section B Steps 1-7 for current session. This:
_principles.md (universal context if any){tag}/_thread.md (topic context if any)This guarantees that even if migration of legacy logs crashes, the current chat's working context is preserved in the new format.
Search in priority order:
LEGACY_LOCATIONS=()
# 1. Current storage in this repo (may have pre-v2.1 layout inside)
[ -d "$STORAGE" ] && LEGACY_LOCATIONS+=("$STORAGE")
# 2. v1 central storages
for cand in ~/Desktop/bro-workspace ~/.claude/bro; do
[ -d "$cand" ] && [ "$cand" != "$STORAGE" ] && LEGACY_LOCATIONS+=("$cand")
done
# 3. Other bro/ folders in operator's Desktop
while IFS= read -r line; do
[ -d "$line" ] && [ "$line" != "$STORAGE" ] && LEGACY_LOCATIONS+=("$line")
done < <(find ~/Desktop -maxdepth 4 \( -name "bro" -o -path "*/.claude/bro" \) -type d 2>/dev/null)Report findings:
Found legacy logs at:
1. /Users/y/Desktop/frontend/xovi ai svelte/bro/ (10 tags, 25 files)
2. /Users/y/Desktop/frontend/product-os-design/bro/ (1 tag, 7 files)
3. /Users/y/Desktop/cowork/bro/ (3 tags, 14 files)
Migrate all? y / pick / nIf pick → ask which to include.
For each legacy location, for each tag folder:
#### 4a. Inventory the tag
for tag_dir in "$LEGACY_LOC"/*/; do
tag=$(basename "$tag_dir")
files=$(find "$tag_dir" -maxdepth 2 -name "*.md" -type f | sort)
count=$(echo "$files" | wc -l)
has_thread=$(test -f "$tag_dir/_thread.md" && echo y || echo n)
has_marker=$(test -f "$tag_dir/.session.json" && echo y || echo n)
echo "[$tag] $count files, _thread: $has_thread, marker: $has_marker"
done#### 4b. Read all daily files for this tag
ALL_FILES=$(find "$tag_dir" -maxdepth 2 -name "*.md" -type f ! -name "_thread.md" ! -name "_principles.md" | sort)Read each file's full content into context. Multiple files capture evolution of the topic.
#### 4c. LLM semantic classification
For each section in each file, classify:
Persistent universal (→ candidate for _principles.md):
Persistent topic-specific (→ candidate for _thread.md):
Ephemeral (→ daily file):
Use markers as hints (not gospel):
(sticky) → strong signal for persistent(carried since YYYY-MM-DD) → persistent, retain origin date(reinforced YYYY-MM-DD) → persistent, retain reinforcement dates(без изменений с YYYY-MM-DD) → was carried over from prior daily, possibly persistent(чтобы не возвращались) → REJECTED, ephemeral but with lasting do-not-revisit semantic(P0) … (PN) → priority annotation on persistent#### 4d. Cross-file deduplication within tag
When same architectural decision appears in 5 daily files in this tag → write to _thread.md once, with (carried since YYYY-MM-DD) from earliest mention.
When same vocabulary term defined in 3 files → write once in _thread.md. If definition evolved between files, keep latest, optionally note (refined YYYY-MM-DD).
When working-discipline rule appears across multiple tags (i.e., universal) → defer to Step 5 cross-tag aggregation; flag for _principles.md.
#### 4e. Write {tag}/_thread.md
Use the template from this file (see Templates section). Populate:
Add markers (carried since YYYY-MM-DD) for items extracted from older files; (consolidated 2026-04-29 from N source files) to top of file as audit trail.
#### 4f. Rewrite each daily file in v2.1 format
For each {date}.md original:
_thread.md. Replace with: (see _thread.md → Architecture) ref where useful.../_principles.md, _thread.md(чтобы не возвращались) for explicit do-not-revisit on REJECTED).If a file is mostly persistent material that's already been extracted — daily becomes very short. That's fine; do not pad. Minimal daily = metadata + brief operator state + Live threads OPEN.
#### 4g. Move originals to {tag}/_legacy-pre-v2.1/
mkdir -p "$tag_dir/_legacy-pre-v2.1"
for orig in $ORIGINAL_FILES; do
mv "$orig" "$tag_dir/_legacy-pre-v2.1/"
doneOriginals never deleted. Operator can /bro clean-legacy later (manual command, not auto).
#### 4h. Apply UUID postfix to old tag name
If old tag has no .session.json marker (legacy):
sha256(tag_name + earliest_file_creation_time).hex[:6]mv "$tag_dir" "$LEGACY_LOC/${tag}-${postfix}"This unifies all tags under the v2.1 naming convention.
If old tag had a .session.json marker (rare in legacy, common in our v2.0 storage):
_principles.md (consolidated repo-wide)After processing all tags in this storage:
_thread.md candidates → write once in _principles.md)(observed in tags: foo, bar, baz; carried since 2026-04-15)_principles.md from template, populated with consolidated contentecho "═══ VERIFICATION ═══"
echo "Total files before: $(tar tzf $BACKUP | grep -c '\.md$')"
echo "Total files after:"
find "$STORAGE" -name "*.md" -type f | wc -l
# Spot-check: did persistent appear in _principles.md / _thread.md?
echo "_principles.md sections:"
grep "^## " "$STORAGE/_principles.md" 2>/dev/null
# Spot-check: did dailies shrink?
for tag_dir in "$STORAGE"/*/; do
tag=$(basename "$tag_dir")
for daily in "$tag_dir"*.md; do
[ -f "$daily" ] || continue
[[ "$(basename "$daily")" == "_thread.md" ]] && continue
new_size=$(wc -l < "$daily")
legacy=$(ls "$tag_dir/_legacy-pre-v2.1/$(basename "$daily")" 2>/dev/null)
if [ -n "$legacy" ]; then
old_size=$(wc -l < "$legacy")
echo " [$tag/$(basename "$daily")] $old_size → $new_size lines"
fi
done
done═══ MIGRATION COMPLETE ═══
Locations migrated: 3
Tags processed: 14
Files extracted to persistent: 47 sections → _principles.md + _thread.md (× tags)
Daily files rewritten: 35
Originals preserved at: {tag}/_legacy-pre-v2.1/ (35 files, untouched)
Backup: ~/Desktop/_bro-migrate-backup-{ts}.tar.gz
Next steps:
- Open _principles.md and {one}/_thread.md to spot-check classification
- If migration looks correct after a few days → /bro clean-legacy to delete _legacy-pre-v2.1/ folders
- If migration looks wrong → restore from backup: tar xzf backup.tar.gz -C //bro clean-legacy (separate command, only run manually)After operator has reviewed migration and is confident:
# Find all _legacy-pre-v2.1/ folders in this repo's storage
find "$STORAGE" -name "_legacy-pre-v2.1" -type dConfirm with operator:
Found N _legacy-pre-v2.1/ folders. Delete them? Backup at {path} still exists; deletion is irreversible. y / n
Only on y → rm -rf each. Tar backup remains as final fallback.
_principles.md (repo-wide universal)# bro principles — {repo-name}
> Persistent context for this repo. Read on /compact and on first entry to a new chat.
> Updated rarely. Items here apply to ALL work threads in this repo.
## Project descriptor
- name: {project name}
- repo: `{git URL}`
- local path: `{cwd}`
- main branch: `{branch}`
- deploy branch: `{if applicable}`
- first canonical commit: `{if known}`
## Sticky rules (git / code safety / process)
- {Hard rule}
- {Hard rule}
## Working discipline (universal)
[Numbered rules with optional markers like (carried since YYYY-MM-DD), (reinforced YYYY-MM-DD)]
1. **{Rule name}.** {Description, optionally with operator's verbatim quote.}
2. **{Rule name}.** {Description.}
## People in context (cross-thread)
- **Name** — role, relevance across multiple threads{tag}/_thread.md (thread-wide stable)# bro thread context — {tag-with-postfix}
> Stable context for this work topic. Updated rarely. Read after `_principles.md` on /compact.
> Items here apply to THIS thread's work topic.
## Architecture (sticky)
### {Decision name}
- **Chose:** ...
- **Over:** ...
- **Because:** ...
- **Revisit if:** ...
## Design principles (numbered, P0 most important)
- **P0** — {principle}
- **P1** — {principle}
## Shortcut vocabulary
- **Term** — definition. (Origin: who/when introduced)
- **Русский термин** — definition (Russian idioms preserved verbatim)
## Roadmap
| Slice | Focus | Estimated | Status (in daily) |
|---|---|---|---|
| 1 | ... | ... | (see today's daily for current status) |
| 2 | ... | ... | |
## Open architectural items
[Long-term questions, not currently in flight]
1. {Question} *(open since YYYY-MM-DD)*
2. {Question} *(open since YYYY-MM-DD)*
## Known subtleties / gotchas
- {Tribal knowledge that's easy to forget}
## File / module map
[ASCII tree or annotated list]
src/ ├── runtime/ │ └── ... └── cli.ts
- `path/to/file` — relevance, what it does
## References / external resources
- {Path or URL with one-line description}{tag}/{YYYY-MM-DD}.md (daily ephemeral)# bro — {YYYY-MM-DD} / {tag-with-postfix}
- thread: {tag-with-postfix}
- workspace: `{cwd}`
- branch: `{git branch}` (if code work)
- previous bro: `./{prev-date}.md` (if exists in this thread)
- continues from: {if non-trivial gap}
- dominant theme today: {one-line summary}
- read order on /compact:
1. THIS FILE FIRST
2. `../_principles.md`
3. `_thread.md` (if exists)
4. {specific files for current task}
---
## Operator state
[Today's energy, mode, mood markers; what changed from prior sessions]
## Live threads
### OPEN
- {Carried from prior days + new today; explicit status `Pending: ...` / `Waiting on: ...` / `Status: paused`}
### DECIDED — {topic 1}
- {Today's decisions for this topic}
### REJECTED (чтобы не возвращались)
- {Hard do-not-revisit; with rationale}
---
<!-- Optional sections — added when material exists -->
## Что произошло сегодня (Session arc)
[Numbered narrative phases — for sessions with significant flow]
### 1. {Phase name}
[Prose description]
## Roadmap status
- Slice {N}: {status delta from `_thread.md` Roadmap}
## Canon gaps surfaced today
- {Discrepancy between code and product truth, flagged for next sync}
## Investigation state
- Current hypothesis: ...
- Checked (clean): ...
- Checked (found): ...
- Not yet checked: ...
- Next probe: ...
## Outputs this session
- Reports (committed): ...
- Canon (committed): ...
- Project branches/commits: `{commit hash}` — {description}
## Не сделано сегодня
- {Deferred, with reason}
## Meta-insights (today)
- {Reflection — candidate for promotion to `_thread.md` or `_principles.md` next /bro}
## Cross-thread links
- {other thread name in this repo}: {state of that thread today}
## Next steps (post-compact)
1. ...
2. ...{tag}/.session.json (session marker){
"sessionUuid": "9e09887a-c861-4916-a49f-9d0fee261393",
"createdAt": "2026-04-29T05:52:44Z",
"firstMessage": "...first 200 chars of first user message...",
"lastKnownTitle": "Bro Migration Fix",
"titleSource": "custom",
"previousTitles": [
{"source": "ai", "title": "Fix plugin log storage and multiple chat handling"},
{"source": "custom", "title": "Bro Migration Fix"}
]
}Standard markers across all bro files. Use them consistently — they signal status to future-you (or future Claude reading on /compact).
| Marker | Where | Meaning |
|---|---|---|
(sticky) | _principles.md / _thread.md decisions | Append-only; supersede with note, don't rewrite |
(carried since YYYY-MM-DD) | rules / vocabulary | Inherited from prior session, still valid; date = first appearance |
(reinforced YYYY-MM-DD) | rules | Repeated for emphasis on this date |
(new YYYY-MM-DD) | rules / vocabulary / decisions | Added on this date |
(refined YYYY-MM-DD) | vocabulary terms | Definition updated on this date (latest definition wins) |
(consolidated YYYY-MM-DD from N source files) | top of _thread.md after migration | Audit trail of migration |
(без изменений с YYYY-MM-DD) | whole sections in daily | Anti-duplication signal |
(чтобы не возвращались) | REJECTED items | Hard do-not-revisit; saves tokens on relitigation |
(P0) … (PN) | design principles | Priority order |
(WAIT: <reason>) | OPEN items | Blocked on external condition |
(parking-lot) / (deferred) / (YAGNI) | items | Postponed; not actively in flight |
(stress-test #N: clean/found) | verification artifacts | Test pass status |
(prototype-override) | code-vs-canon gaps | Code is ahead of canon, temporarily acceptable |
(canon revision pending) | canon gaps | Needs sync; flagged for owner |
(supersedes prior, dated YYYY-MM-DD) | sticky decisions | Pivot replaces an earlier decision |
(renamed from {old-tag} since YYYY-MM-DD) | top of daily file | Tag was renamed when chat title changed |
(observed in tags: foo, bar; carried since YYYY-MM-DD) | _principles.md rules | Cross-tag deduplication source citation |
The operator may work in any language (typically Russian + English mix). Rules:
## Operator state, ### OPEN, ### REJECTED, ## Architecture).### REJECTED (чтобы не возвращались), ## Live threads (незавершённые разговоры), ## Shortcut vocabulary (выработанные в разговоре термины). Use sparingly, only where it captures a distinct nuance.> "груз отпал", > "эта война нам не нужна".Assistants can't inspect their own context structurally — it's an architectural property of transformer models. The interpretation layer (what the conversation means) is ephemeral. Only input tokens, output tokens, and training weights persist across sessions.
bro externalizes that ephemeral interpretation into text while attention is still active on it. After /compact, reading _principles.md + _thread.md + today's daily file re-hydrates working understanding instead of starting from zero.
Why three tiers, not one file? Because operators don't write everything fresh every day. Project descriptors, git safety rules, architecture decisions, and shortcut vocabulary persist across sessions. Daily logs of "what we decided today" don't. Mixing them in one file means rewriting the persistent material every day (waste) or losing it (worse). Three tiers map onto three real timescales:
_principles.md — repo-lifetime: project descriptor, sticky rules, universal discipline_thread.md — topic-lifetime: architecture, vocabulary, roadmap structure for this work{date}.md — session-lifetime: today's state, decisions, what changedWhy tag from chat title + UUID postfix? v2.0 derived tag from first user message. That works but produces low-quality tags when the first message is "let's do X" or contains stop-words. v2.1 reads the chat title (manual rename via UI takes priority over auto-generated AI title) — what operators see in the sidebar and reason about. UUID postfix guarantees uniqueness without depending on time-of-day heuristics that break for parallel chats started in the same minute.
Why LLM-driven migration? Old logs come in many forms — v1 single-file, v2.0 partial multi-tier, mixed bro/nerve, inconsistent markers. Regex parsers are brittle on this variety. LLM-driven semantic classification reads each file in context, recognizes recurring decisions across files, deduplicates, and writes faithful new-format output. It costs more tokens than regex but loses no nuance — operator's explicit constraint is "на полную глубину без экономии ресурсов".
Formal docs capture products of work (the architecture spec, the API contract, the design system docs). bro captures state of work in flight — what you need to continue, not conclusions you've reached.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.