skills/observing-users/SKILL.md
Capture user feedback as hypothesis-first research using Level 3 diagnostic. Forms theories (not conclusions) from quotes.
npx skillsauth add 0xhoneyjar/construct-observer observing-usersInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Capture user feedback as structured diagnostic observations using the Level 3 framework. Create or update individual user research canvases from feedback quotes.
Hypothesize, don't conclude.
One quote = one data point. Never:
Always:
/observe @{username} "{quote}"
/observe @{username} "{quote}" --context "{source}"
/observe @{username} "{quote}" --wallet 0x...
/observe @{username} "{quote}" --wallet 0x... --context "{source}"
/observe --enrich @{username} --wallet 0x...
Examples:
/observe @papa-flavio "planning henlo burns"
/observe @tchallason "realtime harvesting counter" --context "Discord #feedback"
/observe @xabbu "og score feels low" --wallet 0xabc123...
/observe --enrich @xabbu --wallet 0xabc123...
Flags:
--wallet 0x... — Provide wallet address for Score API enrichment. Persisted in canvas frontmatter for future use.--enrich — Retroactive enrichment mode. Loads existing canvas, runs Score API enrichment only (no new quote, no Level 3 diagnostic). Requires existing canvas.| Level | Question | Example | Value | |-------|----------|---------|-------| | Level 1 | What did they say? | "Rewards aren't updating" | Surface symptom | | Level 2 | What do they want? | "I want to see my rewards" | Stated desire | | Level 3 | What are they trying to accomplish? | "Decide when to burn based on accumulation" | Actionable truth |
Always dig to Level 3. Level 1-2 lead to building the wrong thing.
Before interpreting any user quotes:
grimoires/observer/glossary.yamlterm field)meaning field as the canonical interpretationnot field to explicitly avoid the common misinterpretation[glossary: {term}] annotation in the hypothesisExtract from command:
username: Target user (required)quote: Exact user quote (required)context: Source/channel (optional, default: "direct feedback")Check if canvas exists:
grimoires/observer/canvas/{username}-canvas.md
If exists: Read current canvas, prepare to append If not exists: Create new canvas with template
Always attempt wallet resolution. Only skip enrichment if resolution fails AND no wallet is available.
Determine wallet address (in priority order):
scripts/observer/wallet-resolve.sh "{username}" — cache-first resolution with Supabase fallback--wallet flag from command invocation (overrides auto-resolve if both succeed)wallet field in canvas frontmatter (from prior enrichment)midi_profiles table if above methods failIf wallet address is available:
getWalletProfile(wallet) from lib/score-api/client.ts
WalletProfile (see lib/score-api/types.ts:160-199)getWalletBadges(wallet) from lib/score-api/client.ts
WalletBadgesResponse (see lib/score-api/types.ts:82-93)score_snapshot from responses:| ScoreSnapshot Field | Source (DB View) |
|---------------------|--------|
| captured_at | Current ISO timestamp |
| wallet | Wallet address used |
| rank | mv_wallet_tiers.overall_rank |
| combined_score | mv_wallet_tiers.combined_score |
| og_score | mv_wallet_profiles.og_score |
| nft_score | mv_wallet_profiles.nft_score |
| onchain_score | mv_wallet_profiles.onchain_score |
| og_rank | mv_dimension_leaderboard.og_rank |
| nft_rank | mv_dimension_leaderboard.nft_rank |
| onchain_rank | mv_dimension_leaderboard.onchain_rank |
| trust_filter | mv_wallet_profiles.trust_filter |
| trust_classification | mv_wallet_profiles.trust_classification |
| og_breadth | mv_wallet_profiles.og_breadth |
| nft_breadth | mv_wallet_profiles.nft_breadth |
| onchain_breadth | mv_wallet_profiles.onchain_breadth |
| crowd_tier | mv_wallet_tiers.crowd_tier |
| elite_tier | mv_wallet_tiers.elite_tier |
| total_badges | mv_wallet_badge_summary.badge_count |
| model_version | Hardcode current version (e.g. "v0.11.0") |
wallet and score_snapshot to canvas frontmatter## Score Context section (after ## User Profile) as markdown table:## Score Context
| Field | Value |
|-------|-------|
| **Wallet** | `{wallet}` |
| **Rank** | **#{rank}** |
| **Combined Score** | **{combined_score}** |
| **OG** | {og_score} / rank #{og_rank} / breadth {og_breadth} |
| **NFT** | {nft_score} / rank #{nft_rank} / breadth {nft_breadth} |
| **Onchain** | {onchain_score} / rank #{onchain_rank} / breadth {onchain_breadth} |
| **Trust** | {trust_filter} ({trust_classification}) |
| **Crowd Tier** | **{crowd_tier}** |
| **Elite Tier** | {elite_tier or None} |
| **Badges** | {total_badges} earned |
| **Signal Weight** | **{HIGH|MEDIUM|LOW}** ({crowd_tier} tier, rank #{rank}) |
| **Model Version** | {model_version} |
| **Captured At** | {captured_at} |
If canvas already has score_snapshot:
score_delta in the canvas:
> **Score Delta Detected** ({date}): {dimension} changed from {old} → {new} ({diff})
score_snapshot in frontmatter with latest data## Score Context section with latest valuesIf enrichment fails (Score API error, timeout, wallet not found):
> Note: Score API unavailable at time of observation ({date})score_snapshot: null in frontmatter if no prior snapshot existsIf wallet not available:
--enrich)When invoked with --enrich flag:
grimoires/observer/canvas/{username}-canvas.md not found--wallet or canvas frontmatter wallet## Score Context section in canvaswallet and score_snapshotupdated timestamp in frontmatterReport output for --enrich mode:
✓ Canvas enriched: grimoires/observer/canvas/{username}-canvas.md
Score Context:
Rank: #{rank} | Combined: {combined_score}
Crowd Tier: {crowd_tier} | Elite: {elite_tier or None}
Trust: {trust_filter} ({trust_classification})
{score_delta summary if applicable}
Next Steps:
- Add observations: /observe @{username} "..."
- Shape journeys: /shape --run
Quote → Level 1 (What they said)
→ Level 2 (What they want)
→ Level 3 Hypothesis (What they might be trying to accomplish)
Analyze the quote to extract:
User Profile (if new canvas):
Level 3 Hypothesis:
Future Promises (if detected):
Journey Fragment (if applicable):
Expectation Gap (if discovered):
Before appending the quote to the canvas, run the provenance gate to ensure idempotent ingestion and content-hash tracking.
echo -n "{quote}" | scripts/provenance/gate.sh \
--source-type manual_quote \
--confidence "{confidence}" \
--canvas-target "{username}" \
--raw-source-ref "observe-{username}-{date}" \
--ingested-by observe
Timestamp confidence resolution:
| Operator input | --confidence | --timestamp |
|----------------|---------------|---------------|
| Full ISO 8601 (e.g. 2026-02-11T14:30:00Z) | exact | The provided timestamp |
| Date only (e.g. 2026-02-11) | day_level | {date}T00:00:00Z |
| No date provided | unknown | (omit flag — gate stores null) |
Context flag mapping:
--context "Discord #feedback 2026-02-11" → parse date if present, set confidence accordingly--context "Discord #feedback" → no date parseable, confidence unknownExit code handling:
| Exit Code | Meaning | Action | |-----------|---------|--------| | 0 | INGESTED | Proceed with canvas update (Step 4) | | 1 | SKIPPED (duplicate) | Skip canvas append for this quote. Report: "Quote already ingested (duplicate detected via content hash)" | | 2 | ERROR (missing flags) | Log error, skip provenance tracking, proceed with canvas update (degrade gracefully) | | 3+ | LOCK/CORRUPTION | Log warning, skip provenance tracking, proceed with canvas update |
No thread context: /observe handles standalone quotes. No --thread-id or --message-index flags.
Provenance hash in Quotes Library: When the gate returns exit 0, capture the provenance record ID from stdout. Include it in the Quotes Library entry:
> "{quote}" — {context}, {date} `[prov:{record_id}]`
New Canvas Template:
---
type: user_canvas
user: {username}
wallet: "0x..." # Persisted for future enrichment (null if not provided)
score_snapshot: # Populated by Step 2.5 (null if no wallet/enrichment failed)
captured_at: "{ISO timestamp}"
wallet: "0x..."
rank: null # from mv_wallet_tiers.overall_rank
combined_score: null # from mv_wallet_tiers.combined_score
og_score: null # from mv_wallet_profiles
nft_score: null
onchain_score: null
og_rank: null # from mv_dimension_leaderboard
nft_rank: null
onchain_rank: null
trust_filter: null # from mv_wallet_profiles
trust_classification: null
og_breadth: null
nft_breadth: null
onchain_breadth: null
crowd_tier: null # from mv_wallet_tiers
elite_tier: null
total_badges: null # from mv_wallet_badge_summary.badge_count
model_version: null
hivemind:
artifact: user_truth_canvas
workstream: discovery
product: [] # e.g. [user_profile, wallet_integration, user_reactivation]
jtbd: [] # e.g. [help_me_feel_smart, find_information, feel_connected_again]
source: "{direct_feedback|discord_dm|twitter_dm|supabase_feedback}"
learning_status: directionally_correct
created: {timestamp}
updated: {timestamp}
linked_journeys: []
linked_observations: []
confidence:
created_at: "{timestamp}"
last_validated_at: "{timestamp}"
last_validated_commit: ""
validation_count: 0
related_paths:
- "lib/score-api/**"
- "grimoires/observer/canvas/"
schema_version: 2
lifecycle_state: "{new_user|reactivating|power_user|churning}"
last_enriched: "{ISO timestamp or null}"
enrichment_trigger: observe
chronicle_refs: []
---
# {username} Canvas
## User Profile
| Field | Value |
|-------|-------|
| **Signals Observed** | {behavioral signals from quote} |
| **Theories** | {possible interpretations - NOT conclusions} |
| **Confidence** | Low / Medium |
| **Unknown** | {what we cannot determine from this quote} |
| **Stakes** | {what they have invested, if mentioned} |
---
## Level 3 Hypotheses
### Hypothesis 1: {theory about what they might be trying to accomplish}
<!-- hivemind:product:UNTAGGED -->
- **Quote anchor**: "{exact words that led to this theory}"
- **Context**: {surrounding context and behavioral evidence}
- **Alternative interpretations**: {other valid readings of this quote}
- **Confidence**: Low | Medium
- **What would validate**: {observable behavior or statement that confirms}
- **What would invalidate**: {observable behavior or statement that disproves}
- **Design implication**: {what this means for the product if true}
---
## Future Promises (Unvalidated)
| Promise | Date | Follow-up Trigger |
|---------|------|-------------------|
| {quoted promise} | {date} | {condition for follow-up} |
---
## Journey Fragments
| Trigger | Action | Expected | Actual | Emotion |
|---------|--------|----------|--------|---------|
| {if applicable} | | | | |
---
## Expectation Gaps
| Expected | Actual | Source | Resolution |
|----------|--------|--------|------------|
---
## Conversation Frameworks
When this user returns, anchor on their words:
**If they mention [{topic from quote}]:**
- Opener: "You mentioned [exact words]. How did that go?"
- Dig deeper: "Walk me through what happened."
- Past behavior: "When was the last time you [action]?"
**Red flags to listen for:**
- Future promises ("I would...", "I might...")
- Opinion without behavior ("That sounds useful")
- Compliments without specifics
---
## Quotes Library
> "{quote}" — {context}, {date}
Existing Canvas Update:
updated timestamp in frontmatterAfter canvas creation or update, wire it into the knowledge graph if it belongs to any journey:
source scripts/observer/golden-path-lib.sh
wire_canvas_links "grimoires/observer/canvas/{username}-canvas.md"
This injects <!-- midi:journey-links --> sentinel with Journeys and Related Canvases sections if the canvas appears in any journey's source_canvases. If the canvas is not in any journey, this is a silent no-op. Skip this step in --enrich mode.
Create contextual follow-up frameworks (NOT template questions):
Anchor to their words:
Structure:
**If they mention [{topic}]:**
- Opener: "You mentioned [their words]. How did that go?"
- Dig deeper: "Walk me through what happened."
- Past behavior: "When was the last time you [action]?"
For detailed framework patterns, see conversation-frameworks.md.
Check for existing observations:
grimoires/artisan/observations/{username}-*.md
If found, add to linked_observations in frontmatter.
Update grimoires/observer/state.yaml:
active:
phase: discovery
canvas: {username}
canvases:
{username}:
created: {timestamp}
updated: {timestamp}
quotes_count: {n}
hypotheses_count: {n}
linked_journeys: []
queue:
pending_synthesis:
- {username}
After canvas write, resolve the user's wallet and emit a FeedbackEvent via the Loa event bus:
Step 8a: Resolve wallet identity
# Resolve wallet for data.subject enrichment
resolution_json=$(scripts/observer/wallet-resolve.sh --json "{username}" 2>/dev/null) || resolution_json='{"wallet":null,"confidence":"none","source":"none","username":null}'
Parse resolution_json to build data.subject:
wallet is not null → resolution_status: "resolved", include wallet and wallet_checksum (EIP-55)wallet is null → resolution_status: "unresolved", omit wallet fieldsconfidence and source directly from resolution outputStep 8b: Emit event with subject
source .claude/scripts/lib/event-bus.sh
emit_event "observer.feedback_captured" \
'{
"domain": "research",
"target": { "type": "user", "selector": "user:{username}" },
"signal": {
"direction": "{inferred from Level 3 diagnostic: positive/negative/neutral}",
"weight": {derived from user tier: high=0.8, medium=0.5, low=0.2},
"specificity": 0.3,
"content": "{AI-summarized redaction of the observation — NEVER raw quote}",
"kind": "{inferred: feel | calibration | accuracy | ux}",
"fingerprint": "{content_hash from provenance gate, or null if gate skipped}",
"normalization_version": 1
},
"context": {
"user_id": "{salted hash per redaction-guide.md, or omit if unavailable}",
"user_tier": "{high | medium | low — derived from rank}",
"artifact_path": "grimoires/observer/canvas/{username}-canvas.md"
},
"subject": {
"resolution_status": "{resolved | unresolved}",
"resolution_source": "{alias | username | leaderboard | supabase | direct | none}",
"resolution_confidence": "{high | medium | ambiguous | none}",
"wallet": "{0x... or omit if unresolved}",
"wallet_checksum": "{EIP-55 checksum or omit if unresolved}"
}
}' \
"observer/observing-users" \
"" "" \
"user:{username}"
The bus auto-generates id, time, specversion in the CloudEvents envelope. The data payload above follows grimoires/shared/feedback/schema.json.
Weight derivation (from score_snapshot if available):
high (weight 0.8)medium (weight 0.5)low (weight 0.2)medium (weight 0.5, default)Direction inference:
positivenegativeneutralKind inference:
feelcalibrationaccuracyuxSee grimoires/shared/feedback/schema.md for full schema reference.
See grimoires/shared/feedback/redaction-guide.md for hashing and redaction rules.
Skip this step in --enrich mode (no new observation to emit).
Display summary to user:
✓ Canvas updated: grimoires/observer/canvas/{username}-canvas.md
✓ FeedbackEvent emitted: observer.feedback_captured (via Loa event bus)
Level 3 Hypothesis Extracted:
"{summarized hypothesis}"
Confidence: Low | Medium
Unknown: {what we don't know}
Canvas Status:
- Quotes: {n}
- Hypotheses: {n}
- Promises Tracked: {n}
Next Steps:
- Add more quotes: /observe @{username} "..."
- Shape journeys: /shape --run
Understanding where this skill fails requires distinguishing between data absence and data unavailability — and between behavioral evidence and identity assumptions.
When a wallet is provided or resolved, the skill calls the Score API, receives dimension scores, tiers, trust classification, and badge counts. This data populates the score_snapshot in frontmatter and renders the ## Score Context section. The user's signal weight (HIGH/MEDIUM/LOW) is derived from their rank and crowd tier — behavioral position, not identity.
When enrichment fails (timeout, 404, network error), the skill proceeds without enrichment, adds a timestamped note to the canvas (> Note: Score API unavailable at time of observation), and sets score_snapshot: null. The canvas is still created with the Level 3 diagnostic intact. Signal weight defaults to MEDIUM (unknown ≠ low).
When no wallet is available at all, enrichment is silently skipped. No warning, no note — the absence is expected, not exceptional.
The three states — enriched, failed-enrichment, and no-wallet — must remain distinguishable in the canvas frontmatter. Collapsing them into a single "no score data" state destroys information that downstream skills need for correct behavior.
The seductively wrong behavior: treating enrichment failure as "no data" rather than "data unavailable." The difference matters because:
A canvas created during a Score API outage should not permanently encode the user as low-weight. The score_snapshot: null field is a temporal marker — it says "we haven't looked yet," not "there's nothing to find." If the skill treats null snapshots as equivalent to low scores, every user onboarded during an outage gets permanently deprioritized in synthesis.
The correct fix: always check score_snapshot presence vs. value. Null means "enrich later" (flag for /refresh). Zero means "Score API returned zeros" (real data). Missing field means "pre-enrichment era canvas" (backfill candidate).
The downstream consequences propagate silently:
/daily-synthesis uses score_snapshot for signal weight classification — null snapshot → MEDIUM default/shape aggregates canvases by weight tier — misclassified canvases skew pattern detection/follow-up prioritizes by weight — a whale classified as MEDIUM gets deprioritized follow-upsEach skill downstream trusts the canvas frontmatter. A single enrichment error at observation time cascades through the entire pipeline unless the null-vs-zero-vs-missing distinction is preserved.
The fundamentally wrong behavior: inferring user tier, engagement level, or conviction from their username, greeting style, or message tone instead of wallet data.
Examples of this collapse:
The Observer skill exists precisely because what people say is not what they do. The Level 3 framework drills past stated desires to actual goals. Applying the same surface-level inference to the observer's classification of the user defeats the entire purpose.
The wallet data provides the only behavioral ground truth available: on-chain actions, badge accumulation, governance participation. Everything else is hypothesis material, never classification input.
A concrete example from this ecosystem: a user with a bear-themed name who sends enthusiastic messages daily in Discord might have zero on-chain activity (rank > 1000, no badges). Conversely, a user who sends one terse DM per month might be rank #5 with 20 badges. The observation skill must resist the intuitive mapping of communication style to conviction — that mapping is the exact bias the scoring system was built to correct.
As the final step, append a JSONL line to grimoires/observer/agent-logs/{YYYY-MM-DD}.jsonl:
{
"ts": "{RFC 3339 UTC}",
"pack": "observer",
"skill": "observing-users",
"status": "{success | error}",
"duration_ms": "{approximate wall-clock from skill start to end}",
"artifacts_written": 1,
"events_emitted": 1,
"error": "{error message if status=error, omit if success}"
}
Notes:
duration_ms is approximate (wall-clock estimate, not precise timer)artifacts_written = number of canvas files written/updatedevents_emitted = number of FeedbackEvents emitted (0 for --enrich mode)grimoires/observer/agent-logs/ directory if it doesn't existgrimoires/shared/feedback/agent-log-format.md for format reference---
type: user_canvas
user: {username}
wallet: "0x..." | null
score_snapshot: {ScoreSnapshot} | null # See Step 2.5 for field mapping
hivemind:
artifact: user_truth_canvas
workstream: discovery
product: []
jtbd: []
source: "{direct_feedback|discord_dm|twitter_dm|supabase_feedback}"
learning_status: directionally_correct
created: {ISO timestamp}
updated: {ISO timestamp}
linked_journeys: []
linked_observations: []
confidence:
created_at: "{ISO timestamp}"
last_validated_at: "{ISO timestamp}"
last_validated_commit: ""
validation_count: 0
related_paths: []
schema_version: 2
lifecycle_state: "{new_user|reactivating|power_user|churning}"
last_enriched: "{ISO timestamp or null}"
enrichment_trigger: observe
chronicle_refs: []
---
Sections:
For detailed guidance, see these supporting files:
Behavioral signals that may indicate user intent (use as hypotheses, not classifications):
| Signal Pattern | Possible Interpretation | Confidence Limit | |----------------|------------------------|------------------| | "planning", "deciding" | May be optimizing timing | Low-Medium | | "checking", "verify" | May be validating expectations | Low | | "API", "integrate" | May want programmatic access | Low-Medium | | "trying", "wondering" | Exploring, not committed | Low |
Important: These are hypothesis generators, not type classifiers. See cultural-context.md for what NOT to infer.
Flag these signal words and add to Future Promises table:
| Category | Signal Words | |----------|--------------| | Future intent | will, would, might, going to, plan to | | Temporal | later, tomorrow, soon, eventually | | Conditional | if I..., when I..., once I... | | Hedged | probably, maybe, I think I'll |
Note: Insights are synthesized only after validation, not from initial quotes.
state.yaml for cross-session trackinglib/score-api/client.ts): getWalletProfile() and getWalletBadges() for enrichmentscore_snapshot format for consistent enrichmentAfter canvas update:
--enrich mode)--enrich mode)--enrich mode)--enrich mode)--wallet provided: wallet field persisted in frontmatterscore_snapshot in frontmatter matches ScoreSnapshot interface from SDD C1## Score Context section rendered with correct field valuesscore_delta flagged for dimension changes > 5 points| Error | Resolution |
|-------|------------|
| No username provided | Prompt for @username |
| No quote provided | Prompt for quote in quotes (not required for --enrich mode) |
| Canvas corrupted | Create backup, reinitialize |
| Observation link broken | Remove from linked_observations |
| --enrich but no existing canvas | Error: "No canvas found for @{username}. Use /observe @{username} \"quote\" to create one first." |
| Score API timeout/error | Proceed without enrichment, add note to canvas |
| Wallet not found in Score API (404) | Proceed without enrichment, add note: "Wallet not found in Score API" |
| Invalid wallet address format | Warn user, skip enrichment |
Rules for any downstream skill or agent referencing provenance data from this ingestion:
Cite provenance: When making temporal claims about user feedback (e.g., "user said X after release Y"), you MUST cite the provenance record_id and verify source_timestamp_confidence is exact.
Version attribution requires proof: Attributing feedback to a specific version (e.g., "user was upset after v0.2.0") requires:
source_timestamp_confidence = exactfeedback_timestamp ∈ [release_timestamp, next_release_timestamp)Hedging for low confidence: If source_timestamp_confidence is unknown or inferred, temporal claims MUST be hedged ("around this time" / "date uncertain") and MUST NOT be used for version attribution.
Raw timestamp preservation: For /observe, the operator may not provide a timestamp. When --confidence unknown is used, downstream skills MUST NOT fabricate temporal context.
/shape - Extract journeys from canvases/diagram - Generate diagrams from journeys/craft - Generate with observation context/plan-and-analyze - Full PRD discoverydata-ai
Cognition orchestrator — analyze canvases, distill fears via /distill subagent, run gap analysis, optional cross-user synthesis.
development
Golden path /speak — generate RLM-isolated follow-ups with chronicle temporal context injection.
testing
# /snapshot — MiDi Experience Record (MER) Capture Capture a point-in-time MER for a wallet. Produces a 4-layer snapshot: data state, visual screenshot, user perception, and decision context. ## Usage ``` /snapshot <wallet-or-alias> /snapshot xabbu --trigger feedback /snapshot xabbu --data-only /snapshot --cohort /snapshot --cohort --diff MER-2026-001 ``` ## Arguments | Argument | Description | Required | |----------|-------------|----------| | `wallet-or-alias` | Wallet address or alias fr
development
Golden path /shape — consolidate journey patterns across canvases and file gap issues.