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.
- Overview
- Directory Structure
- AGENTS.md Template
- Crew Role Definitions
- Scripts
- Overseer Setup
- Operational Setup
- Bootstrap Checklist
| 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 |
- 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)
- Crew works in worktrees, commits to main
- Ambassador creates signals on significant work
sync-overseerpulls updates to Overseer's review clone- Blogger captures significant moments
- Archivist tracks research and decisions
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/.processedCreate 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.**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 commitmentsCreate 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 archiveCreate 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
\`\`\`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 narrativeCreate 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)Create .scripts/ directory and add the following scripts. Make all scripts executable with chmod +x .scripts/*.
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
;;
esacCreate .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"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!"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!"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!"
fiCreate .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)"
fiCreate .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
fiThe 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.
The Overseer (human) needs a full checkout for review:
mkdir -p "$OVERSEER_ROOT"
git clone <repo-url> "$OVERSEER_ROOT/<project-name>"Create $OVERSEER_ROOT/<project>/.overseer-config:
# Overseer Configuration
# notifications: true|false - Desktop notifications when signals arrive
notifications: trueAfter each push from crew:
.scripts/sync-overseerThis pulls updates to the overseer mirror automatically.
For notification when town is idle:
crontab -e
# Add (use full path to the script):
*/5 * * * * /path/to/repo/.scripts/gt-idle-pollAfter creating the directory structure and files, you need to set up the Gas Town operational layer.
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
Bring up the orchestration layer:
gt upThis 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 statusIf you want polecats (ephemeral workers) to spawn for tasks:
cd <rig-directory>
gt namepool set mad-max # Or: minerals, wastelandAvailable themes:
mad-max- furiosa, nux, slit, rictus, etc.minerals- obsidian, quartz, jasper, onyx, etc.wasteland- rust, chrome, nitro, guzzle, etc.
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:
- 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
- After conducting web searches
- When referencing external documentation
- When making design decisions based on research
- When specifications are revised based on new information
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:
- Check Blogger Protocol in AGENTS.md
- Determine if this event warrants a blog post
- Write the post following the format in
crew/blogger/ROLE.md - Save to
project-blog/NN-title.md - Commit and push
| 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.
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>When setting up a new project:
- Create directory structure (Section 2)
- Create
AGENTS.mdfrom 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
- 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
- Set
OVERSEER_ROOTenvironment variable (if using non-default location) - Set up overseer clone (Section 6)
- Configure notifications in
.overseer-config
# 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| 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 |