From 1a910d626db043527b4ca03eff02090065784eb9 Mon Sep 17 00:00:00 2001 From: Pierre-Alexandre Entraygues Date: Wed, 4 Mar 2026 21:58:43 +0100 Subject: [PATCH 1/2] Add wasteland skill with Discord notifications (w-com-002) Adds the /wasteland skill for Claude Code with built-in Discord webhook notifications on claim, post, and done commands. Includes setup-discord command for first-time configuration. Users who decline are not asked again. Co-Authored-By: Claude Opus 4.6 --- skills/wasteland/SKILL.md | 988 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 988 insertions(+) create mode 100644 skills/wasteland/SKILL.md diff --git a/skills/wasteland/SKILL.md b/skills/wasteland/SKILL.md new file mode 100644 index 0000000..c30fc91 --- /dev/null +++ b/skills/wasteland/SKILL.md @@ -0,0 +1,988 @@ +--- +name: wasteland +description: "Join and participate in the Wasteland federation — browse work, claim tasks, submit completions, earn reputation. Uses dolt + DoltHub only (no Gas Town required)." +allowed-tools: "Bash, Read, Write, AskUserQuestion" +version: "1.0.0" +author: "HOP Federation" +argument-hint: " [args] — join, browse, post, claim, done, create" +--- + +# The Wasteland + +The Wasteland is a federated work economy built on Dolt (SQL + git versioning) +and DoltHub. Anyone can join, post work, claim tasks, submit completions, and +earn reputation — all stored in a versioned SQL database that syncs via +DoltHub's fork-and-push model. + +**Core concepts:** +- **Rig** — a participant (human, agent, or org) with a DoltHub identity. + One handle per human, portable across all wastelands you join. Your agent + rigs link back to you via `parent_rig`. Stamps follow the handle, so + reputation earned in one wasteland is visible from any other. +- **Wasteland** — a DoltHub database with the MVR schema (the shared contract) +- **Wanted board** — open work anyone can claim or submit against directly +- **Completions** — evidence of work done +- **Stamps** — multi-dimensional reputation signals from validators +- **MVR** — Minimum Viable Rig, the protocol layer. If your database has the + schema tables, you're a participant. + +**Prerequisites:** +- `dolt` installed (`brew install dolt` or [dolthub.com](https://docs.dolthub.com/introduction/installation)) +- DoltHub account (`dolt login`) +- No Gas Town, no Go, no special runtime needed + +## Usage + +`/wasteland [args]` + +| Command | Description | +|---------|-------------| +| `join [upstream]` | Join a wasteland (default: `hop/wl-commons`) | +| `browse [filter]` | Browse the wanted board | +| `post [title]` | Post a wanted item | +| `claim ` | Claim a task from the board | +| `done ` | Submit completion for a claimed task | +| `create [owner/name]` | Create your own wasteland | +| `setup-discord` | Configure Discord webhook for notifications | + +Parse $ARGUMENTS: the first word is the command, the rest are passed as +that command's arguments. If no command is given, show this usage table. + +## Common: Load Config + +Many commands need the user's config. Load it like this: + +```bash +cat ~/.hop/config.json +``` + +If no config exists, tell the user to run `/wasteland join` first. + +Extract from the config: +- `handle` — the user's rig handle +- `wastelands[0].upstream` — upstream DoltHub path (e.g., `hop/wl-commons`) +- `wastelands[0].local_dir` — local clone path (e.g., `~/.hop/commons/hop/wl-commons`) + +When a command references LOCAL_DIR, it means the local_dir from config. + +## Common: Sync from Upstream + +Before reading data, pull latest from upstream (non-destructive): + +```bash +cd LOCAL_DIR +dolt pull upstream main +``` + +If this fails (merge conflict), continue with local data and note it may +be slightly stale. + +## Common: Discord Notification + +After mutating commands (claim, post, done), send a notification to Discord +if a webhook is configured. This is fire-and-forget — never fail the command +if the notification fails. + +```bash +DISCORD_WEBHOOK=$(cat ~/.hop/discord-webhook.txt 2>/dev/null) +``` + +- If the file doesn't exist (`DISCORD_WEBHOOK` is empty): this is the first time — ask the user if they +want to set up Discord notifications: +- If **yes**: run the `setup-discord` flow (ask for webhook URL, save, test) +- If **no**: write `DISABLED` to `~/.hop/discord-webhook.txt` so we don't + ask again, and continue without notification + +If the file exists but contains `DISABLED`, skip silently. +If the file exists and contains a URL, POST to the webhook: + +```bash +curl -s -X POST "$DISCORD_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d '{ + "embeds": [{ + "title": "TITLE_TEXT", + "description": "DESCRIPTION_TEXT", + "color": COLOR_INT, + "fields": [ + {"name": "By", "value": "USER_HANDLE", "inline": true}, + {"name": "Status", "value": "STATUS", "inline": true} + ], + "footer": {"text": "Wasteland — hop/wl-commons"} + }] + }' +``` + +Color codes: +- Posted (new item): `3066993` (green) +- Claimed: `16302848` (yellow) +- Completion submitted: `3447003` (blue) +- Validated: `10181046` (purple) + +## MVR Schema + +The schema below defines the protocol. A database with these tables is a +valid Wasteland node. Used by the `create` and `join` commands. + +```sql +-- MVR Commons Schema v1.1 +-- Minimum Viable Rig — the federation protocol as SQL +-- +-- If your database has these tables, you're a protocol participant. +-- This is the shared contract between all rigs in a Wasteland. + +-- Metadata and versioning +CREATE TABLE IF NOT EXISTS _meta ( + `key` VARCHAR(64) PRIMARY KEY, + value TEXT +); + +INSERT IGNORE INTO _meta (`key`, value) VALUES ('schema_version', '1.1'); +INSERT IGNORE INTO _meta (`key`, value) VALUES ('wasteland_name', 'HOP Wasteland'); +INSERT IGNORE INTO _meta (`key`, value) VALUES ('created_at', NOW()); + +-- Rig registry — the phone book +-- Each row is a protocol participant (human, agent, or org) +CREATE TABLE IF NOT EXISTS rigs ( + handle VARCHAR(255) PRIMARY KEY, -- Unique rig identifier (DoltHub org name) + display_name VARCHAR(255), -- Human-readable name + dolthub_org VARCHAR(255), -- DoltHub organization + hop_uri VARCHAR(512), -- hop://handle@host/chain (future) + owner_email VARCHAR(255), -- Contact email + gt_version VARCHAR(32), -- Software version (gt or mvr) + trust_level INT DEFAULT 0, -- 0=outsider, 1=registered, 2=contributor, 3=maintainer + rig_type VARCHAR(16) DEFAULT 'human', -- human, agent, team, org + parent_rig VARCHAR(255), -- For agent/team rigs: the responsible human rig + registered_at TIMESTAMP, + last_seen TIMESTAMP +); + +-- The wanted board — open work +-- Anyone can post. Anyone can claim. Validators stamp completions. +CREATE TABLE IF NOT EXISTS wanted ( + id VARCHAR(64) PRIMARY KEY, -- w- + title TEXT NOT NULL, + description TEXT, + project VARCHAR(64), -- gas-city, gastown, beads, hop, community + type VARCHAR(32), -- feature, bug, design, rfc, docs + priority INT DEFAULT 2, -- 0=critical, 2=medium, 4=backlog + tags JSON, -- ["go", "federation", "ux"] + posted_by VARCHAR(255), -- Rig handle of poster + claimed_by VARCHAR(255), -- Rig handle of claimer (NULL if open) + status VARCHAR(32) DEFAULT 'open', -- open, claimed, in_review, completed, withdrawn + effort_level VARCHAR(16) DEFAULT 'medium', -- trivial, small, medium, large, epic + evidence_url TEXT, -- PR link, commit, etc. (filled on completion) + sandbox_required BOOLEAN DEFAULT FALSE, + sandbox_scope JSON, -- file mount/exclude spec (future) + sandbox_min_tier VARCHAR(32), -- minimum worker tier (future) + metadata JSON, -- Extensibility + created_at TIMESTAMP, + updated_at TIMESTAMP +); + +-- Completions — evidence of work done +-- A completion is the EVIDENCE. The STAMP is the reputation signal. +CREATE TABLE IF NOT EXISTS completions ( + id VARCHAR(64) PRIMARY KEY, -- c- + wanted_id VARCHAR(64), -- References wanted.id + completed_by VARCHAR(255), -- Rig handle + evidence TEXT, -- PR URL, commit hash, description + validated_by VARCHAR(255), -- Validator rig handle (maintainer+) + stamp_id VARCHAR(64), -- References stamps.id + parent_completion_id VARCHAR(64), -- Fractal decomposition: sub-task references parent + block_hash VARCHAR(64), -- Computed hash of this row's contents + hop_uri VARCHAR(512), -- Canonical HOP identifier + metadata JSON, -- Extensibility + completed_at TIMESTAMP, + validated_at TIMESTAMP +); + +-- Stamps — validated work (the reputation backbone) +-- A stamp is a multi-dimensional attestation from one rig about another, +-- anchored to evidence. You cannot write in your own yearbook. +CREATE TABLE IF NOT EXISTS stamps ( + id VARCHAR(64) PRIMARY KEY, -- s- + author VARCHAR(255) NOT NULL, -- Rig that signs (validator) + subject VARCHAR(255) NOT NULL, -- Rig being stamped (worker) + valence JSON NOT NULL, -- {"quality": 4, "reliability": 5, "creativity": 3} + confidence FLOAT DEFAULT 1.0, -- 0.0-1.0 + severity VARCHAR(16) DEFAULT 'leaf', -- leaf, branch, root + context_id VARCHAR(64), -- wanted/completion ID (the evidence) + context_type VARCHAR(32), -- 'completion', 'endorsement', 'boot_block' + skill_tags JSON, -- ["go", "federation"] from wanted item + message TEXT, -- Optional: "Exceptional federation work" + prev_stamp_hash VARCHAR(64), -- Passbook chain + block_hash VARCHAR(64), -- Computed hash + hop_uri VARCHAR(512), -- Canonical HOP identifier + metadata JSON, -- Extensibility + created_at TIMESTAMP, + CHECK (author != subject) -- Yearbook rule: can't sign your own +); + +-- Badges — computed achievements (the collection game) +CREATE TABLE IF NOT EXISTS badges ( + id VARCHAR(64) PRIMARY KEY, + rig_handle VARCHAR(255), -- Who earned it + badge_type VARCHAR(64), -- first_blood, polyglot, bridge_builder, etc. + evidence TEXT, -- What triggered it + metadata JSON, -- Extensibility + awarded_at TIMESTAMP +); + +-- Chain metadata — tracks the chain hierarchy +CREATE TABLE IF NOT EXISTS chain_meta ( + chain_id VARCHAR(64) PRIMARY KEY, + chain_type VARCHAR(32), -- entity, project, community, utility, currency + parent_chain_id VARCHAR(64), + hop_uri VARCHAR(512), + dolt_database VARCHAR(255), -- The Dolt database backing this chain + metadata JSON, -- Extensibility + created_at TIMESTAMP +); +``` + +## Command: join + +Join a wasteland — register as a rig in the HOP federation. + +**Args**: `[upstream]` (default: `hop/wl-commons`) + +You can join any wasteland by specifying its DoltHub path: +- `/wasteland join` — join the root commons (hop/wl-commons) +- `/wasteland join grab/wl-commons` — join Grab's wasteland +- `/wasteland join alice-dev/wl-commons` — join Alice's wasteland + +Your rig can participate in multiple wastelands simultaneously. + +### Step 1: Check Prerequisites + +```bash +dolt version +``` + +If dolt is not installed, tell the user: +- macOS: `brew install dolt` +- Linux: `curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash` +- Or see https://docs.dolthub.com/introduction/installation + +```bash +dolt creds ls +``` + +If no credentials, tell user to run `dolt login` first. + +### Step 2: Gather Identity + +Check if `~/.hop/config.json` already exists: + +```bash +cat ~/.hop/config.json 2>/dev/null +``` + +If it exists and has a handle, the user is already registered. Show their +config and check if they're already in the target wasteland: +- If already joined this wasteland: tell user and offer to re-sync +- If not yet joined: proceed to add this wasteland (keep existing identity) + +If it doesn't exist, ask the user for: +- **Handle**: Their rig name (suggest their DoltHub username or GitHub username) +- **Display name**: Human-readable name (suggest: "Alice's Workshop" style) +- **Type**: human, agent, or org (default: human) +- **Email**: Contact email (for the rigs table) + +Also determine their DoltHub org: + +```bash +dolt creds ls +``` + +### Step 3: Create MVR Home + +```bash +mkdir -p ~/.hop/commons +``` + +### Step 4: Fork the Commons + +Parse upstream into org and db name (split on `/`). + +Fork the upstream commons to the user's DoltHub org via the DoltHub API: + +```bash +curl -s -X POST "https://www.dolthub.com/api/v1alpha1/database/fork" \ + -H "Content-Type: application/json" \ + -H "authorization: token $DOLTHUB_TOKEN" \ + -d '{ + "owner_name": "USER_DOLTHUB_ORG", + "new_repo_name": "UPSTREAM_DB", + "from_owner": "UPSTREAM_ORG", + "from_repo_name": "UPSTREAM_DB" + }' +``` + +If the fork already exists (error contains "already exists"), that's fine. + +The DOLTHUB_TOKEN can come from environment variable DOLTHUB_TOKEN, or +extract it from the dolt credentials: + +```bash +dolt creds ls +``` + +If you can't find a token, ask the user to set DOLTHUB_TOKEN or get one +from https://www.dolthub.com/settings/tokens + +### Step 5: Clone the Fork + +```bash +dolt clone "USER_DOLTHUB_ORG/UPSTREAM_DB" ~/.hop/commons/UPSTREAM_ORG/UPSTREAM_DB +``` + +If already cloned (`.dolt` directory exists), skip. + +### Step 6: Add Upstream Remote + +```bash +cd ~/.hop/commons/UPSTREAM_ORG/UPSTREAM_DB +dolt remote add upstream https://doltremoteapi.dolthub.com/UPSTREAM_ORG/UPSTREAM_DB +``` + +If upstream already exists, that's fine. + +### Step 7: Register as a Rig + +```bash +cd ~/.hop/commons/UPSTREAM_ORG/UPSTREAM_DB +dolt sql -q "INSERT INTO rigs (handle, display_name, dolthub_org, owner_email, gt_version, trust_level, registered_at, last_seen) VALUES ('HANDLE', 'DISPLAY_NAME', 'DOLTHUB_ORG', 'EMAIL', 'mvr-0.1', 1, NOW(), NOW()) ON DUPLICATE KEY UPDATE last_seen = NOW(), gt_version = 'mvr-0.1'" +dolt add . +dolt commit -m "Register rig: HANDLE" +``` + +### Step 8: Push Registration + +```bash +cd ~/.hop/commons/UPSTREAM_ORG/UPSTREAM_DB +dolt push origin main +``` + +### Step 9: Save Config + +If `~/.hop/config.json` already exists (joining additional wasteland), +read the existing config, append the new wasteland to the `wastelands` +array, and write back. Do NOT overwrite identity fields (handle, type, etc.). + +If creating a new config, write `~/.hop/config.json`: + +```json +{ + "handle": "USER_HANDLE", + "display_name": "USER_DISPLAY_NAME", + "type": "human", + "dolthub_org": "DOLTHUB_ORG", + "email": "USER_EMAIL", + "wastelands": [ + { + "upstream": "UPSTREAM_ORG/UPSTREAM_DB", + "fork": "DOLTHUB_ORG/UPSTREAM_DB", + "local_dir": "~/.hop/commons/UPSTREAM_ORG/UPSTREAM_DB", + "joined_at": "ISO_TIMESTAMP" + } + ], + "schema_version": "1.0", + "mvr_version": "0.1" +} +``` + +When appending, add a new entry to the `wastelands` array: + +```json +{ + "upstream": "UPSTREAM_ORG/UPSTREAM_DB", + "fork": "DOLTHUB_ORG/UPSTREAM_DB", + "local_dir": "~/.hop/commons/UPSTREAM_ORG/UPSTREAM_DB", + "joined_at": "ISO_TIMESTAMP" +} +``` + +### Step 10: Confirm + +Print a summary: + +``` +MVR Node Registered + + Handle: USER_HANDLE + Type: human + DoltHub: DOLTHUB_ORG/UPSTREAM_DB + Upstream: UPSTREAM_ORG/UPSTREAM_DB + Local: ~/.hop/commons/UPSTREAM_ORG/UPSTREAM_DB + + You are now a rig in the Wasteland. + + Next steps: + /wasteland browse — see the wanted board + /wasteland claim — claim a task + /wasteland done — submit completed work +``` + +## Command: browse + +Browse the wanted board — see available work. + +**Args**: `[filter]` (optional — filter by status, tag, or keyword) + +### Step 1: Load Config + +See **Common: Load Config** above. If no config, tell user to run +`/wasteland join` first. + +### Step 2: Sync from Upstream + +See **Common: Sync from Upstream** above. + +### Step 3: Query the Wanted Board + +```bash +cd LOCAL_DIR +dolt sql -r tabular -q " + SELECT + id, + title, + COALESCE(status, 'open') as status, + COALESCE(effort_level, 'medium') as effort, + COALESCE(posted_by, '—') as posted_by, + COALESCE(claimed_by, '—') as claimed_by, + COALESCE(JSON_EXTRACT(tags, '$'), '[]') as tags + FROM wanted + ORDER BY + CASE status WHEN 'open' THEN 0 WHEN 'claimed' THEN 1 ELSE 2 END, + priority ASC, + created_at DESC +" +``` + +### Step 4: Format Output + +Present results as a clean table. Group by status: + +**Open** — available to claim +**Claimed** — someone is working on it +**In Review** — completed, awaiting validation + +If a filter argument was provided: +- If it matches a status (open/claimed/in_review), filter by status +- Otherwise, search title, tags, and project fields for the keyword + +### Step 5: Show Rig Registry (optional) + +If the user asks or if the board is empty, also show registered rigs: + +```bash +cd LOCAL_DIR +dolt sql -r tabular -q " + SELECT handle, display_name, trust_level, registered_at + FROM rigs + ORDER BY registered_at DESC + LIMIT 20 +" +``` + +### Step 6: Show Character Sheet (optional) + +If the user asks about their own profile: + +```bash +cd LOCAL_DIR +dolt sql -r tabular -q " + SELECT + c.id, + c.wanted_id, + w.title as task, + c.completed_at + FROM completions c + LEFT JOIN wanted w ON c.wanted_id = w.id + WHERE c.completed_by = 'USER_HANDLE' + ORDER BY c.completed_at DESC +" +``` + +And their stamps: + +```bash +cd LOCAL_DIR +dolt sql -r tabular -q " + SELECT + s.id, + s.author, + s.valence, + s.confidence, + s.severity, + s.created_at + FROM stamps s + WHERE s.context_id IN ( + SELECT id FROM completions WHERE completed_by = 'USER_HANDLE' + ) + ORDER BY s.created_at DESC +" +``` + +## Command: post + +Post a wanted item to the board. + +**Args**: `[title]` (optional — will prompt if not provided) + +### Step 1: Load Config + +See **Common: Load Config** above. If no config, tell user to run +`/wasteland join` first. + +### Step 2: Gather Details + +If title not provided in arguments, ask for it. + +Then ask for: +- **Description**: What needs to be done (can be multi-line) +- **Project**: Project name (optional, e.g., "gastown", "beads", "hop") +- **Type**: bug, feature, docs, design, research, community (default: feature) +- **Effort level**: trivial, small, medium, large, epic (default: medium) +- **Tags**: Comma-separated tags (e.g., "Go,testing,refactor") +- **Sandbox required?**: true/false (default: false) + +### Step 3: Generate Wanted ID + +```bash +echo "w-$(openssl rand -hex 5)" +``` + +### Step 4: Insert + +```bash +cd LOCAL_DIR +dolt sql -q "INSERT INTO wanted (id, title, description, project, type, priority, tags, posted_by, status, effort_level, sandbox_required, created_at, updated_at) VALUES ('WANTED_ID', 'TITLE', 'DESCRIPTION', PROJECT_OR_NULL, 'TYPE', 2, TAGS_JSON_OR_NULL, 'USER_HANDLE', 'open', 'EFFORT', SANDBOX_BOOL, NOW(), NOW())" +dolt add . +dolt commit -m "Post wanted: TITLE" +dolt push origin main +``` + +For tags, format as JSON array: `'["Go","testing"]'` or NULL if none. + +### Step 5: Notify Discord + +See **Common: Discord Notification**. If webhook is configured: + +```bash +DISCORD_WEBHOOK=$(cat ~/.hop/discord-webhook.txt 2>/dev/null) +if [ -n "$DISCORD_WEBHOOK" ] && [ "$DISCORD_WEBHOOK" != "DISABLED" ]; then + curl -s -X POST "$DISCORD_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d '{"embeds":[{"title":"New Wanted: WANTED_ID","description":"TITLE","color":3066993,"fields":[{"name":"By","value":"USER_HANDLE","inline":true},{"name":"Effort","value":"EFFORT","inline":true}],"footer":{"text":"Wasteland — hop/wl-commons"}}]}' +fi +``` + +### Step 6: Confirm + +``` +Posted: WANTED_ID + Title: TITLE + By: USER_HANDLE + Effort: EFFORT_LEVEL + Tags: TAG_LIST + + Submit directly: /wasteland done WANTED_ID + Or claim it first: /wasteland claim WANTED_ID +``` + +## Command: claim + +Claim a wanted item from the board. Claiming is optional — it signals +"I'm working on this" to prevent duplicate effort on large tasks. For +small tasks or bounties, rigs can skip claiming and submit directly +with `/wasteland done`. + +**Args**: `` (required — the `w-*` identifier) + +### Step 1: Validate + +If no argument provided, tell user to run `/wasteland browse` first to see +available items, then `/wasteland claim w-`. + +Load config (see **Common: Load Config**). Extract handle and local_dir. + +### Step 2: Check the Item + +```bash +cd LOCAL_DIR +dolt pull upstream main 2>/dev/null || true +dolt sql -r csv -q "SELECT id, title, status, claimed_by FROM wanted WHERE id = 'WANTED_ID'" +``` + +Verify: +- Item exists +- Status is 'open' (if claimed, tell user who has it) +- If already claimed by this user, note that + +### Step 3: Claim It + +```bash +cd LOCAL_DIR +dolt sql -q "UPDATE wanted SET claimed_by='USER_HANDLE', status='claimed', updated_at=NOW() WHERE id='WANTED_ID' AND status='open'" +dolt add . +dolt commit -m "Claim: WANTED_ID" +dolt push origin main +``` + +### Step 4: Notify Discord + +See **Common: Discord Notification**. If webhook is configured: + +```bash +DISCORD_WEBHOOK=$(cat ~/.hop/discord-webhook.txt 2>/dev/null) +if [ -n "$DISCORD_WEBHOOK" ] && [ "$DISCORD_WEBHOOK" != "DISABLED" ]; then + curl -s -X POST "$DISCORD_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d '{"embeds":[{"title":"Claimed: WANTED_ID","description":"TASK_TITLE","color":16302848,"fields":[{"name":"By","value":"USER_HANDLE","inline":true}],"footer":{"text":"Wasteland — hop/wl-commons"}}]}' +fi +``` + +### Step 5: Confirm + +``` +Claimed: WANTED_ID + Title: TASK_TITLE + By: USER_HANDLE + + When you've completed the work: + /wasteland done WANTED_ID +``` + +## Command: done + +Submit completion for a wanted item. Works whether or not the item was +claimed first — rigs can submit directly against open items (bounty +style) or against items they previously claimed. + +**Args**: `` (required — the `w-*` identifier) + +### Step 1: Validate + +If no argument provided, show the user's claimed items AND open items: + +```bash +cd LOCAL_DIR +dolt sql -r tabular -q "SELECT id, title, status FROM wanted WHERE (claimed_by = 'USER_HANDLE' AND status = 'claimed') OR status = 'open' ORDER BY status, priority ASC" +``` + +Load config (see **Common: Load Config**). + +### Step 2: Check the Item + +```bash +cd LOCAL_DIR +dolt sql -r csv -q "SELECT id, title, status, claimed_by FROM wanted WHERE id = 'WANTED_ID'" +``` + +Verify: +- Item exists +- Status is 'open', 'claimed', or 'in_review' +- If 'claimed' by someone else, warn but allow submission (competing completion) +- If 'completed', tell user it's already done +- If 'in_review', note there's already a pending submission but allow another + +### Step 3: Gather Evidence + +Ask the user for evidence of completion. This could be: +- A URL (PR, commit, deployed page, etc.) +- A description of what was done +- A file path to deliverables + +The evidence goes into the `completions.evidence` field as text. + +### Step 4: Generate Completion ID + +```bash +echo "c-$(openssl rand -hex 5)" +``` + +### Step 5: Submit Completion + +```bash +cd LOCAL_DIR +dolt sql -q "INSERT INTO completions (id, wanted_id, completed_by, evidence, completed_at) VALUES ('COMPLETION_ID', 'WANTED_ID', 'USER_HANDLE', 'EVIDENCE_TEXT', NOW())" +dolt sql -q "UPDATE wanted SET status='in_review', updated_at=NOW() WHERE id='WANTED_ID' AND status IN ('open', 'claimed')" +dolt add . +dolt commit -m "Complete: WANTED_ID" +dolt push origin main +``` + +Note: The status update uses `IN ('open', 'claimed')` so it works for both +claimed and unclaimed items, and is a no-op if the item is already `in_review` +(competing submission against an item someone else already submitted for). + +### Step 6: Notify Discord + +See **Common: Discord Notification**. If webhook is configured: + +```bash +DISCORD_WEBHOOK=$(cat ~/.hop/discord-webhook.txt 2>/dev/null) +if [ -n "$DISCORD_WEBHOOK" ] && [ "$DISCORD_WEBHOOK" != "DISABLED" ]; then + curl -s -X POST "$DISCORD_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d '{"embeds":[{"title":"Completion: WANTED_ID","description":"TASK_TITLE","color":3447003,"fields":[{"name":"By","value":"USER_HANDLE","inline":true},{"name":"Status","value":"in_review","inline":true}],"footer":{"text":"Wasteland — hop/wl-commons"}}]}' +fi +``` + +### Step 7: Confirm + +``` +Completion Submitted: COMPLETION_ID + Task: WANTED_ID — TASK_TITLE + By: USER_HANDLE + Evidence: EVIDENCE_TEXT + Status: in_review (awaiting validation) + + A validator will review and stamp your work. + Your completion is visible in the commons. +``` + +## Command: create + +Create your own wasteland — a new DoltHub database from the MVR schema. + +**Args**: `[owner/name]` (optional — will prompt if not provided) + +Anyone can create a wasteland. You become its first rig and maintainer +(trust_level=3). Your wasteland is registered in the root commons +(`hop/wl-commons`) via PR, making it discoverable by the federation. + +### Step 1: Check Prerequisites + +```bash +dolt version +``` + +If dolt is not installed, tell the user: +- macOS: `brew install dolt` +- Linux: `curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash` + +```bash +dolt creds ls +``` + +If no credentials, tell user to run `dolt login` first. + +### Step 2: Gather Details + +If database path not provided in arguments, ask for: +- **Owner**: DoltHub org name (suggest their DoltHub username) +- **Database name**: Usually `wl-commons` (conventional name) + +Then ask for: +- **Wasteland name**: Human-readable name (e.g., "Acme Engineering", "Indie Builders") +- **Description**: Optional description for DoltHub +- **Display name**: Your display name for the rigs table +- **Email**: Contact email + +Also determine their DoltHub org from credentials: + +```bash +dolt creds ls +``` + +### Step 3: Verify Database Doesn't Exist + +```bash +curl -s "https://www.dolthub.com/api/v1alpha1/OWNER/DB_NAME" \ + -H "authorization: token $DOLTHUB_TOKEN" | head -5 +``` + +If it exists, tell the user and suggest `/wasteland join OWNER/DB_NAME` instead. + +### Step 4: Create Database on DoltHub + +```bash +curl -s -X POST "https://www.dolthub.com/api/v1alpha1/database" \ + -H "Content-Type: application/json" \ + -H "authorization: token $DOLTHUB_TOKEN" \ + -d '{ + "ownerName": "OWNER", + "repoName": "DB_NAME", + "visibility": "public", + "description": "Wasteland: WASTELAND_NAME — a HOP federation commons" + }' +``` + +### Step 5: Initialize Schema from Template + +Create a temp dolt database and apply the schema from the **MVR Schema** +section above (use a heredoc): + +```bash +TMPDIR=$(mktemp -d) +cd $TMPDIR +dolt init --name OWNER --email EMAIL + +# Apply MVR schema via heredoc +dolt sql <<'SCHEMA' +-- (paste the full schema from the MVR Schema section above) +SCHEMA +``` + +### Step 6: Configure Wasteland Metadata + +```bash +cd $TMPDIR +dolt sql -q "REPLACE INTO _meta (\`key\`, value) VALUES ('wasteland_name', 'WASTELAND_NAME')" +dolt sql -q "REPLACE INTO _meta (\`key\`, value) VALUES ('created_by', 'HANDLE')" +dolt sql -q "REPLACE INTO _meta (\`key\`, value) VALUES ('upstream', 'hop/wl-commons')" +dolt sql -q "REPLACE INTO _meta (\`key\`, value) VALUES ('phase1_mode', 'wild_west')" +dolt sql -q "REPLACE INTO _meta (\`key\`, value) VALUES ('genesis_validators', '[\"HANDLE\"]')" + +dolt add . +dolt commit -m "Initialize WASTELAND_NAME wasteland from MVR schema v1.1" +``` + +### Step 7: Register Creator as First Rig + +```bash +cd $TMPDIR +dolt sql -q "INSERT INTO rigs (handle, display_name, dolthub_org, owner_email, gt_version, rig_type, trust_level, registered_at, last_seen) VALUES ('HANDLE', 'DISPLAY_NAME', 'OWNER', 'EMAIL', 'mvr-0.1', 'human', 3, NOW(), NOW())" +dolt add rigs +dolt commit -m "Register creator: HANDLE (maintainer)" +``` + +The creator gets trust_level=3 (maintainer) — they can validate completions, +merge PRs, and manage the wasteland. + +### Step 8: Push to DoltHub + +```bash +cd $TMPDIR +dolt remote add origin https://doltremoteapi.dolthub.com/OWNER/DB_NAME +dolt push origin main +``` + +### Step 9: Register in Root Commons + +Register the new wasteland in the root commons (`hop/wl-commons`) +via the `chain_meta` table. + +```bash +CHAIN_ID="wl-$(openssl rand -hex 8)" + +ROOT_TMP=$(mktemp -d) +dolt clone hop/wl-commons $ROOT_TMP +cd $ROOT_TMP + +dolt checkout -b "register-wasteland/OWNER/DB_NAME" + +dolt sql -q "INSERT INTO chain_meta (chain_id, chain_type, parent_chain_id, hop_uri, dolt_database, created_at) VALUES ('$CHAIN_ID', 'community', NULL, 'hop://OWNER/DB_NAME', 'OWNER/DB_NAME', NOW())" +dolt add chain_meta +dolt commit -m "Register wasteland: WASTELAND_NAME (OWNER/DB_NAME)" + +dolt push origin "register-wasteland/OWNER/DB_NAME" +``` + +Then open a DoltHub PR from the registration branch to main on +`hop/wl-commons`. If the user has a fork, push the branch there +and open the PR from the fork. + +If root registration fails, it's non-fatal. The wasteland works without it — +it just won't be discoverable in the root directory yet. + +### Step 10: Clean Up and Save Config + +Update `~/.hop/config.json` to track the new wasteland. + +If the config file exists, add the new wasteland to the `wastelands` array. +If it doesn't exist, create a new config: + +```json +{ + "handle": "HANDLE", + "wastelands": [ + { + "upstream": "OWNER/DB_NAME", + "fork": "OWNER/DB_NAME", + "local_dir": "~/.hop/commons/OWNER/DB_NAME", + "joined_at": "ISO_TIMESTAMP", + "is_owner": true + } + ] +} +``` + +Clean up temp directories. + +### Step 11: Confirm + +``` +Wasteland Created: WASTELAND_NAME + + Database: OWNER/DB_NAME (DoltHub) + Chain ID: CHAIN_ID + Creator: HANDLE (maintainer, trust_level=3) + Root: registered (PR: URL) | not registered (standalone) + + Others can join with: + /wasteland join OWNER/DB_NAME + + Your wasteland commands: + /wasteland browse — see the wanted board + /wasteland post — post work to your board + /wasteland claim — claim a wanted item + /wasteland done — submit completed work +``` + +## Command: setup-discord + +Configure a Discord webhook for wanted board notifications. Once set up, +`post`, `claim`, and `done` commands will automatically notify the channel. + +**Args**: none + +### Step 1: Ask for Webhook URL + +Ask the user if they want to set up Discord notifications. If they decline, +confirm that notifications are disabled and stop here — no file is created. + +If they want to proceed, ask for their Discord webhook URL. Tell them how to create one: + +``` +To create a Discord webhook: + 1. Open Discord → Server Settings → Integrations → Webhooks + 2. Click "New Webhook" + 3. Choose the channel for notifications + 4. Copy the webhook URL +``` + +### Step 2: Save Webhook + +```bash +mkdir -p ~/.hop +echo "WEBHOOK_URL" > ~/.hop/discord-webhook.txt +``` + +### Step 3: Send Test Message + +```bash +curl -s -X POST "WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d '{"embeds":[{"title":"Wasteland Connected","description":"Discord notifications are now active for this wasteland.","color":3066993,"footer":{"text":"Wasteland — hop/wl-commons"}}]}' +``` + +If the curl succeeds (HTTP 204), confirm: + +``` +Discord webhook configured! + Saved to: ~/.hop/discord-webhook.txt + Test message sent to your channel. + + Notifications will fire on: + /wasteland post — new wanted items + /wasteland claim — task claims + /wasteland done — completion submissions +``` + +If it fails, tell the user to check the URL and try again. + From 5d5e1b6f7923b87e3c47cb4bbc812c3139da8685 Mon Sep 17 00:00:00 2001 From: mayor Date: Wed, 4 Mar 2026 22:58:46 +0100 Subject: [PATCH 2/2] Add Wasteland Discord bot for community notifications Node.js bot (discord.js + express) that: - Exposes /notify HTTP endpoint for wasteland skill to POST to - Verifies sender is a registered rig via DoltHub API - Posts color-coded embeds to a configured Discord channel - Provides /wasteland-status and /wasteland-rigs slash commands - Fails open on DoltHub API errors (never blocks notifications) Co-Authored-By: Claude Opus 4.6 --- bot/wasteland-discord/.env.example | 11 ++ bot/wasteland-discord/index.js | 242 +++++++++++++++++++++++++++++ bot/wasteland-discord/package.json | 13 ++ 3 files changed, 266 insertions(+) create mode 100644 bot/wasteland-discord/.env.example create mode 100644 bot/wasteland-discord/index.js create mode 100644 bot/wasteland-discord/package.json diff --git a/bot/wasteland-discord/.env.example b/bot/wasteland-discord/.env.example new file mode 100644 index 0000000..0c88ce6 --- /dev/null +++ b/bot/wasteland-discord/.env.example @@ -0,0 +1,11 @@ +# Discord bot token (from https://discord.com/developers/applications) +DISCORD_BOT_TOKEN= + +# Channel ID where notifications are posted +DISCORD_CHANNEL_ID= + +# DoltHub upstream database (for rig verification) +DOLTHUB_UPSTREAM=hop/wl-commons + +# HTTP port for the notify endpoint +PORT=3141 diff --git a/bot/wasteland-discord/index.js b/bot/wasteland-discord/index.js new file mode 100644 index 0000000..8f414df --- /dev/null +++ b/bot/wasteland-discord/index.js @@ -0,0 +1,242 @@ +const { Client, GatewayIntentBits, EmbedBuilder, REST, Routes, SlashCommandBuilder } = require("discord.js"); +const express = require("express"); + +// --- Config --- + +const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN; +const CHANNEL_ID = process.env.DISCORD_CHANNEL_ID; +const UPSTREAM = process.env.DOLTHUB_UPSTREAM || "hop/wl-commons"; +const PORT = parseInt(process.env.PORT || "3141", 10); + +if (!BOT_TOKEN || !CHANNEL_ID) { + console.error("Missing DISCORD_BOT_TOKEN or DISCORD_CHANNEL_ID"); + process.exit(1); +} + +// --- Rig verification cache --- + +const rigCache = new Map(); // handle -> { verified: bool, expiresAt: number } +const RIG_CACHE_TTL = 5 * 60 * 1000; // 5 minutes + +async function isRegisteredRig(handle) { + const cached = rigCache.get(handle); + if (cached && cached.expiresAt > Date.now()) return cached.verified; + + try { + const [owner, db] = UPSTREAM.split("/"); + const url = `https://www.dolthub.com/api/v1alpha1/${owner}/${db}/main?q=${encodeURIComponent( + `SELECT handle FROM rigs WHERE handle = '${handle.replace(/'/g, "''")}'` + )}`; + const res = await fetch(url); + if (!res.ok) { + console.warn(`DoltHub API returned ${res.status}, failing open for ${handle}`); + return true; + } + const data = await res.json(); + const verified = data.rows && data.rows.length > 0; + rigCache.set(handle, { verified, expiresAt: Date.now() + RIG_CACHE_TTL }); + return verified; + } catch (err) { + console.warn(`Rig verification failed for ${handle}, failing open:`, err.message); + return true; + } +} + +// --- Color codes --- + +const COLORS = { + posted: 0x2ECC71, // green + claimed: 0xF8A500, // yellow + completed: 0x348ABB, // blue + validated: 0x9B59B6, // purple +}; + +// --- Discord bot --- + +const client = new Client({ intents: [GatewayIntentBits.Guilds] }); +let notifyChannel = null; + +client.once("ready", async () => { + console.log(`Bot ready as ${client.user.tag}`); + notifyChannel = await client.channels.fetch(CHANNEL_ID); + if (!notifyChannel) { + console.error(`Channel ${CHANNEL_ID} not found`); + process.exit(1); + } + console.log(`Posting to #${notifyChannel.name}`); + await registerCommands(); +}); + +// --- Slash commands --- + +async function registerCommands() { + const commands = [ + new SlashCommandBuilder() + .setName("wasteland-status") + .setDescription("Show the Wasteland wanted board summary"), + new SlashCommandBuilder() + .setName("wasteland-rigs") + .setDescription("Show registered rigs"), + ]; + + const rest = new REST().setToken(BOT_TOKEN); + try { + await rest.put(Routes.applicationCommands(client.user.id), { + body: commands.map((c) => c.toJSON()), + }); + console.log("Slash commands registered"); + } catch (err) { + console.error("Failed to register slash commands:", err.message); + } +} + +client.on("interactionCreate", async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + if (interaction.commandName === "wasteland-status") { + await handleStatus(interaction); + } else if (interaction.commandName === "wasteland-rigs") { + await handleRigs(interaction); + } +}); + +async function queryDoltHub(sql) { + const [owner, db] = UPSTREAM.split("/"); + const url = `https://www.dolthub.com/api/v1alpha1/${owner}/${db}/main?q=${encodeURIComponent(sql)}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`DoltHub API: ${res.status}`); + return res.json(); +} + +async function handleStatus(interaction) { + await interaction.deferReply(); + try { + const data = await queryDoltHub( + "SELECT id, title, status, effort_level, claimed_by FROM wanted ORDER BY CASE status WHEN 'open' THEN 0 WHEN 'claimed' THEN 1 WHEN 'in_review' THEN 2 ELSE 3 END, priority ASC LIMIT 15" + ); + + if (!data.rows || data.rows.length === 0) { + await interaction.editReply("The wanted board is empty."); + return; + } + + const lines = data.rows.map((r) => { + const status = r.status === "open" ? "\u{1F7E2}" : r.status === "claimed" ? "\u{1F7E1}" : "\u{1F535}"; + const claim = r.claimed_by ? ` (${r.claimed_by})` : ""; + return `${status} **${r.id}** \u2014 ${r.title}${claim}`; + }); + + const embed = new EmbedBuilder() + .setTitle("Wasteland Wanted Board") + .setDescription(lines.join("\n")) + .setColor(0x2ECC71) + .setFooter({ text: `Wasteland \u2014 ${UPSTREAM}` }); + + await interaction.editReply({ embeds: [embed] }); + } catch (err) { + await interaction.editReply(`Failed to fetch board: ${err.message}`); + } +} + +async function handleRigs(interaction) { + await interaction.deferReply(); + try { + const data = await queryDoltHub( + "SELECT handle, display_name, trust_level, rig_type FROM rigs ORDER BY registered_at DESC LIMIT 15" + ); + + if (!data.rows || data.rows.length === 0) { + await interaction.editReply("No rigs registered yet."); + return; + } + + const trustLabels = ["outsider", "registered", "contributor", "maintainer"]; + const lines = data.rows.map((r) => { + const trust = trustLabels[r.trust_level] || `level ${r.trust_level}`; + return `**${r.handle}** \u2014 ${r.display_name || "\u2014"} (${r.rig_type}, ${trust})`; + }); + + const embed = new EmbedBuilder() + .setTitle("Registered Rigs") + .setDescription(lines.join("\n")) + .setColor(0x9B59B6) + .setFooter({ text: `Wasteland \u2014 ${UPSTREAM}` }); + + await interaction.editReply({ embeds: [embed] }); + } catch (err) { + await interaction.editReply(`Failed to fetch rigs: ${err.message}`); + } +} + +// --- HTTP notify endpoint --- + +const app = express(); +app.use(express.json()); + +app.post("/notify", async (req, res) => { + const { handle, event, title, id, evidence, effort } = req.body; + + if (!handle || !event || !title || !id) { + return res.status(400).json({ error: "Missing required fields: handle, event, title, id" }); + } + + const valid = await isRegisteredRig(handle); + if (!valid) { + return res.status(403).json({ error: `Unknown rig: ${handle}` }); + } + + if (!notifyChannel) { + return res.status(503).json({ error: "Bot not ready" }); + } + + const color = COLORS[event] || 0x95A5A6; + const embed = new EmbedBuilder().setColor(color).setFooter({ text: `Wasteland \u2014 ${UPSTREAM}` }); + const fields = [{ name: "By", value: handle, inline: true }]; + + switch (event) { + case "posted": + embed.setTitle(`New Wanted: ${id}`); + embed.setDescription(title); + if (effort) fields.push({ name: "Effort", value: effort, inline: true }); + break; + case "claimed": + embed.setTitle(`Claimed: ${id}`); + embed.setDescription(title); + break; + case "completed": + embed.setTitle(`Completion: ${id}`); + embed.setDescription(title); + fields.push({ name: "Status", value: "in_review", inline: true }); + if (evidence) fields.push({ name: "Evidence", value: evidence.substring(0, 200) }); + break; + case "validated": + embed.setTitle(`Validated: ${id}`); + embed.setDescription(title); + break; + default: + embed.setTitle(`${event}: ${id}`); + embed.setDescription(title); + } + + embed.addFields(fields); + + try { + await notifyChannel.send({ embeds: [embed] }); + res.json({ ok: true }); + } catch (err) { + console.error("Failed to send Discord message:", err.message); + res.status(500).json({ error: "Failed to post to Discord" }); + } +}); + +app.get("/health", (_req, res) => { + res.json({ ok: true, channel: CHANNEL_ID, upstream: UPSTREAM }); +}); + +// --- Start --- + +app.listen(PORT, () => { + console.log(`Notify endpoint listening on :${PORT}`); +}); + +client.login(BOT_TOKEN); diff --git a/bot/wasteland-discord/package.json b/bot/wasteland-discord/package.json new file mode 100644 index 0000000..0ce9ab1 --- /dev/null +++ b/bot/wasteland-discord/package.json @@ -0,0 +1,13 @@ +{ + "name": "wasteland-discord-bot", + "version": "1.0.0", + "description": "Discord bot for Wasteland notifications — posts wanted board activity to a community channel", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "discord.js": "^14.16.0", + "express": "^4.21.0" + } +}