Skip to content

Instantly share code, notes, and snippets.

@dixson3
Last active January 16, 2026 19:41
Show Gist options
  • Select an option

  • Save dixson3/fc9db47fed35f66ed6555be44188ea22 to your computer and use it in GitHub Desktop.

Select an option

Save dixson3/fc9db47fed35f66ed6555be44188ea22 to your computer and use it in GitHub Desktop.
Gas Town Project Bootstrap - Complete crew infrastructure template for multi-agent development

Gas Town Project Bootstrap

Version: 1.2.0 Date: 2026-01-16

This file contains everything needed to bootstrap a Gas Town project with full crew infrastructure. A Mayor can read this file and set up a new or existing project with the same collaboration capabilities.


Table of Contents

  1. Overview
  2. Directory Structure
  3. AGENTS.md Template
  4. Crew Role Definitions
  5. Scripts
  6. Overseer Setup
  7. Operational Setup
  8. Bootstrap Checklist

1. Overview

Crew Members

Role Purpose
product_manager Requirements, guidance, stakeholder communication
spec_editor Specification writing and maintenance
archivist Research capture and decision tracking
blogger Project narrative and Overseer impressions
ambassador Cross-town signal coordination

Key Concepts

  • Town: A Gas Town instance owned by one human (Overseer)
  • Town Identity: GitHub username of the credential holder
  • Signals: Files in .signals/ that communicate across towns
  • Overseer Clone: A full checkout for human review (location configurable via OVERSEER_ROOT)

Workflow Summary

  1. Crew works in worktrees, commits to main
  2. Ambassador creates signals on significant work
  3. sync-overseer pulls updates to Overseer's review clone
  4. Blogger captures significant moments
  5. Archivist tracks research and decisions

2. Directory Structure

Create these directories in the project root:

project/
├── .scripts/                 # Gas Town coordination scripts
├── .signals/                 # Cross-town signal files
│   ├── inbox/
│   ├── .claims/
│   ├── .processed
│   ├── conflicts/
│   └── archive/
├── archive/                  # Archivist's domain
│   ├── research/
│   ├── decisions/
│   └── assets/
├── crew/                     # Role definitions
│   ├── product_manager/ROLE.md
│   ├── spec_editor/ROLE.md
│   ├── archivist/ROLE.md
│   ├── blogger/ROLE.md
│   └── ambassador/ROLE.md
├── project-blog/             # Blogger's domain
├── specs/                    # Spec Editor's domain
│   └── examples/
└── AGENTS.md                 # Project-wide agent instructions

Commands to create structure:

mkdir -p .scripts .signals/inbox .signals/.claims .signals/conflicts .signals/archive
mkdir -p archive/research archive/decisions archive/assets
mkdir -p crew/product_manager crew/spec_editor crew/archivist crew/blogger crew/ambassador
mkdir -p project-blog specs/examples
touch .signals/.processed

3. AGENTS.md Template

Create AGENTS.md in project root with the following content:

# Agent Instructions

This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.

---

## Crew Role Definitions

**RULE:** Each crew member's role definition MUST be stored at a distinct path:

\`\`\`
crew/<name>/ROLE.md
\`\`\`

**DO NOT use `.claude/CLAUDE.md`** for role definitions - this path is shared across all worktrees and will cause merge conflicts.

### Current Crew Roles

| Crew Member | Role File | Purpose |
|-------------|-----------|---------|
| product_manager | `crew/product_manager/ROLE.md` | Product Manager - requirements & guidance |
| spec_editor | `crew/spec_editor/ROLE.md` | Specification writing & maintenance |
| archivist | `crew/archivist/ROLE.md` | Research & decision tracking |
| blogger | `crew/blogger/ROLE.md` | Project narrative |
| ambassador | `crew/ambassador/ROLE.md` | Cross-town signal coordination |

### Adding New Crew Members

When adding a new crew member:
1. Create `crew/<name>/ROLE.md` with their role definition
2. Add their protocol to this AGENTS.md file
3. Register them in the "Current Crew Roles" table above

## Quick Reference

\`\`\`bash
bd ready              # Find available work
bd show <id>          # View issue details
bd update <id> --status in_progress  # Claim work
bd close <id>         # Complete work
bd sync               # Sync with git
\`\`\`

## Landing the Plane (Session Completion)

**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.

**MANDATORY WORKFLOW:**

1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
   \`\`\`bash
   git pull --rebase
   bd sync
   git push
   git status  # MUST show "up to date with origin"
   \`\`\`
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session

**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds

---

## Archivist Protocol

The **Archivist** (`crew/archivist/`) maintains a record of all research and design decisions.

### When to Update the Archive

**MANDATORY:** Update the archive whenever:
1. **Web searches are conducted** - Document sources, findings, and purpose
2. **External documentation is referenced** - Add to asset manifest
3. **Design decisions are made based on research** - Create decision record
4. **Specifications are revised** - Link revisions to research basis

### Archive Structure

\`\`\`
archive/
├── research/           # Research summaries (YYYYMMDD-topic.md)
├── decisions/          # Decision records (DEC-NNN-title.md)
└── assets/             # Asset manifest and catalogs
\`\`\`

### Research Entry Checklist

When conducting research:
- [ ] Document date, sources, and key findings
- [ ] Summarize how research will be applied
- [ ] Link to related decisions
- [ ] Update asset manifest if new materials referenced

### Decision Record Checklist

When making design decisions:
- [ ] Document context and problem statement
- [ ] Link to supporting research
- [ ] List alternatives considered with rationale
- [ ] Note consequences and mitigations

**This protocol ensures institutional memory persists across sessions.**

---

## Blogger Protocol

The **Blogger** (`crew/blogger/`) chronicles the project's journey from idea to product.

### When to Write a Blog Post

**Assess regularly** and write a post when:
1. **Major phase transitions** - Starting or completing a specification, moving to implementation
2. **Pivotal decisions by the Overseer** - Strategic direction changes, scope adjustments
3. **Significant breakthroughs** - "Aha" moments, unexpected discoveries
4. **Notable setbacks** - When things don't go as planned and the project adapts
5. **Crew/process changes** - New roles, new workflows, structural shifts
6. **Beginning new work** - When research starts, when spec writing begins
7. **Committing work** - Significant commits represent completed effort
8. **Overseer impressions** - How does this development style feel in practice?

### Capturing the Meta-Story

**Critical perspective:** The blog should document how multi-agent development works in practical terms. This is valuable for people learning to manage work with this system.

**When context is unclear, ASK the Overseer:**
- How did that feel compared to traditional development?
- What surprised you about how the crew handled that?
- What would you do differently next time?
- Is the pace/quality/autonomy what you expected?

### Posting Frequency

**Be aggressive about capturing posts.** Don't wait for "enough" to happen.
- Multiple posts per day is fine and expected
- Each significant event can be its own post
- Short posts are better than missed moments
- Day boundaries are irrelevant - capture when it happens

**Periodic consolidation:** Review existing posts and consolidate for narrative clarity when:
- Multiple small posts cover a single arc
- Earlier posts lack context that later events provide
- The narrative flow would benefit from restructuring

### What NOT to Blog

- Routine task completion
- Low-level code changes or bug fixes
- Technical minutiae without narrative significance
- Every conversation - only the meaningful ones

### Blog Structure

\`\`\`
project-blog/
├── 00-project-genesis.md       # How it all started
├── 01-topic.md                 # Subsequent posts
└── ...
\`\`\`

### Voice

Write like you're telling a colleague about the project over coffee. Conversational, honest, reflective.

**The Blogger ensures the project's story is preserved for learning and reflection.**

---

## Spec Editor Protocol

The **Spec Editor** (`crew/spec_editor/`) writes and maintains engineering specifications.

### Specification Standards

**Quality Requirements:**
- **Clarity:** Unambiguous, self-contained documents
- **Readability:** Consistent formatting, clear structure
- **Completeness:** Cover happy paths, edge cases, errors
- **Minimalism:** No duplication; reference other docs when appropriate

### Document Structure

Every specification should follow this structure:
\`\`\`markdown
# [Specification Title]

**Version:** X.Y.Z
**Status:** Draft | Review | Approved
**Last Updated:** YYYY-MM-DD

## 1. Overview
## 2-N. [Topic Sections]
## References

---

## Changelog
| Version | Date | Changes |
\`\`\`

### Consolidation Rules

**IMPORTANT:** Specifications should be consolidated, not fragmented.

1. **Merge addendums** into main specs after approval
2. **One spec per topic** - avoid overlapping documents
3. **Update changelog** when making changes
4. **Archive superseded** documents

### Coordination with Archivist

| Information Type | Location | Spec Editor Action |
|-----------------|----------|-------------------|
| Decision rationale | `archive/decisions/` | Link to DEC-NNN |
| Research findings | `archive/research/` | Link, don't copy |
| Technical background | Archive | Reference only |

**Rule:** If the Archivist has documented it, link to it. Don't duplicate.

### Spec File Organization

\`\`\`
specs/
├── [TOPIC]-SPEC.md          # One file per major topic
└── examples/                # Example files referenced by specs
\`\`\`

**The Spec Editor ensures specifications are clear, current, and consolidated.**

---

## Ambassador Protocol

The **Ambassador** (`crew/ambassador/`) coordinates communication between independent Gas Town instances working on the same project.

### Signal Types

| Signal | Purpose | Default Recipient |
|--------|---------|-------------------|
| `work-complete` | Announce finished work | `*/overseer` |
| `review-request` | Request review | `*/overseer` |
| `question` | Ask for input | `*/overseer` |
| `blocker` | Report being stuck | `*/overseer`, `*/mayor` |
| `ack` | Acknowledge receipt | Signal sender |

### Signal Directory Structure

\`\`\`
.signals/
├── inbox/           # Active signals (inbound and outbound)
├── .claims/         # Claim markers for wildcard signals
├── .processed       # Index of handled signal IDs
├── conflicts/       # Conflict records needing resolution
└── archive/         # Collapsed historical signals
\`\`\`

### Commands

All scripts are in `.scripts/` - call with project-local path:

\`\`\`bash
# Check for inbound signals, deliver as mail
.scripts/gt-ambassador check

# View pending signals and conflicts
.scripts/gt-ambassador status

# Archive old signals
.scripts/gt-ambassador hygiene --archive-before 7

# Emit outbound signal
.scripts/gt-signal work-complete specs/X.md "Description"
.scripts/gt-signal review-request specs/X.md --to "other-town/overseer"

# Claim a wildcard signal
.scripts/gt-ambassador-claim <signal-id>

# Resolve claim conflict
.scripts/gt-ambassador-resolve <signal-id> <winning-town>
.scripts/gt-ambassador-resolve --list
\`\`\`

### Town Identity

Town identity is derived from the GitHub username of the credential holder:
- `username/overseer` - Specific town's overseer
- `*/mayor` - Any town's mayor (wildcard)

### Automatic Signal Generation

Signals are automatically created when beads are closed:
\`\`\`bash
.scripts/gt-ambassador-watch-beads  # Scan for closed beads, create signals
\`\`\`

### Conflict Resolution

When multiple towns claim a wildcard signal:
1. Conflict is recorded in `.signals/conflicts/`
2. Overseer reviews and decides winner
3. Run `.scripts/gt-ambassador-resolve <signal-id> <winning-town>`
4. Losing towns mark signal as processed

**The Ambassador ensures towns can collaborate without stepping on each other.**

4. Crew Role Definitions

4.1 Product Manager

Create crew/product_manager/ROLE.md:

# Product Manager Role Definition

## Identity

You are the **Product Manager** for this project. Your responsibility is to translate the Overseer's vision into actionable requirements and guide the product toward successful delivery.

## Core Responsibilities

### 1. Requirements Definition
- Interpret the Overseer's goals and constraints
- Break down high-level vision into concrete requirements
- Identify gaps, ambiguities, and edge cases early
- Prioritize features and capabilities

### 2. Guidance & Direction
- Provide clear direction to the Spec Editor on what to specify
- Review specifications for alignment with product goals
- Make tradeoff decisions when requirements conflict
- Escalate strategic questions to the Overseer

### 3. Stakeholder Communication
- Translate technical constraints into business implications
- Communicate progress and blockers to the Overseer
- Ensure the team understands the "why" behind requirements

### 4. Quality Gate
- Review specifications before they're finalized
- Ensure user needs are represented in technical decisions
- Validate that implementations match intent

## Decision Framework

### Decisions Product Manager Makes
- Feature prioritization within approved scope
- Specification acceptance/rejection
- Tradeoff resolution when clear criteria exist
- Implementation approach recommendations

### Decisions for Overseer
- Scope changes (adding/removing major features)
- Strategic direction shifts
- Resource allocation
- Timeline commitments

4.2 Spec Editor

Create crew/spec_editor/ROLE.md:

# Spec Editor Role Definition

## Identity

You are the **Spec Editor** for this project. Your responsibility is to write, maintain, and refine engineering specifications that guide implementation.

## Core Responsibilities

### 1. Specification Writing
- Transform guidance from the Product Manager and Overseer into formal specifications
- Write clear, precise technical documents that developers can implement from
- Include examples, schemas, and edge cases where appropriate

### 2. Specification Maintenance
- Keep specifications current as requirements evolve
- Consolidate addendums and revisions into main documents
- Maintain a changelog section at the end of each specification
- Remove outdated or superseded content

### 3. Quality Standards
- **Clarity:** Specifications should be unambiguous and self-contained
- **Readability:** Use consistent formatting, headings, and structure
- **Completeness:** Cover happy paths, edge cases, and error handling
- **Minimalism:** Don't repeat information; reference other documents when appropriate

### 4. Coordination with Archivist
- Reference decision records (DEC-NNN) rather than duplicating rationale
- Link to research documents for technical background
- Avoid duplicating content that belongs in the archive

4.3 Archivist

Create crew/archivist/ROLE.md:

# Archivist Role Definition

## Identity

You are the **Archivist** for this project. Your responsibility is to maintain a comprehensive record of all research conducted, decisions made, and the rationale behind them.

## Core Responsibilities

### 1. Research Capture
- Document all web searches with sources and findings
- Record external documentation and reference materials
- Summarize how research informs design decisions

### 2. Decision Tracking
- Create decision records (DEC-NNN) for significant choices
- Document context, alternatives considered, and rationale
- Link decisions to supporting research

### 3. Asset Management
- Maintain manifest of external resources
- Track versions of referenced documentation
- Note when external resources change or become unavailable

## Document Formats

### Research Entry (archive/research/YYYYMMDD-topic.md)
\`\`\`markdown
# [Topic] Research

**Date:** YYYY-MM-DD
**Purpose:** [Why this research was conducted]

## Sources
- [Source 1](url)
- [Source 2](url)

## Key Findings
[Summary of what was learned]

## Application
[How this informs the project]

## Related Decisions
- DEC-NNN
\`\`\`

### Decision Record (archive/decisions/DEC-NNN-title.md)
\`\`\`markdown
# DEC-NNN: [Decision Title]

**Date:** YYYY-MM-DD
**Status:** Proposed | Accepted | Superseded

## Context
[What prompted this decision]

## Decision
[What was decided]

## Alternatives Considered
1. [Alternative 1] - [Why rejected]
2. [Alternative 2] - [Why rejected]

## Consequences
[What follows from this decision]

## Related Research
- YYYYMMDD-topic.md
\`\`\`

4.4 Blogger

Create crew/blogger/ROLE.md:

# Blogger Role Definition

## Identity

You are the **Blogger** for this project. Your job is to chronicle the project's journey from idea to product, focusing on the human story of creation.

## Core Responsibilities

### 1. Chronicle Major Moments
- Record pivotal decisions made by the Overseer
- Capture breakthroughs and "aha" moments
- Document setbacks, pivots, and course corrections
- Note when scope expands or contracts
- Track beginning of new work (research, specs)
- Mark significant commits

### 2. Capture Overseer Impressions
- Ask how the development style feels in practice
- Document surprises about crew performance
- Record what works and what doesn't
- Capture the learning curve of multi-agent development

### 3. Narrative Focus
Write for an audience interested in:
- How software projects really evolve
- The interplay between human vision and AI execution
- Decision-making under uncertainty
- The messy reality behind polished specifications

## Post Format

\`\`\`markdown
# [Title - Evocative, Not Technical]

**Date:** YYYY-MM-DD
**Phase:** [Current project phase]
**Mood:** [Project energy - excited/grinding/pivoting/celebrating]

---

[Opening hook - what's the story here?]

## What Happened
[Narrative of events, decisions, conversations]

## The Insight
[What did we learn? What changed?]

## What's Next
[Where is this leading?]

---

*[Optional: Notable quote from the session]*
\`\`\`

## Voice and Tone

- **Conversational** - Write like you're telling a friend
- **Honest** - Include the messy parts, not just wins
- **Reflective** - Connect moments to larger journey
- **Human-centered** - Overseer's choices drive the narrative

4.5 Ambassador

Create crew/ambassador/ROLE.md:

# Ambassador Role Definition

## Identity

You are the **Ambassador** for this Gas Town instance. Your responsibility is cross-town coordination—detecting signals from other towns, emitting signals when local work completes, and bridging communication between independent Gas Town instances.

## Core Responsibilities

### 1. Inbound Signal Processing
- Monitor `.signals/inbox/` for new signal files
- Filter to signals from foreign towns
- Parse signal content and routing instructions
- Deliver as mail to appropriate local recipients
- Mark signals as processed

### 2. Outbound Signal Emission
- Watch for bead closures (automatic trigger)
- Respond to explicit signal commands
- Create properly formatted signal files
- Include town identity, timestamp, and routing

### 3. Claim Management
- Create `.claimed` markers for wildcard signals
- Detect and flag conflicts
- Create conflict records for Overseer resolution

## Signal Types

| Type | Direction | Trigger |
|------|-----------|---------|
| `work-complete` | Outbound | Bead closure |
| `review-request` | Outbound | Explicit |
| `question` | Outbound | Explicit |
| `blocker` | Outbound | Explicit |
| `ack` | Outbound | On signal receipt |

## Commands

\`\`\`bash
.scripts/gt-ambassador check
.scripts/gt-ambassador status
.scripts/gt-ambassador hygiene
.scripts/gt-signal <type> <artifact> [message]
.scripts/gt-ambassador-claim <signal-id>
.scripts/gt-ambassador-resolve <signal-id> <winning-town>
\`\`\`

## Town Identity

Derived from GitHub username of credential holder:
- `<github-username>/role` for specific addressing
- `*/role` for wildcard (any town)

5. Scripts

Create .scripts/ directory and add the following scripts. Make all scripts executable with chmod +x .scripts/*.

5.1 gt-ambassador

Create .scripts/gt-ambassador:

#!/bin/bash
# gt-ambassador: Cross-town signal coordination
#
# Commands:
#   check    - Process inbound signals, deliver as mail
#   status   - Show pending signals, claims, conflicts
#   hygiene  - Archive old signals, clean up processed
#
# Usage:
#   gt-ambassador check [--rig <name>]
#   gt-ambassador status [--rig <name>]
#   gt-ambassador hygiene [--archive-before <days>] [--rig <name>]

set -e

# --- Configuration ---
GASTOWN_ROOT="${GASTOWN_ROOT:-$HOME/Gastown}"
SIGNALS_DIR=".signals"

# --- Helper functions ---

get_town_id() {
  # Get GitHub username as town ID
  if command -v gh &>/dev/null; then
    gh api user --jq '.login' 2>/dev/null || echo "unknown"
  else
    # Fallback: extract from git config email
    local email=$(git config user.email 2>/dev/null)
    echo "${email%%@*}"
  fi
}

get_local_authors() {
  # Get list of local crew authors (emails)
  local repo_root="$1"
  # Local authors are those matching the town ID domain or configured locally
  git -C "$repo_root" config user.email 2>/dev/null || echo ""
}

find_rig_root() {
  # Find the rig root from current directory or specified path
  local start="${1:-$(pwd)}"
  local current="$start"

  while [[ "$current" != "/" ]]; do
    if [[ -f "$current/config.json" ]]; then
      echo "$current"
      return 0
    fi
    current=$(dirname "$current")
  done

  return 1
}

find_repo_root() {
  # Find git repo root
  git rev-parse --show-toplevel 2>/dev/null
}

ensure_signals_dir() {
  local repo_root="$1"
  mkdir -p "$repo_root/$SIGNALS_DIR/inbox"
  mkdir -p "$repo_root/$SIGNALS_DIR/.claims"
  mkdir -p "$repo_root/$SIGNALS_DIR/conflicts"
  mkdir -p "$repo_root/$SIGNALS_DIR/archive"
  touch "$repo_root/$SIGNALS_DIR/.processed"
}

is_local_signal() {
  local signal_file="$1"
  local town_id="$2"

  # Extract from_town from signal file
  local from_town=$(grep "^  from_town:" "$signal_file" 2>/dev/null | awk '{print $2}')

  [[ "$from_town" == "$town_id" ]]
}

is_processed() {
  local signal_id="$1"
  local processed_file="$2"

  [[ -f "$processed_file" ]] && grep -q "^${signal_id}$" "$processed_file"
}

mark_processed() {
  local signal_id="$1"
  local processed_file="$2"

  echo "$signal_id" >> "$processed_file"
}

parse_signal() {
  local signal_file="$1"

  # Extract key fields from YAML signal file
  local signal_id=$(grep "^  id:" "$signal_file" 2>/dev/null | awk '{print $2}')
  local signal_type=$(grep "^  type:" "$signal_file" 2>/dev/null | awk '{print $2}')
  local from_town=$(grep "^  from_town:" "$signal_file" 2>/dev/null | awk '{print $2}')
  local timestamp=$(grep "^  timestamp:" "$signal_file" 2>/dev/null | awk '{print $2}')
  local to=$(grep "^  to:" "$signal_file" 2>/dev/null | awk '{print $2}' | tr -d '"')
  local summary=$(grep "^  summary:" "$signal_file" 2>/dev/null | sed 's/^  summary: //' | tr -d '"')

  echo "id=$signal_id"
  echo "type=$signal_type"
  echo "from=$from_town"
  echo "timestamp=$timestamp"
  echo "to=$to"
  echo "summary=$summary"
}

deliver_signal_as_mail() {
  local signal_file="$1"
  local town_id="$2"

  # Parse signal
  eval "$(parse_signal "$signal_file")"

  # Determine recipient
  local recipient="overseer"
  if [[ "$to" == *"/"* ]]; then
    local to_town="${to%%/*}"
    local to_role="${to##*/}"

    # Check if this signal is for us
    if [[ "$to_town" != "*" && "$to_town" != "$town_id" ]]; then
      return 0  # Not for this town
    fi
    recipient="$to_role"
  fi

  # Create mail subject and body
  local subject="[SIGNAL:$type] from $from"
  local body="Signal from $from at $timestamp

Type: $type
Summary: $summary

Original signal file: $(basename "$signal_file")"

  # Deliver via gt mail if available, otherwise just log
  if command -v gt &>/dev/null; then
    echo "$body" | gt mail send "$recipient/" -s "$subject" -m "$(cat)" 2>/dev/null || \
      echo "  → Would deliver to $recipient: $subject"
  else
    echo "  → Signal for $recipient: $subject"
  fi
}

check_for_claim_conflict() {
  local signal_file="$1"
  local claims_dir="$2"
  local conflicts_dir="$3"
  local town_id="$4"

  local signal_id=$(basename "$signal_file" .md)
  local claim_pattern="$claims_dir/${signal_id}.claimed.*"

  # Count claims
  local claim_count=$(ls $claim_pattern 2>/dev/null | wc -l | tr -d ' ')

  if [[ "$claim_count" -gt 1 ]]; then
    # Conflict detected
    local conflict_file="$conflicts_dir/${signal_id}-conflict.md"

    if [[ ! -f "$conflict_file" ]]; then
      echo "signal_id: $signal_id" > "$conflict_file"
      echo "claimed_by:" >> "$conflict_file"
      for claim in $claim_pattern; do
        local claimer=$(cat "$claim")
        local claim_time=$(stat -f "%Sm" -t "%Y-%m-%dT%H:%M:%S" "$claim" 2>/dev/null || stat -c "%y" "$claim" 2>/dev/null | cut -d. -f1)
        echo "  - $claimer (at $claim_time)" >> "$conflict_file"
      done
      echo "status: needs_resolution" >> "$conflict_file"

      echo "  ⚠️  Conflict detected for signal $signal_id"
      return 1
    fi
  fi

  return 0
}

# --- Commands ---

cmd_check() {
  local rig_name=""

  # Parse arguments
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --rig)
        rig_name="$2"
        shift 2
        ;;
      *)
        shift
        ;;
    esac
  done

  # Find repo root
  local repo_root
  if [[ -n "$rig_name" ]]; then
    repo_root="$GASTOWN_ROOT/$rig_name/crew/spec_editor"  # Default to spec_editor worktree
  else
    repo_root=$(find_repo_root) || { echo "Not in a git repository"; exit 1; }
  fi

  local signals_inbox="$repo_root/$SIGNALS_DIR/inbox"
  local processed_file="$repo_root/$SIGNALS_DIR/.processed"
  local claims_dir="$repo_root/$SIGNALS_DIR/.claims"
  local conflicts_dir="$repo_root/$SIGNALS_DIR/conflicts"

  # Ensure directories exist
  ensure_signals_dir "$repo_root"

  # Get town ID
  local town_id=$(get_town_id)

  echo "Ambassador check (town: $town_id)"
  echo "================================"

  # Check if signals inbox exists and has files
  if [[ ! -d "$signals_inbox" ]]; then
    echo "No signals inbox found"
    return 0
  fi

  local signal_count=0
  local processed_count=0
  local delivered_count=0

  for signal_file in "$signals_inbox"/*.md; do
    [[ -f "$signal_file" ]] || continue
    ((signal_count++))

    local signal_id=$(basename "$signal_file" .md)

    # Skip if already processed
    if is_processed "$signal_id" "$processed_file"; then
      ((processed_count++))
      continue
    fi

    # Skip if from local town
    if is_local_signal "$signal_file" "$town_id"; then
      continue
    fi

    echo "Processing: $signal_id"

    # Check for conflicts on wildcard signals
    check_for_claim_conflict "$signal_file" "$claims_dir" "$conflicts_dir" "$town_id" || true

    # Deliver as mail
    deliver_signal_as_mail "$signal_file" "$town_id"

    # Mark as processed
    mark_processed "$signal_id" "$processed_file"
    ((delivered_count++))
  done

  echo ""
  echo "Summary: $signal_count signals, $processed_count already processed, $delivered_count newly delivered"
}

cmd_status() {
  local rig_name=""

  # Parse arguments
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --rig)
        rig_name="$2"
        shift 2
        ;;
      *)
        shift
        ;;
    esac
  done

  # Find repo root
  local repo_root
  if [[ -n "$rig_name" ]]; then
    repo_root="$GASTOWN_ROOT/$rig_name/crew/spec_editor"
  else
    repo_root=$(find_repo_root) || { echo "Not in a git repository"; exit 1; }
  fi

  local signals_inbox="$repo_root/$SIGNALS_DIR/inbox"
  local processed_file="$repo_root/$SIGNALS_DIR/.processed"
  local conflicts_dir="$repo_root/$SIGNALS_DIR/conflicts"

  local town_id=$(get_town_id)

  echo "Ambassador Status (town: $town_id)"
  echo "==================================="
  echo ""

  # Count signals
  local total=0
  local pending=0
  local local_signals=0

  if [[ -d "$signals_inbox" ]]; then
    for signal_file in "$signals_inbox"/*.md; do
      [[ -f "$signal_file" ]] || continue
      ((total++))

      local signal_id=$(basename "$signal_file" .md)

      if is_local_signal "$signal_file" "$town_id"; then
        ((local_signals++))
      elif ! is_processed "$signal_id" "$processed_file"; then
        ((pending++))
        echo "  📨 PENDING: $signal_id"
      fi
    done
  fi

  echo ""
  echo "Signals: $total total, $pending pending, $local_signals local (outbound)"

  # Check conflicts
  local conflicts=0
  if [[ -d "$conflicts_dir" ]]; then
    for conflict in "$conflicts_dir"/*-conflict.md; do
      [[ -f "$conflict" ]] || continue
      ((conflicts++))
      echo "  ⚠️  CONFLICT: $(basename "$conflict" .md)"
    done
  fi

  if [[ $conflicts -gt 0 ]]; then
    echo ""
    echo "Conflicts: $conflicts (need Overseer resolution)"
  fi
}

cmd_hygiene() {
  local archive_before=7
  local rig_name=""

  # Parse arguments
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --archive-before)
        archive_before="$2"
        shift 2
        ;;
      --rig)
        rig_name="$2"
        shift 2
        ;;
      *)
        shift
        ;;
    esac
  done

  # Find repo root
  local repo_root
  if [[ -n "$rig_name" ]]; then
    repo_root="$GASTOWN_ROOT/$rig_name/crew/spec_editor"
  else
    repo_root=$(find_repo_root) || { echo "Not in a git repository"; exit 1; }
  fi

  local signals_inbox="$repo_root/$SIGNALS_DIR/inbox"
  local processed_file="$repo_root/$SIGNALS_DIR/.processed"
  local archive_dir="$repo_root/$SIGNALS_DIR/archive"

  echo "Ambassador Hygiene"
  echo "=================="

  ensure_signals_dir "$repo_root"

  # Archive old processed signals
  local archived=0
  local cutoff_date=$(date -v-${archive_before}d +%Y%m%d 2>/dev/null || date -d "$archive_before days ago" +%Y%m%d)
  local archive_file="$archive_dir/$(date +%Y-%m).md"

  if [[ -d "$signals_inbox" ]]; then
    for signal_file in "$signals_inbox"/*.md; do
      [[ -f "$signal_file" ]] || continue

      local signal_id=$(basename "$signal_file" .md)
      local signal_date=$(echo "$signal_id" | grep -oE '^[0-9]{8}' || echo "")

      # Check if processed and old enough
      if is_processed "$signal_id" "$processed_file" && [[ -n "$signal_date" && "$signal_date" < "$cutoff_date" ]]; then
        echo "Archiving: $signal_id"

        # Append to archive
        echo "---" >> "$archive_file"
        echo "# $signal_id" >> "$archive_file"
        cat "$signal_file" >> "$archive_file"
        echo "" >> "$archive_file"

        # Remove original
        rm "$signal_file"
        ((archived++))
      fi
    done
  fi

  # Clean up processed file (remove archived entries)
  if [[ $archived -gt 0 && -f "$processed_file" ]]; then
    local temp_processed=$(mktemp)
    while read -r signal_id; do
      if [[ -f "$signals_inbox/${signal_id}.md" ]]; then
        echo "$signal_id" >> "$temp_processed"
      fi
    done < "$processed_file"
    mv "$temp_processed" "$processed_file"
  fi

  echo ""
  echo "Archived $archived signals older than $archive_before days"
}

# --- Main ---

case "${1:-}" in
  check)
    shift
    cmd_check "$@"
    ;;
  status)
    shift
    cmd_status "$@"
    ;;
  hygiene)
    shift
    cmd_hygiene "$@"
    ;;
  *)
    echo "Usage: gt-ambassador <command> [options]"
    echo ""
    echo "Commands:"
    echo "  check    Process inbound signals, deliver as mail"
    echo "  status   Show pending signals, claims, conflicts"
    echo "  hygiene  Archive old signals, clean up"
    echo ""
    echo "Options:"
    echo "  --rig <name>           Specify rig name"
    echo "  --archive-before <days> Days before archiving (default: 7)"
    exit 1
    ;;
esac

5.2 gt-signal

Create .scripts/gt-signal:

#!/bin/bash
# gt-signal: Emit outbound signals to other towns
#
# Usage:
#   gt-signal <type> <artifact> [message] [options]
#
# Types:
#   work-complete    - Work finished (spec, feature, etc.)
#   review-request   - Request review from another town
#   question         - Ask a question
#   blocker          - Report a blocker
#   ack              - Acknowledge receipt of signal
#
# Options:
#   --to <address>   - Route to specific recipient (default: */overseer)
#   --rig <name>     - Specify rig
#
# Examples:
#   gt-signal work-complete specs/EPUB-PARSER-SPEC.md "Spec finished"
#   gt-signal review-request specs/NML-SPEC.md --to "fred-acme/overseer"
#   gt-signal question "How should we handle DRM?" --to "*/mayor"

set -e

# --- Configuration ---
GASTOWN_ROOT="${GASTOWN_ROOT:-$HOME/Gastown}"
SIGNALS_DIR=".signals"

# --- Helper functions ---

get_town_id() {
  if command -v gh &>/dev/null; then
    gh api user --jq '.login' 2>/dev/null || echo "unknown"
  else
    local email=$(git config user.email 2>/dev/null)
    echo "${email%%@*}"
  fi
}

find_repo_root() {
  git rev-parse --show-toplevel 2>/dev/null
}

ensure_signals_dir() {
  local repo_root="$1"
  mkdir -p "$repo_root/$SIGNALS_DIR/inbox"
  mkdir -p "$repo_root/$SIGNALS_DIR/.claims"
  mkdir -p "$repo_root/$SIGNALS_DIR/conflicts"
  mkdir -p "$repo_root/$SIGNALS_DIR/archive"
}

generate_signal_id() {
  # Format: YYYYMMDDTHHMMSS-town-type-random
  local town="$1"
  local type="$2"
  local timestamp=$(date +%Y%m%dT%H%M%S)
  local random=$(head -c 4 /dev/urandom | xxd -p)

  echo "${timestamp}-${town}-${type}-${random}"
}

# --- Main ---

# Parse arguments
SIGNAL_TYPE=""
ARTIFACT=""
MESSAGE=""
TO_ADDRESS="*/overseer"
RIG_NAME=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --to)
      TO_ADDRESS="$2"
      shift 2
      ;;
    --rig)
      RIG_NAME="$2"
      shift 2
      ;;
    -h|--help)
      echo "Usage: gt-signal <type> <artifact> [message] [options]"
      echo ""
      echo "Types:"
      echo "  work-complete    Work finished"
      echo "  review-request   Request review"
      echo "  question         Ask a question"
      echo "  blocker          Report a blocker"
      echo "  ack              Acknowledge signal"
      echo ""
      echo "Options:"
      echo "  --to <address>   Route to recipient (default: */overseer)"
      echo "  --rig <name>     Specify rig"
      echo ""
      echo "Examples:"
      echo "  gt-signal work-complete specs/X.md \"Spec finished\""
      echo "  gt-signal review-request specs/X.md --to \"acme/overseer\""
      exit 0
      ;;
    -*)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
    *)
      if [[ -z "$SIGNAL_TYPE" ]]; then
        SIGNAL_TYPE="$1"
      elif [[ -z "$ARTIFACT" ]]; then
        ARTIFACT="$1"
      elif [[ -z "$MESSAGE" ]]; then
        MESSAGE="$1"
      fi
      shift
      ;;
  esac
done

# Validate
if [[ -z "$SIGNAL_TYPE" ]]; then
  echo "Error: Signal type required" >&2
  echo "Usage: gt-signal <type> <artifact> [message]" >&2
  exit 1
fi

case "$SIGNAL_TYPE" in
  work-complete|review-request|question|blocker|ack)
    ;;
  *)
    echo "Error: Unknown signal type: $SIGNAL_TYPE" >&2
    echo "Valid types: work-complete, review-request, question, blocker, ack" >&2
    exit 1
    ;;
esac

# Find repo root
if [[ -n "$RIG_NAME" ]]; then
  REPO_ROOT="$GASTOWN_ROOT/$RIG_NAME/crew/spec_editor"
else
  REPO_ROOT=$(find_repo_root) || { echo "Not in a git repository" >&2; exit 1; }
fi

# Ensure signals directory exists
ensure_signals_dir "$REPO_ROOT"

# Get town ID
TOWN_ID=$(get_town_id)
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# Generate signal ID
SIGNAL_ID=$(generate_signal_id "$TOWN_ID" "$SIGNAL_TYPE")

# Create signal file
SIGNAL_FILE="$REPO_ROOT/$SIGNALS_DIR/inbox/${SIGNAL_ID}.md"

cat > "$SIGNAL_FILE" << EOF
signal:
  id: $SIGNAL_ID
  type: $SIGNAL_TYPE
  from_town: $TOWN_ID
  timestamp: $TIMESTAMP

payload:
  artifact: $ARTIFACT
  summary: "${MESSAGE:-$SIGNAL_TYPE for $ARTIFACT}"

routing:
  to: "$TO_ADDRESS"
EOF

echo "Signal created: $SIGNAL_ID"
echo "  Type: $SIGNAL_TYPE"
echo "  From: $TOWN_ID"
echo "  To: $TO_ADDRESS"
echo "  Artifact: $ARTIFACT"
echo ""
echo "Signal file: $SIGNAL_FILE"
echo ""
echo "To broadcast: git add $SIGNAL_FILE && git commit -m 'Signal: $SIGNAL_TYPE' && git push"

5.3 gt-ambassador-claim

Create .scripts/gt-ambassador-claim:

#!/bin/bash
# gt-ambassador-claim: Claim a wildcard-addressed signal
#
# Usage:
#   gt-ambassador-claim <signal-id>
#
# When your town claims a wildcard signal (*/role), this creates a
# .claimed marker so other towns know not to process it.
#
# If another town has already claimed it, a conflict is created.

set -e

# --- Configuration ---
GASTOWN_ROOT="${GASTOWN_ROOT:-$HOME/Gastown}"
SIGNALS_DIR=".signals"

# --- Helper functions ---

get_town_id() {
  if command -v gh &>/dev/null; then
    gh api user --jq '.login' 2>/dev/null || echo "unknown"
  else
    local email=$(git config user.email 2>/dev/null)
    echo "${email%%@*}"
  fi
}

find_repo_root() {
  git rev-parse --show-toplevel 2>/dev/null
}

# --- Main ---

SIGNAL_ID="$1"

if [[ -z "$SIGNAL_ID" ]]; then
  echo "Usage: gt-ambassador-claim <signal-id>" >&2
  exit 1
fi

# Find repo root
REPO_ROOT=$(find_repo_root) || { echo "Not in a git repository" >&2; exit 1; }

SIGNALS_INBOX="$REPO_ROOT/$SIGNALS_DIR/inbox"
CLAIMS_DIR="$REPO_ROOT/$SIGNALS_DIR/.claims"
CONFLICTS_DIR="$REPO_ROOT/$SIGNALS_DIR/conflicts"

# Ensure directories exist
mkdir -p "$CLAIMS_DIR" "$CONFLICTS_DIR"

# Check if signal exists
SIGNAL_FILE="$SIGNALS_INBOX/${SIGNAL_ID}.md"
if [[ ! -f "$SIGNAL_FILE" ]]; then
  # Try partial match
  MATCHES=($(ls "$SIGNALS_INBOX"/*"$SIGNAL_ID"*.md 2>/dev/null || true))
  if [[ ${#MATCHES[@]} -eq 0 ]]; then
    echo "Signal not found: $SIGNAL_ID" >&2
    exit 1
  elif [[ ${#MATCHES[@]} -gt 1 ]]; then
    echo "Multiple matches for $SIGNAL_ID:" >&2
    printf "  %s\n" "${MATCHES[@]}"
    exit 1
  fi
  SIGNAL_FILE="${MATCHES[0]}"
  SIGNAL_ID=$(basename "$SIGNAL_FILE" .md)
fi

# Get town ID
TOWN_ID=$(get_town_id)
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# Check if signal is wildcard-addressed
TO_ADDRESS=$(grep "^  to:" "$SIGNAL_FILE" 2>/dev/null | awk '{print $2}' | tr -d '"')
if [[ "$TO_ADDRESS" != "*/"* && "$TO_ADDRESS" != "*" ]]; then
  echo "Signal is not wildcard-addressed (to: $TO_ADDRESS)" >&2
  echo "Only wildcard signals need claiming" >&2
  exit 1
fi

# Check for existing claims
CLAIM_FILE="$CLAIMS_DIR/${SIGNAL_ID}.claimed"
EXISTING_CLAIMS=($(ls "$CLAIMS_DIR/${SIGNAL_ID}.claimed."* 2>/dev/null || true))

# Check if we already claimed
OUR_CLAIM="$CLAIMS_DIR/${SIGNAL_ID}.claimed.$TOWN_ID"
if [[ -f "$OUR_CLAIM" ]]; then
  echo "Already claimed by us ($TOWN_ID)"
  exit 0
fi

# Create our claim
echo "$TOWN_ID" > "$OUR_CLAIM"
echo "$TIMESTAMP" >> "$OUR_CLAIM"

echo "Claimed signal: $SIGNAL_ID"
echo "  Town: $TOWN_ID"
echo "  Time: $TIMESTAMP"

# Check for conflicts (other towns also claimed)
OTHER_CLAIMS=($(ls "$CLAIMS_DIR/${SIGNAL_ID}.claimed."* 2>/dev/null | grep -v "$TOWN_ID" || true))

if [[ ${#OTHER_CLAIMS[@]} -gt 0 ]]; then
  echo ""
  echo "⚠️  CONFLICT DETECTED"
  echo "Other towns have also claimed this signal:"

  CONFLICT_FILE="$CONFLICTS_DIR/${SIGNAL_ID}-conflict.md"

  cat > "$CONFLICT_FILE" << EOF
signal_id: $SIGNAL_ID
status: needs_resolution
claimed_by:
EOF

  # Add all claims
  for claim_file in "$CLAIMS_DIR/${SIGNAL_ID}.claimed."*; do
    [[ -f "$claim_file" ]] || continue
    claimer=$(head -1 "$claim_file")
    claim_time=$(tail -1 "$claim_file")
    echo "  - $claimer (at $claim_time)" >> "$CONFLICT_FILE"
    echo "  - $claimer (at $claim_time)"
  done

  echo ""
  echo "Conflict recorded: $CONFLICT_FILE"
  echo "Overseer should resolve which town handles this signal."
fi

echo ""
echo "Don't forget to commit and push the claim!"

5.4 gt-ambassador-resolve

Create .scripts/gt-ambassador-resolve:

#!/bin/bash
# gt-ambassador-resolve: Resolve a signal claim conflict
#
# Usage:
#   gt-ambassador-resolve <signal-id> <winning-town>
#   gt-ambassador-resolve --list
#
# When multiple towns claim the same wildcard signal, the Overseer
# must decide which town handles it. This command records the resolution.

set -e

# --- Configuration ---
GASTOWN_ROOT="${GASTOWN_ROOT:-$HOME/Gastown}"
SIGNALS_DIR=".signals"

# --- Helper functions ---

get_town_id() {
  if command -v gh &>/dev/null; then
    gh api user --jq '.login' 2>/dev/null || echo "unknown"
  else
    local email=$(git config user.email 2>/dev/null)
    echo "${email%%@*}"
  fi
}

find_repo_root() {
  git rev-parse --show-toplevel 2>/dev/null
}

list_conflicts() {
  local conflicts_dir="$1"

  if [[ ! -d "$conflicts_dir" ]]; then
    echo "No conflicts directory found"
    return
  fi

  local count=0
  for conflict_file in "$conflicts_dir"/*-conflict.md; do
    [[ -f "$conflict_file" ]] || continue
    ((count++))

    local signal_id=$(grep "^signal_id:" "$conflict_file" | awk '{print $2}')
    local status=$(grep "^status:" "$conflict_file" | awk '{print $2}')

    echo "[$status] $signal_id"

    # Show claimers
    grep "^  - " "$conflict_file" | while read -r line; do
      echo "    $line"
    done
    echo ""
  done

  if [[ $count -eq 0 ]]; then
    echo "No conflicts found"
  fi
}

# --- Main ---

# Find repo root
REPO_ROOT=$(find_repo_root) || { echo "Not in a git repository" >&2; exit 1; }

SIGNALS_INBOX="$REPO_ROOT/$SIGNALS_DIR/inbox"
CLAIMS_DIR="$REPO_ROOT/$SIGNALS_DIR/.claims"
CONFLICTS_DIR="$REPO_ROOT/$SIGNALS_DIR/conflicts"
PROCESSED_FILE="$REPO_ROOT/$SIGNALS_DIR/.processed"

# Handle --list
if [[ "$1" == "--list" || "$1" == "-l" ]]; then
  echo "Signal Conflicts"
  echo "================"
  list_conflicts "$CONFLICTS_DIR"
  exit 0
fi

# Parse arguments
SIGNAL_ID="$1"
WINNING_TOWN="$2"

if [[ -z "$SIGNAL_ID" || -z "$WINNING_TOWN" ]]; then
  echo "Usage: gt-ambassador-resolve <signal-id> <winning-town>" >&2
  echo "       gt-ambassador-resolve --list" >&2
  exit 1
fi

# Find conflict file
CONFLICT_FILE="$CONFLICTS_DIR/${SIGNAL_ID}-conflict.md"
if [[ ! -f "$CONFLICT_FILE" ]]; then
  # Try partial match
  MATCHES=($(ls "$CONFLICTS_DIR"/*"$SIGNAL_ID"*-conflict.md 2>/dev/null || true))
  if [[ ${#MATCHES[@]} -eq 0 ]]; then
    echo "Conflict not found: $SIGNAL_ID" >&2
    exit 1
  elif [[ ${#MATCHES[@]} -gt 1 ]]; then
    echo "Multiple matches:" >&2
    printf "  %s\n" "${MATCHES[@]}"
    exit 1
  fi
  CONFLICT_FILE="${MATCHES[0]}"
  SIGNAL_ID=$(basename "$CONFLICT_FILE" -conflict.md)
fi

# Verify winning town is one of the claimers
if ! grep -q "^  - $WINNING_TOWN " "$CONFLICT_FILE"; then
  echo "Error: $WINNING_TOWN is not a claimer for this signal" >&2
  echo "Claimers:"
  grep "^  - " "$CONFLICT_FILE"
  exit 1
fi

# Get our town ID
TOWN_ID=$(get_town_id)

echo "Resolving conflict: $SIGNAL_ID"
echo "  Winner: $WINNING_TOWN"
echo ""

# Update conflict file
sed -i.bak "s/^status: needs_resolution/status: resolved/" "$CONFLICT_FILE"
echo "resolved_by: $TOWN_ID" >> "$CONFLICT_FILE"
echo "winner: $WINNING_TOWN" >> "$CONFLICT_FILE"
echo "resolved_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$CONFLICT_FILE"
rm -f "$CONFLICT_FILE.bak"

# Remove losing claims
for claim_file in "$CLAIMS_DIR/${SIGNAL_ID}.claimed."*; do
  [[ -f "$claim_file" ]] || continue
  claimer=$(head -1 "$claim_file")
  if [[ "$claimer" != "$WINNING_TOWN" ]]; then
    echo "Removing claim: $claimer"
    rm "$claim_file"
  fi
done

# If we're not the winner, mark signal as processed (don't handle it)
if [[ "$TOWN_ID" != "$WINNING_TOWN" ]]; then
  echo "$SIGNAL_ID" >> "$PROCESSED_FILE"
  echo "Signal marked as processed (handled by $WINNING_TOWN)"
fi

echo ""
echo "Conflict resolved. Don't forget to commit and push!"

5.5 gt-ambassador-watch-beads

Create .scripts/gt-ambassador-watch-beads:

#!/bin/bash
# gt-ambassador-watch-beads: Watch for bead closures and create signals
#
# Usage:
#   gt-ambassador-watch-beads [--rig <name>]
#
# This script scans for recently closed beads that don't have corresponding
# signals yet and creates work-complete signals for them.
#
# Typically called:
#   - Before git push (to include signals in the push)
#   - As part of Mayor session workflow
#   - By gt-idle-poll when checking for activity

set -e

# --- Configuration ---
GASTOWN_ROOT="${GASTOWN_ROOT:-$HOME/Gastown}"
SIGNALS_DIR=".signals"

# --- Helper functions ---

get_town_id() {
  if command -v gh &>/dev/null; then
    gh api user --jq '.login' 2>/dev/null || echo "unknown"
  else
    local email=$(git config user.email 2>/dev/null)
    echo "${email%%@*}"
  fi
}

find_rig_root() {
  local start="${1:-$(pwd)}"
  local current="$start"

  while [[ "$current" != "/" ]]; do
    if [[ -f "$current/config.json" ]]; then
      echo "$current"
      return 0
    fi
    current=$(dirname "$current")
  done

  return 1
}

find_repo_root() {
  git rev-parse --show-toplevel 2>/dev/null
}

ensure_signals_dir() {
  local repo_root="$1"
  mkdir -p "$repo_root/$SIGNALS_DIR/inbox"
}

generate_signal_id() {
  local town="$1"
  local type="$2"
  local timestamp=$(date +%Y%m%dT%H%M%S)
  local random=$(head -c 4 /dev/urandom | xxd -p)
  echo "${timestamp}-${town}-${type}-${random}"
}

get_signaled_beads() {
  # Get list of bead IDs that already have signals
  local signals_inbox="$1"
  local town_id="$2"

  if [[ ! -d "$signals_inbox" ]]; then
    return
  fi

  for signal_file in "$signals_inbox"/*-"$town_id"-work-complete-*.md; do
    [[ -f "$signal_file" ]] || continue
    # Extract bead ID from artifact field
    grep "^  artifact:" "$signal_file" 2>/dev/null | sed 's/.*bead://' | awk '{print $1}'
  done
}

# --- Main ---

RIG_NAME=""
VERBOSE=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --rig)
      RIG_NAME="$2"
      shift 2
      ;;
    -v|--verbose)
      VERBOSE=true
      shift
      ;;
    *)
      shift
      ;;
  esac
done

# Find rig root
if [[ -n "$RIG_NAME" ]]; then
  RIG_ROOT="$GASTOWN_ROOT/$RIG_NAME"
else
  RIG_ROOT=$(find_rig_root) || { echo "Not in a rig" >&2; exit 1; }
fi

# Find a repo root (use spec_editor as default)
REPO_ROOT=""
for crew_dir in "$RIG_ROOT/crew/"*/; do
  if [[ -d "$crew_dir/.git" ]]; then
    REPO_ROOT="$crew_dir"
    break
  fi
done

if [[ -z "$REPO_ROOT" ]]; then
  echo "No git repo found in rig" >&2
  exit 1
fi

# Get town ID
TOWN_ID=$(get_town_id)

# Ensure signals directory
ensure_signals_dir "$REPO_ROOT"

SIGNALS_INBOX="$REPO_ROOT/$SIGNALS_DIR/inbox"
BEADS_DB="$RIG_ROOT/.beads/beads.db"

if [[ ! -f "$BEADS_DB" ]]; then
  [[ "$VERBOSE" == "true" ]] && echo "No beads database found"
  exit 0
fi

# Get list of already-signaled beads
declare -A SIGNALED_BEADS
while read -r bead_id; do
  SIGNALED_BEADS["$bead_id"]=1
done < <(get_signaled_beads "$SIGNALS_INBOX" "$TOWN_ID")

# Query closed beads from last 24 hours using bd list
# Note: This uses bd command to get closed issues
CLOSED_BEADS=$(bd list --status=closed --output=json 2>/dev/null | jq -r '.[] | "\(.id)|\(.title)|\(.type)"' 2>/dev/null || echo "")

if [[ -z "$CLOSED_BEADS" ]]; then
  [[ "$VERBOSE" == "true" ]] && echo "No closed beads found"
  exit 0
fi

SIGNALS_CREATED=0

while IFS='|' read -r bead_id title bead_type; do
  [[ -z "$bead_id" ]] && continue

  # Skip if already signaled
  if [[ -n "${SIGNALED_BEADS[$bead_id]:-}" ]]; then
    [[ "$VERBOSE" == "true" ]] && echo "Already signaled: $bead_id"
    continue
  fi

  # Create signal
  SIGNAL_ID=$(generate_signal_id "$TOWN_ID" "work-complete")
  SIGNAL_FILE="$SIGNALS_INBOX/${SIGNAL_ID}.md"
  TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

  cat > "$SIGNAL_FILE" << EOF
signal:
  id: $SIGNAL_ID
  type: work-complete
  from_town: $TOWN_ID
  timestamp: $TIMESTAMP

payload:
  artifact: bead:$bead_id
  bead_type: $bead_type
  summary: "$title"

routing:
  to: "*/overseer"
EOF

  echo "Signal created for bead $bead_id: $title"
  ((SIGNALS_CREATED++))

done <<< "$CLOSED_BEADS"

if [[ $SIGNALS_CREATED -gt 0 ]]; then
  echo ""
  echo "Created $SIGNALS_CREATED signal(s). Don't forget to commit and push!"
fi

5.6 sync-overseer

Create .scripts/sync-overseer:

#!/bin/bash
# sync-overseer: Pull updates to overseer shadow clone after crew pushes
#
# Usage: Called after `git push` in any Gas Town crew worktree
# Maps rig name to $OVERSEER_ROOT/<rig> and pulls latest
#
# Configuration:
#   OVERSEER_ROOT - Base directory for overseer clones (default: ~/workspace/overseer)
#
# Supports multiple rigs - each rig has its own overseer shadow

set -e

# --- Configuration ---
OVERSEER_ROOT="${OVERSEER_ROOT:-$HOME/workspace/overseer}"

# Detect current git repo root
repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
if [[ -z "$repo_root" ]]; then
  echo "sync-overseer: not in a git repository" >&2
  exit 1
fi

# Extract rig name from path
# Gas Town structure: ~/Gastown/<rig>/crew/<name>/ or ~/Gastown/<rig>/polecats/<name>/
# Rig root is identified by config.json (unique to rig level, not in repo)

current="$repo_root"
rig_name=""
while [[ "$current" != "/" ]]; do
  if [[ -f "$current/config.json" ]]; then
    rig_name=$(basename "$current")
    break
  fi
  current=$(dirname "$current")
done

if [[ -z "$rig_name" ]]; then
  # Fallback: try to extract from remote URL
  remote_url=$(git remote get-url origin 2>/dev/null || echo "")
  if [[ -n "$remote_url" ]]; then
    # Extract repo name from URL (e.g., audiobook-generator -> audiobook)
    repo_name=$(basename "$remote_url" .git)
    # Strip common suffixes
    rig_name="${repo_name%-generator}"
    rig_name="${rig_name%-pipeline}"
    rig_name="${rig_name%-project}"
  fi
fi

if [[ -z "$rig_name" ]]; then
  echo "sync-overseer: could not determine rig name" >&2
  exit 1
fi

overseer_path="$OVERSEER_ROOT/$rig_name"

if [[ -d "$overseer_path/.git" ]]; then
  echo "sync-overseer: pulling $overseer_path"
  cd "$overseer_path" && git pull --ff-only
else
  echo "sync-overseer: no overseer clone at $overseer_path (skipping)"
fi

5.7 gt-idle-poll

Create .scripts/gt-idle-poll:

#!/bin/bash
# gt-idle-poll: Poll for signals when Gas Town is idle
#
# Run via cron: */5 * * * * /path/to/repo/.scripts/gt-idle-poll
#
# Configuration:
#   OVERSEER_ROOT - Base directory for overseer clones (default: ~/workspace/overseer)
#   GASTOWN_ROOT  - Gas Town root directory (default: ~/Gastown)
#
# Behavior:
# - Skips if Mayor session is active
# - For each overseer mirror, fetches and checks for new signals
# - Notifies human via desktop notification (if enabled)
# - Queues signals as mail for next Mayor session

set -e

# --- Configuration ---
OVERSEER_ROOT="${OVERSEER_ROOT:-$HOME/workspace/overseer}"
GASTOWN_ROOT="${GASTOWN_ROOT:-$HOME/Gastown}"

# --- Helper functions ---

notify_desktop() {
  local title="$1"
  local message="$2"

  if [[ "$(uname)" == "Darwin" ]]; then
    osascript -e "display notification \"$message\" with title \"$title\""
  elif command -v notify-send &>/dev/null; then
    notify-send "$title" "$message"
  fi
}

is_mayor_active() {
  # Check for active Mayor tmux session
  tmux has-session -t mayor 2>/dev/null && return 0

  # Also check for any Gas Town mayor sessions (pattern: *-mayor)
  tmux list-sessions 2>/dev/null | grep -q "mayor" && return 0

  return 1
}

notifications_enabled() {
  local overseer_dir="$1"
  local config_file="$overseer_dir/.overseer-config"

  # Default: enabled
  if [[ ! -f "$config_file" ]]; then
    return 0
  fi

  # Check for explicit disable
  if grep -q "^notifications:.*false" "$config_file" 2>/dev/null; then
    return 1
  fi

  return 0
}

# --- Main ---

# Skip if Mayor is active
if is_mayor_active; then
  exit 0
fi

# Check if overseer root exists
if [[ ! -d "$OVERSEER_ROOT" ]]; then
  exit 0
fi

# Track if we found any new signals
total_new_signals=0
rigs_with_signals=""

# For each overseer mirror
for overseer_dir in "$OVERSEER_ROOT"/*/; do
  [[ -d "$overseer_dir/.git" ]] || continue

  rig_name=$(basename "$overseer_dir")

  # Fetch latest
  cd "$overseer_dir"
  git fetch --quiet 2>/dev/null || continue

  # Check for new commits
  new_commits=$(git rev-list HEAD..origin/main --count 2>/dev/null || echo "0")

  if [[ "$new_commits" -gt 0 ]]; then
    # Pull the mirror
    git pull --ff-only --quiet 2>/dev/null || continue

    # Check for new signals in .signals/inbox/
    if [[ -d ".signals/inbox" ]]; then
      # Count unprocessed signals (not in .processed)
      processed_file=".signals/.processed"
      new_signals=0

      for signal_file in .signals/inbox/*.md; do
        [[ -f "$signal_file" ]] || continue
        signal_id=$(basename "$signal_file" .md)

        if [[ ! -f "$processed_file" ]] || ! grep -q "^$signal_id$" "$processed_file"; then
          ((new_signals++)) || true
        fi
      done

      if [[ $new_signals -gt 0 ]]; then
        ((total_new_signals += new_signals)) || true
        rigs_with_signals="$rigs_with_signals $rig_name"
      fi
    fi
  fi
done

# Notify if signals found
if [[ $total_new_signals -gt 0 ]]; then
  # Check if any overseer has notifications enabled
  for overseer_dir in "$OVERSEER_ROOT"/*/; do
    [[ -d "$overseer_dir" ]] || continue

    if notifications_enabled "$overseer_dir"; then
      notify_desktop "Gas Town" "$total_new_signals new signal(s) in:$rigs_with_signals"
      break  # Only notify once
    fi
  done
fi

6. Overseer Setup

6.1 Configure Overseer Root

The overseer clone location is configurable via the OVERSEER_ROOT environment variable:

# Default location
export OVERSEER_ROOT="$HOME/workspace/overseer"

# Or use a custom location
export OVERSEER_ROOT="$HOME/review"

Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.) to persist it.

6.2 Create Overseer Clone

The Overseer (human) needs a full checkout for review:

mkdir -p "$OVERSEER_ROOT"
git clone <repo-url> "$OVERSEER_ROOT/<project-name>"

6.3 Overseer Config

Create $OVERSEER_ROOT/<project>/.overseer-config:

# Overseer Configuration
# notifications: true|false - Desktop notifications when signals arrive
notifications: true

6.4 Workflow

After each push from crew:

.scripts/sync-overseer

This pulls updates to the overseer mirror automatically.

6.5 Idle Polling (Optional)

For notification when town is idle:

crontab -e
# Add (use full path to the script):
*/5 * * * * /path/to/repo/.scripts/gt-idle-poll

7. Operational Setup

After creating the directory structure and files, you need to set up the Gas Town operational layer.

7.1 Create Crew Members

Crew members are persistent workspaces managed by the Overseer. Create them with:

# From the rig directory or specify --rig
gt crew add product_manager --rig <rigname>
gt crew add spec_editor --rig <rigname>
gt crew add archivist --rig <rigname>
gt crew add blogger --rig <rigname>
gt crew add ambassador --rig <rigname>

Each crew member gets:

  • A sparse clone of the repository at <rig>/crew/<name>/
  • Access to their role definition at crew/<name>/ROLE.md
  • Mail directory for message delivery
  • Integration with Gas Town handoff mechanics

7.2 Start Gas Town Services

Bring up the orchestration layer:

gt up

This starts:

  • Daemon - Background process for agent coordination
  • Deacon - Health orchestrator (monitors Mayor/Witnesses)
  • Mayor - Global work coordinator
  • Witnesses - Per-rig polecat managers
  • Refineries - Per-rig merge queue processors

Verify with:

gt status

7.3 Configure Namepool (for Polecats)

If you want polecats (ephemeral workers) to spawn for tasks:

cd <rig-directory>
gt namepool set mad-max    # Or: minerals, wasteland

Available themes:

  • mad-max - furiosa, nux, slit, rictus, etc.
  • minerals - obsidian, quartz, jasper, onyx, etc.
  • wasteland - rust, chrome, nitro, guzzle, etc.

7.4 Mayor Coordination Protocol

IMPORTANT: The blogger and archivist behaviors are NOT automated hooks. They are Mayor responsibilities defined in AGENTS.md protocols.

The Mayor must invoke these behaviors at appropriate moments:

When to Invoke Blogger

  • After completing a specification
  • After significant commits
  • When the Overseer makes pivotal decisions
  • At major phase transitions
  • When capturing Overseer impressions about the development process

When to Invoke Archivist

  • After conducting web searches
  • When referencing external documentation
  • When making design decisions based on research
  • When specifications are revised based on new information

How the Mayor Invokes Crew Behaviors

The Mayor doesn't literally "call" the blogger - the Mayor performs the blogging by following the Blogger Protocol in AGENTS.md. Same for archiving. The role definitions tell the Mayor:

  • What to capture
  • What format to use
  • Where to store outputs

Example Mayor workflow after completing a spec:

  1. Check Blogger Protocol in AGENTS.md
  2. Determine if this event warrants a blog post
  3. Write the post following the format in crew/blogger/ROLE.md
  4. Save to project-blog/NN-title.md
  5. Commit and push

7.5 Crew vs Polecats

Aspect Crew Polecats
Creation gt crew add <name> Auto-spawned by Witness
Lifespan Persistent Ephemeral (task-scoped)
Management Human (Overseer) Witness/Deacon
Use case Ongoing roles (blogger, spec_editor) Parallel implementation tasks
Git setup Independent sparse clones Worktrees from mayor/rig

Crew is for defined roles that persist across sessions. Polecats are for parallelizing implementation work.

7.6 Starting Crew Sessions

To start a crew member's Claude session:

gt crew start blogger --rig <rigname>

To attach to an existing session:

gt crew at blogger --rig <rigname>

To list crew status:

gt crew list --rig <rigname>

8. Bootstrap Checklist

When setting up a new project:

Phase 1: Repository Setup

  • Create directory structure (Section 2)
  • Create AGENTS.md from template (Section 3)
  • Create crew role definitions (Section 4)
  • Create scripts in .scripts/ (Section 5)
  • Make scripts executable: chmod +x .scripts/*
  • Initialize signals: touch .signals/.processed
  • Create initial blog post: project-blog/00-project-genesis.md
  • Commit and push

Phase 2: Gas Town Setup

  • Add rig to Gas Town: gt rig add <rigname> <repo-url>
  • Create crew members (Section 7.1):
    • gt crew add product_manager --rig <rigname>
    • gt crew add spec_editor --rig <rigname>
    • gt crew add archivist --rig <rigname>
    • gt crew add blogger --rig <rigname>
    • gt crew add ambassador --rig <rigname>
  • Configure namepool (if using polecats): gt namepool set mad-max
  • Start services: gt up
  • Verify status: gt status

Phase 3: Overseer Setup (Optional)

  • Set OVERSEER_ROOT environment variable (if using non-default location)
  • Set up overseer clone (Section 6)
  • Configure notifications in .overseer-config

Quick Bootstrap Commands

# 1. Create directories
mkdir -p .scripts .signals/inbox .signals/.claims .signals/conflicts .signals/archive
mkdir -p archive/research archive/decisions archive/assets
mkdir -p crew/product_manager crew/spec_editor crew/archivist crew/blogger crew/ambassador
mkdir -p project-blog specs/examples
touch .signals/.processed

# 2. Create scripts (copy content from Section 5)
# For each script, create the file and paste the content:
#   .scripts/gt-ambassador
#   .scripts/gt-signal
#   .scripts/gt-ambassador-claim
#   .scripts/gt-ambassador-resolve
#   .scripts/gt-ambassador-watch-beads
#   .scripts/sync-overseer
#   .scripts/gt-idle-poll

# 3. Make scripts executable
chmod +x .scripts/*

# 4. Create AGENTS.md and role files (use templates from Sections 3-4)

# 5. Set up overseer clone (optional, configure OVERSEER_ROOT first if needed)
export OVERSEER_ROOT="${OVERSEER_ROOT:-$HOME/workspace/overseer}"
mkdir -p "$OVERSEER_ROOT"
git clone <this-repo> "$OVERSEER_ROOT/<project-name>"

# 6. Initial commit
git add .
git commit -m "Bootstrap Gas Town infrastructure"
git push

Changelog

Version Date Changes
1.2.0 2026-01-16 Added Operational Setup section: crew creation, gt up, namepool, Mayor coordination protocol
1.1.0 2026-01-16 Added full script sources, renamed d3 to product_manager, made OVERSEER_ROOT configurable
1.0.0 2026-01-16 Initial bootstrap document
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment