一键导入
snapshotting
Capture point-in-time MER (MiDi Experience Record) for a wallet — data state, visual screenshot, perception context, and decision metadata.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Capture point-in-time MER (MiDi Experience Record) for a wallet — data state, visual screenshot, perception context, and decision metadata.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Compare user expectations (UTCs) with code reality to identify gaps. Use when you need to understand discrepancies between what users expect and what code actually does.
Batch scan canvases for GAP sections, deduplicate, route to correct repos, draft issue bodies, and file confirmed issues.
Parallel multi-user processing using Claude Code native teams. Leader spawns workers per user, each runs /ingest-dm, leader aggregates for cross-canvas patterns.
Validate gap hypotheses by manually simulating features for individual users and measuring commitment, not satisfaction.
Automated end-to-end feedback pipeline that pulls new Supabase entries, enriches, classifies, routes to canvases, and generates a synthesis report.
Show what changed since last validation for a specific artifact. Computes confidence without writing to frontmatter.
| name | snapshotting |
| description | Capture point-in-time MER (MiDi Experience Record) for a wallet — data state, visual screenshot, perception context, and decision metadata. |
| user-invocable | true |
| allowed-tools | Read, Write, Glob, Grep, Edit, Bash |
Capture a point-in-time MER for a wallet. Produces a 4-layer snapshot: data state, visual screenshot, user perception, and decision context.
/snapshot <wallet-or-alias>
/snapshot xabbu --trigger feedback
/snapshot xabbu --data-only
/snapshot --cohort
/snapshot --cohort --diff MER-2026-001
| Argument | Description | Required |
|---|---|---|
wallet-or-alias | Wallet address or alias from wallets.yaml | Yes (unless --cohort) |
--trigger | Trigger type: feedback, model-bump, issue-creation, manual, ecosystem-event | No (default: manual) |
--data-only | Suppress visual capture (data + perception only) | No |
--cohort | Process all wallets from wallets.yaml | No |
--diff MER-ID | Compare against baseline MER | No (P3) |
Parse the invocation arguments:
wallet_or_alias from first positional argument--trigger value (default: manual)--data-only, --cohort, --diff flagsIf --cohort is specified:
grimoires/keeper/wallets.yaml# --cohort mode: iterate all tracked wallets
if [[ "$cohort_mode" == "true" ]]; then
wallets=$(yq -r '.wallets | keys[]' grimoires/keeper/wallets.yaml 2>/dev/null || echo "")
if [[ -z "$wallets" ]]; then
echo "ERROR: No wallets found in grimoires/keeper/wallets.yaml" >&2
exit 1
fi
total=0
created=0
skipped_low=0
for alias in $wallets; do
total=$((total + 1))
echo "Processing wallet $total: $alias..." >&2
# Execute Steps 2-12 for this wallet
# wallet_or_alias="$alias"
# trigger="${trigger:-manual}" (from --trigger arg or default)
# data_only="${data_only:-false}"
#
# Step 5 will exit early for LOW wallets — catch and count
# If MER created: created++
# If skipped (LOW): skipped_low++
done
echo "" >&2
echo "Cohort snapshot complete:" >&2
echo " $created MERs created for $total wallets ($skipped_low skipped as LOW)" >&2
exit 0
fi
If --diff is specified:
grimoires/keeper/timeline/{MER-ID}.md# --diff mode: compare current state against baseline MER
if [[ -n "$diff_baseline" ]]; then
baseline_file="grimoires/keeper/timeline/${diff_baseline}.md"
if [[ ! -f "$baseline_file" ]]; then
echo "ERROR: Baseline MER not found: $baseline_file" >&2
exit 1
fi
# Extract baseline data from the body's Data State markdown table
# Format: "| Metric | Value |" — extract the Value column
extract_data_state() {
local file="$1" metric="$2"
awk -v m="$metric" '/## Data State/,/^$/{if($0 ~ "\\| "m" \\|"){split($0,a,"|"); gsub(/^[ \t]+|[ \t]+$/,"",a[3]); print a[3]}}' "$file"
}
baseline_combined=$(extract_data_state "$baseline_file" "Combined Score")
baseline_og=$(extract_data_state "$baseline_file" "OG Score")
baseline_nft=$(extract_data_state "$baseline_file" "NFT Score")
baseline_onchain=$(extract_data_state "$baseline_file" "Onchain Score")
baseline_rank=$(extract_data_state "$baseline_file" "Overall Rank")
baseline_crowd=$(extract_data_state "$baseline_file" "Crowd Tier")
baseline_elite=$(extract_data_state "$baseline_file" "Elite Tier")
# Screenshot URL is in frontmatter: wallets[0].visual_snapshots.profile
baseline_screenshot=$(awk '/visual_snapshots:/,/profile:/{if(/profile:/){gsub(/.*profile: */,""); gsub(/"/,""); print}}' "$baseline_file")
fi
After Step 4, the diff comparison table is generated and included in the MER body (after Data State, before Visual Evidence):
## Model Diff: vs {baseline_mer_id}
| Metric | Baseline | Current | Delta |
|--------|----------|---------|-------|
| Combined Score | {baseline} | {current} | {+/-delta} |
| OG Score | {baseline} | {current} | {+/-delta} |
| NFT Score | {baseline} | {current} | {+/-delta} |
| Onchain Score | {baseline} | {current} | {+/-delta} |
| Overall Rank | #{baseline} | #{current} | {+/-delta} |
| Crowd Tier | {baseline} | {current} | {changed/unchanged} |
| Elite Tier | {baseline} | {current} | {changed/unchanged} |
If both MERs have screenshots, include side-by-side visual comparison:
### Visual Comparison
| Baseline ({baseline_mer_id}) | Current ({current_mer_id}) |
|------------------------------|---------------------------|
|  |  |
If either MER lacks a screenshot, show text-only comparison with a note.
When used with --cohort, the diff runs per-wallet: each wallet's current state is compared against the same wallet's entry in the baseline MER set. Wallets not present in the baseline are marked as "new (no baseline)".
wallet=$(scripts/observer/wallet-resolve.sh "$wallet_or_alias")
Extract the alias for use in filenames. If wallet-resolve.sh fails:
mer_id=$(scripts/observer/generate-mer-id.sh)
If generate-mer-id.sh fails (lock timeout):
data_json=$(scripts/observer/score-api-query.sh profile "$wallet" --format snapshot)
Store JSON sidecar for archival:
mkdir -p "grimoires/keeper/timeline/snapshots/${mer_id}"
echo "$data_json" > "grimoires/keeper/timeline/snapshots/${mer_id}/${alias}-data.json"
Extract fields for the MER Data State table:
combined_score, og_score, nft_score, onchain_scoreoverall_rank, crowd_tier_display, elite_tier_displayDegradation: If Score API is unavailable (exit 1), set all data fields to "unavailable" and continue. The MER is still valid with empty data state.
Determine signal weight from the data:
overall_rank from data responseClassification rules:
If Score API was unavailable in Step 4:
Important: --data-only suppresses visual capture only. LOW wallets never produce a MER regardless of flags.
For HIGH-weight wallets only (unless --data-only flag is set):
has_visual=false
capture_env='{"environment":"unavailable"}'
if [[ "$signal_weight" == "HIGH" && "$data_only" != "true" ]]; then
if metadata=$(scripts/observer/capture-screenshot.sh "$wallet" "$alias" "/tmp/${mer_id}-profile.png" 2>&1); then
capture_env=$metadata
has_visual=true
else
echo "WARNING: Screenshot capture failed — continuing with data-only MER" >&2
fi
fi
The script uses QA fixture auth bypass (sf-qa-effective-address localStorage injection) to capture the wallet's profile page without wallet connection. Accepts optional --base-url for non-localhost environments.
Degradation: agent-browser failure → data-only MER with warning. Never halt pipeline. MEDIUM-weight wallets always skip visual capture.
If capture succeeded in Step 6, upload to Supabase Storage:
screenshot_url=""
if [[ "$has_visual" == "true" ]]; then
if url=$(scripts/observer/upload-snapshot.sh "/tmp/${mer_id}-profile.png" "${mer_id}/${alias}-profile.png" 2>&1); then
screenshot_url="$url"
else
echo "WARNING: Upload failed — local cache retained" >&2
fi
fi
Requires SUPABASE_URL and SUPABASE_STORAGE_TOKEN environment variables. Uses scoped storage token (NEVER service role key).
Degradation: Upload failure → visual_snapshots.profile set to null, local screenshot retained at /tmp/${mer_id}-profile.png. Local cache also at gitignored grimoires/keeper/timeline/snapshots/.
Check if a canvas exists for this wallet:
canvas_path="grimoires/keeper/canvas/${alias}-canvas.md"
If canvas exists:
> blockquotes)If canvas does not exist:
.claude/constructs/packs/observer/templates/mer-template.md{{PLACEHOLDER}} values with actual data:| Placeholder | Source |
|---|---|
{{MER_ID}} | Step 3 output |
{{TITLE}} | Generate from: "{alias} — {trigger} snapshot" |
{{EVENT_DATE}} | Today's date (ISO 8601) |
{{DATE_RECORDED}} | Current timestamp (ISO 8601) |
{{MODEL_VERSION}} | Read from Score API response or "unknown" |
{{OWNER}} | "observer" |
{{TRIGGER}} | From --trigger argument |
{{SIGNAL_WEIGHT}} | Step 5 classification |
{{WALLET_ADDRESS}} | Step 2 resolved address |
{{WALLET_ALIAS}} | Step 2 alias |
{{LOCAL_CACHE_PATH}} | grimoires/keeper/timeline/snapshots/${mer_id}/${alias}-data.json |
{{SCREENSHOT_URL}} | Step 7 URL or null |
{{ERA}} | null (future use) |
{{CORE_CONVICTION}} | From canvas or "To be determined" |
{{CAPTURE_*}} | Step 6 metadata or null |
{{COMBINED_SCORE}}, etc. | Step 4 data fields |
{{CONTEXT_DESCRIPTION}} | Generate brief context sentence |
{{USER_QUOTE}} | Step 8 extracted quote or "No user signals captured" |
{{QUOTE_SOURCE}} | Canvas path or "N/A" |
{{EXPECTED}}, {{ACTUAL}}, {{GAP_TYPE}} | Step 8 perception data or placeholders |
{{CORE_CONVICTION_ANSWER}} | From canvas or "Record only" |
{{TARGET_AUDIENCE}} | Infer from wallet tier or "General participant" |
{{COMMUNITY_PULSE}} | From canvas quotes or "No community signal" |
{{ONE_LESSON}} | "Record only — first snapshot" for initial MERs |
{{STATE_DIAGRAM}} | Simple state node or "stateDiagram-v2\n [*] --> Captured" |
Handle conditional sections:
has_visual == true: render {{#IF_VISUAL}} block, remove {{#IF_NO_VISUAL}} blockhas_visual == false: render {{#IF_NO_VISUAL}} block, remove {{#IF_VISUAL}} blockWrite to temp path:
grimoires/keeper/timeline/.${mer_id}.tmp.md
scripts/observer/validate-mer.sh --instance "grimoires/keeper/timeline/.${mer_id}.tmp.md"
If validation fails, log error and exit (do not commit invalid MER).
Use commit-mer.sh for atomic INDEX.json entry + MER file placement:
final_path=$(scripts/observer/commit-mer.sh \
"$mer_id" \
"$event_date" \
"$title" \
"$trigger" \
"$alias" \
"$signal_weight" \
"grimoires/keeper/timeline/.${mer_id}.tmp.md")
If commit-mer.sh fails:
Emit observer.snapshot_captured after MER write AND INDEX update succeed:
source .claude/scripts/lib/event-bus.sh
# Build layers_captured array dynamically
layers="[\"data\""
if [[ "$has_visual" == "true" ]]; then
layers="${layers},\"visual\""
fi
if [[ -f "$canvas_path" ]]; then
layers="${layers},\"perception\""
fi
layers="${layers}]"
# Build payload
payload=$(cat <<PAYLOAD
{
"target": {"type": "wallet", "selector": "wallet:${wallet_address}"},
"signal": {
"mer_id": "${mer_id}",
"trigger": "${trigger}",
"signal_weight": "${signal_weight}",
"layers_captured": ${layers},
"screenshot_url": ${screenshot_url:+\"$screenshot_url\"}${screenshot_url:-null},
"canvas_exists": $([ -f "$canvas_path" ] && echo true || echo false)
},
"idempotency_key": "${mer_id}:${wallet_address}:${trigger}:${event_date}"
}
PAYLOAD
)
# Emit — failure must not block the pipeline
if emit_event "observer.snapshot_captured" "$payload" "observer/snapshotting" "${correlation_id:-}"; then
echo "Event emitted: observer.snapshot_captured" >&2
else
echo "WARNING: Event emission failed — MER is still valid" >&2
fi
Payload matches snapshot-event.schema.json: target (wallet selector), signal (mer_id, trigger, signal_weight, layers_captured, screenshot_url, canvas_exists), idempotency_key.
layers_captured is built dynamically from actual capture results (e.g., ["data"] when visual skipped, ["data","visual","perception"] when all layers succeed).
event_date in idempotency key is sourced from the MER frontmatter event_date field (ISO 8601 date, set at MER creation time in Step 9).
Degradation: Emission failure → MER still written, just not broadcast. Log warning.
Print completion summary:
MER Created: ${mer_id}
Path: grimoires/keeper/timeline/${mer_id}.md
Wallet: ${alias} (${wallet_address})
Trigger: ${trigger}
Weight: ${signal_weight}
Layers: ${layers_captured}
Visual: ${screenshot_url:-"unavailable"}
Event: skipped (P0)
Where layers_captured is built dynamically:
"data" (unless Score API unavailable)"visual" if has_visual == true"perception" if canvas existsobserver.feedback_capturedRegistered in manifest as consumer group snapshotting with pull delivery. The event bus routes observer.feedback_captured events to this skill.
Define a handler function and pass it to consume_events. The handler receives one CloudEvents JSON object via stdin per invocation.
source .claude/scripts/lib/event-bus.sh
handle_snapshot_trigger() {
local event
read -r event
# === Extract fields ===
# Envelope fields (CloudEvents standard)
local event_source
event_source=$(echo "$event" | jq -r '.source // empty')
local correlation_id
correlation_id=$(echo "$event" | jq -r '.id // empty')
# Data payload fields (per grimoires/shared/feedback/schema.json)
local target_selector
target_selector=$(echo "$event" | jq -r '.data.target.selector')
local user_tier
user_tier=$(echo "$event" | jq -r '.data.context.user_tier // "medium"')
local resolved_wallet
resolved_wallet=$(echo "$event" | jq -r '.data.subject.wallet // empty')
CRITICAL: This is a self-consumption pattern (same pack emits feedback_captured and consumes it). The .source field in the CloudEvents envelope identifies the emitting skill (e.g., "observer/snapshotting").
# MUST check envelope .source to prevent infinite loops
if [[ "$event_source" == "observer/snapshotting" ]]; then
echo "SKIP: Ignoring feedback_captured from snapshotting (loop prevention SKP-007)" >&2
return 0
fi
Feedback events use target.type: "user" with target.selector: "user:{username}" format. If the event has a resolved wallet in subject.wallet, use that directly. Otherwise, extract the username alias for wallet-resolve.sh.
# Prefer resolved wallet from subject (when resolution_status=resolved)
local wallet_or_alias
if [[ -n "$resolved_wallet" ]]; then
wallet_or_alias="$resolved_wallet"
else
# Feedback target uses "user:{username}" format — extract alias
wallet_or_alias=$(echo "$target_selector" | sed 's/^user://')
fi
The feedback schema uses context.user_tier (high/medium/low string derived from rank) rather than an explicit signal weight enum. Map to MER signal weight classification.
# Map user_tier to signal weight (per SDD §10.2 routing table)
local signal_weight
case "$user_tier" in
high) signal_weight="HIGH" ;;
medium) signal_weight="MEDIUM" ;;
low|*) signal_weight="LOW" ;;
esac
Before creating a MER, check if one already exists for this wallet+trigger+date combination.
local event_date
event_date=$(date -u +"%Y-%m-%d")
# Resolve to actual wallet address for idempotency check
local wallet_address
wallet_address=$(scripts/observer/wallet-resolve.sh "$wallet_or_alias" 2>/dev/null || echo "")
if [[ -z "$wallet_address" ]]; then
echo "SKIP: Could not resolve wallet for $wallet_or_alias" >&2
return 0
fi
# Check INDEX.json for existing MER with same wallet + trigger + date
local existing
existing=$(jq -r --arg alias "$wallet_or_alias" --arg date "$event_date" \
'.entries[] | select(.trigger == "feedback") | select(.date == $date) | select(.wallets == $alias)' \
grimoires/keeper/timeline/INDEX.json 2>/dev/null || true)
if [[ -n "$existing" ]]; then
echo "SKIP: MER already exists for ${wallet_or_alias}:feedback:${event_date}" >&2
return 0
fi
case "$signal_weight" in
HIGH)
# Full snapshot with visual capture
echo "Triggering: /snapshot $wallet_or_alias --trigger feedback" >&2
# Execute Steps 2-12 of this SKILL.md with:
# wallet_or_alias="$wallet_or_alias"
# trigger="feedback"
# data_only="false"
# correlation_id="$correlation_id"
;;
MEDIUM)
# Data-only snapshot (no visual capture)
echo "Triggering: /snapshot $wallet_or_alias --trigger feedback --data-only" >&2
# Execute Steps 2-12 with:
# wallet_or_alias="$wallet_or_alias"
# trigger="feedback"
# data_only="true"
# correlation_id="$correlation_id"
;;
LOW|*)
# LOW weight never creates MERs — log and skip
echo "SKIP: LOW weight feedback for $wallet_or_alias (no MER created)" >&2
return 0
;;
esac
}
# Invoke the consumer — processes all pending events from offset
consumed_count=$(consume_events "observer.feedback_captured" handle_snapshot_trigger "snapshotting")
echo "Processed $consumed_count feedback events for snapshot triggers" >&2
| User Tier | Signal Weight | Action | Visual Capture |
|---|---|---|---|
| high | HIGH | /snapshot {wallet} --trigger feedback | Yes |
| medium | MEDIUM | /snapshot {wallet} --trigger feedback --data-only | No |
| low | LOW | Skip (log only) | N/A |
| Dependency | Available | Unavailable | Behavior |
|---|---|---|---|
| Score API | Full data layer | Empty data table + note | MER created, MEDIUM default |
| agent-browser | Screenshots captured | No visual layer | Data-only MER (P1+) |
| Supabase Storage | Screenshots uploaded | Upload failed | Null URL, local cache (P1+) |
| Canvas | Perception layer filled | "No canvas" note | MER without quotes |
| Event bus | Event emitted | No emission | MER still written (P2+) |
Every dependency failure produces a degraded but valid MER. The pipeline never halts.