Add patrol awareness and wiring to gt rig add / gt doctor (gt-7923)

gt rig add changes:
- Seed patrol molecules (Deacon, Witness, Refinery) when adding rig
- Create plugin directories at town (~gt/plugins/) and rig levels
- Update command documentation and structure output

gt doctor changes:
- patrol-molecules-exist: Verify patrol molecules exist
- patrol-hooks-wired: Verify daemon triggers patrols
- patrol-not-stuck: Detect stale wisps (>1h in_progress)
- patrol-plugins-accessible: Verify plugin directories exist
- patrol-roles-have-prompts: Verify role prompts exist

All patrol checks support --fix where applicable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-23 00:17:32 -08:00
parent 66710aef69
commit 0b5fda69fd
5 changed files with 605 additions and 4 deletions

View File

@@ -269,7 +269,7 @@
{"id":"gt-ca4v.7","title":"More branches to process?","description":"More branches to process?\n\nIf yes: Return to process-branch with next branch.\nIf no: Continue to generate-summary.\n\nTrack: branches processed, branches skipped (with reasons).\n\ninstantiated_from: mol-refinery-patrol\nstep: loop-check","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-22T17:13:47.746798-08:00","updated_at":"2025-12-22T17:13:47.746798-08:00","dependencies":[{"issue_id":"gt-ca4v.7","depends_on_id":"gt-ca4v","type":"parent-child","created_at":"2025-12-22T17:13:47.747125-08:00","created_by":"daemon"},{"issue_id":"gt-ca4v.7","depends_on_id":"gt-ca4v.6","type":"blocks","created_at":"2025-12-22T17:13:48.464174-08:00","created_by":"daemon"}]}
{"id":"gt-ca4v.8","title":"Summarize this patrol cycle.","description":"Summarize this patrol cycle.\n\nInclude:\n- Branches processed (count, names)\n- Test results (pass/fail)\n- Issues filed (if any)\n- Branches skipped (with reasons)\n- Any escalations sent\n\nThis becomes the digest when the patrol is squashed.\n\ninstantiated_from: mol-refinery-patrol\nstep: generate-summary","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-22T17:13:47.827331-08:00","updated_at":"2025-12-22T17:13:47.827331-08:00","dependencies":[{"issue_id":"gt-ca4v.8","depends_on_id":"gt-ca4v","type":"parent-child","created_at":"2025-12-22T17:13:47.827682-08:00","created_by":"daemon"},{"issue_id":"gt-ca4v.8","depends_on_id":"gt-ca4v.7","type":"blocks","created_at":"2025-12-22T17:13:48.545582-08:00","created_by":"daemon"}]}
{"id":"gt-ca4v.9","title":"Check own context usage.","description":"Check own context usage.\n\nIf context is HIGH (\u003e80%):\n- Write handoff summary\n- Prepare for burn/respawn\n\nIf context is LOW:\n- Can continue processing\n\ninstantiated_from: mol-refinery-patrol\nstep: context-check","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-22T17:13:47.909098-08:00","updated_at":"2025-12-22T17:13:47.909098-08:00","dependencies":[{"issue_id":"gt-ca4v.9","depends_on_id":"gt-ca4v","type":"parent-child","created_at":"2025-12-22T17:13:47.909418-08:00","created_by":"daemon"},{"issue_id":"gt-ca4v.9","depends_on_id":"gt-ca4v.8","type":"blocks","created_at":"2025-12-22T17:13:48.621628-08:00","created_by":"daemon"}]}
{"id":"gt-caih","title":"Witness handoff bead state persistence","description":"Implement state persistence for Witness across wisp cycles.\n\n## Problem\nWisps burn between cycles, but Witness needs to remember:\n- Which workers have been nudged\n- How many times (nudge_count)\n- When was last nudge\n- Last observed activity\n\n## Solution\nWitness handoff bead with worker_states field:\n\n```json\n{\n \"id\": \"gt-witness-state\",\n \"type\": \"handoff\",\n \"assignee\": \"\u003crig\u003e/witness\",\n \"pinned\": true,\n \"worker_states\": {\n \"furiosa\": {\n \"issue\": \"gt-123\",\n \"nudge_count\": 2,\n \"last_nudge\": \"2024-12-22T10:00:00Z\"\n }\n },\n \"last_patrol\": \"2024-12-22T10:05:00Z\"\n}\n```\n\n## Implementation\n1. On patrol start: bd show \u003cwitness-handoff-id\u003e to load state\n2. During patrol: update in-memory state\n3. On save-state step: bd update to persist\n4. State survives wisp burn/squash\n\n## Depends on\n- gt-83k0 (mol-witness-patrol definition)","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/furiosa","created_at":"2025-12-22T16:42:57.427131-08:00","updated_at":"2025-12-23T00:06:13.00402-08:00","dependencies":[{"issue_id":"gt-caih","depends_on_id":"gt-83k0","type":"blocks","created_at":"2025-12-22T16:43:59.609821-08:00","created_by":"daemon"}]}
{"id":"gt-caih","title":"Witness handoff bead state persistence","description":"Implement state persistence for Witness across wisp cycles.\n\n## Problem\nWisps burn between cycles, but Witness needs to remember:\n- Which workers have been nudged\n- How many times (nudge_count)\n- When was last nudge\n- Last observed activity\n\n## Solution\nWitness handoff bead with worker_states field:\n\n```json\n{\n \"id\": \"gt-witness-state\",\n \"type\": \"handoff\",\n \"assignee\": \"\u003crig\u003e/witness\",\n \"pinned\": true,\n \"worker_states\": {\n \"furiosa\": {\n \"issue\": \"gt-123\",\n \"nudge_count\": 2,\n \"last_nudge\": \"2024-12-22T10:00:00Z\"\n }\n },\n \"last_patrol\": \"2024-12-22T10:05:00Z\"\n}\n```\n\n## Implementation\n1. On patrol start: bd show \u003cwitness-handoff-id\u003e to load state\n2. During patrol: update in-memory state\n3. On save-state step: bd update to persist\n4. State survives wisp burn/squash\n\n## Depends on\n- gt-83k0 (mol-witness-patrol definition)","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-22T16:42:57.427131-08:00","updated_at":"2025-12-22T16:42:57.427131-08:00","dependencies":[{"issue_id":"gt-caih","depends_on_id":"gt-83k0","type":"blocks","created_at":"2025-12-22T16:43:59.609821-08:00","created_by":"daemon"}]}
{"id":"gt-caz","title":"Timed Beads: Scheduled recurring work","description":"## Summary\n\nTimed beads wake up periodically and get injected into the ready queue by the daemon.\n\n## Schema Extension\n\n```yaml\nid: gt-weekly-sync\ntype: task # or sentinel\nschedule: \"0 9 * * 1\" # cron: Monday 9am\n# OR\ninterval: 24h # every 24 hours\ntier: haiku # cheap model for routine checks\nnext_run: 2025-12-20T09:00:00Z\n```\n\n## Daemon Integration\n\nDaemon heartbeat loop:\n1. Check timed beads where `next_run \u003c= now`\n2. For each due bead:\n - Inject into ready queue (set status to open if needed)\n - Update `next_run` based on schedule/interval\n3. Witnesses pick up work via `bd ready`\n\n## Use Cases\n\n- Weekly team sync reminders\n- Daily health checks\n- Periodic cleanup tasks\n- Scheduled reports\n\n## Interaction with Pinned Beads\n\nA pinned bead can be timed - it wakes up periodically but never closes.\nThis is how you model \"background services\" in Gas Town.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-18T18:07:39.665294-08:00","updated_at":"2025-12-18T18:07:39.665294-08:00"}
{"id":"gt-cik","title":"Overseer Crew: User-managed persistent workspaces","description":"## Overview\n\nCrew workers are the overseer's (human's) personal workspaces within a rig. Unlike polecats which are witness-managed and ephemeral, crew workers are:\n\n- **Persistent**: Not auto-garbage-collected\n- **User-managed**: Overseer controls lifecycle\n- **Long-lived identities**: dave, emma, fred - recognizable names\n- **Gas Town integrated**: Mail, handoff mechanics work\n- **Tmux optional**: Can work in terminal directly\n\n## Directory Structure\n\n```\n\u003crig\u003e/\n polecats/ # Managed workers (witness controls)\n refinery/ # Merge queue processor\n witness/ # Pit boss\n crew/ # Overseer's personal workspaces\n dave/ # Full clone, persistent\n emma/ # Full clone, persistent\n fred/ # Full clone, persistent\n```\n\n## Key Differences from Polecats\n\n- Location: crew/ instead of polecats/\n- Lifecycle: User-managed, not witness-managed\n- Auto-cleanup: Never (polecats auto-cleanup on swarm land)\n- Issue assignment: Optional (polecats require it)\n- Tmux: Optional (polecats require it)\n- Mail \u0026 Handoff: Yes for both\n- Identity: Persistent (polecats are ephemeral)\n\n## CLI Commands\n\n- gt crew add \u003cname\u003e [--rig \u003crig\u003e] - Create crew workspace\n- gt crew list [--rig \u003crig\u003e] - List crew workspaces\n- gt crew at \u003crig\u003e/\u003cname\u003e - Attach to workspace (start session)\n- gt crew attach \u003cname\u003e - Attach (infer rig from cwd)\n- gt crew refresh \u003cname\u003e - Handoff + restart (context cycling)\n- gt crew remove \u003cname\u003e [--force] - Remove workspace\n- gt crew status [\u003cname\u003e] - Show workspace status\n\n## Design Notes\n\n- Crew workers use full git clones (not worktrees)\n- Optional beads integration via BEADS_DIR\n- Mail-to-self handoff works for context cycling\n- No witness monitoring or nudging\n- No automatic issue assignment required\n\n## Background\n\nUsers often maintain separate repo clones for serial agent work. This is tedious to set up manually. Crew workspaces bring these into Gas Town's infrastructure while keeping user control.","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-16T16:47:37.529887-08:00","updated_at":"2025-12-16T20:59:46.13518-08:00","closed_at":"2025-12-16T20:59:46.13518-08:00"}
{"id":"gt-cik.1","title":"Crew directory structure and config","description":"Add crew/ directory support to rig structure. Include:\n- crew/ as peer to polecats/, refinery/, witness/\n- Crew worker subdirectories with full git clones\n- Optional BEADS_DIR configuration for beads integration\n- Crew state tracking (separate from polecat state)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-16T16:48:00.285499-08:00","updated_at":"2025-12-16T20:47:23.003869-08:00","closed_at":"2025-12-16T20:31:23.558027-08:00","dependencies":[{"issue_id":"gt-cik.1","depends_on_id":"gt-cik","type":"parent-child","created_at":"2025-12-16T16:48:00.28789-08:00","created_by":"daemon"}]}
@@ -414,7 +414,7 @@
{"id":"gt-iua8","title":"Merge: gt-frs","description":"branch: polecat/Slit\ntarget: main\nsource_issue: gt-frs\nrig: gastown","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-19T16:30:05.529099-08:00","updated_at":"2025-12-19T18:26:14.104887-08:00","closed_at":"2025-12-19T17:48:44.654109-08:00"}
{"id":"gt-j4nu","title":"Merge: gt-g44u.3","description":"branch: polecat/Ace\ntarget: main\nsource_issue: gt-g44u.3\nrig: gastown","status":"closed","priority":0,"issue_type":"merge-request","created_at":"2025-12-19T16:14:52.767156-08:00","updated_at":"2025-12-19T17:35:36.663796-08:00","closed_at":"2025-12-19T17:35:36.663796-08:00"}
{"id":"gt-j5tk","title":"Work assignment messages should auto-close on completion","description":"When a polecat completes work on an issue, the work assignment message (msg-type:task) stays open. Found 7 stale work assignments in gastown after swarm completed.\n\nProposal: When bd close is called on an issue, auto-close any work assignment messages that reference that issue in their body.\n\nAlternative: Work assignment messages could use a different lifecycle - perhaps they should be acked (closed) when the polecat starts working, not when they finish.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T03:12:28.403974-08:00","updated_at":"2025-12-20T03:12:28.403974-08:00"}
{"id":"gt-j6s8","title":"Refinery startup: bond mol-refinery-patrol on start","description":"Wire up Refinery to automatically bond its patrol molecule on startup.\n\n## Current state\n- mol-refinery-patrol exists in builtin_molecules.go\n- prompts/roles/refinery.md describes the protocol\n- Refinery doesn't auto-bond on startup\n\n## Desired behavior\nOn Refinery session start:\n1. gt prime detects RoleRefinery\n2. Check for existing in-progress patrol: bd list --status=in_progress --assignee=refinery\n3. If found: resume from current step\n4. If not found: bd mol bond mol-refinery-patrol --wisp\n5. Output patrol context to agent\n\n## Implementation options\nA) Add to gt prime (outputRefineryPatrolContext)\nB) Add startup hook in refinery CLAUDE.md\nC) Both (prime detects, template reinforces)\n\n## Testing\n- Start refinery session\n- Verify patrol bonds automatically\n- Kill mid-patrol, restart, verify resumes\n\n## Depends on\n- gt-3x0z.10 (existing issue for Refinery patrol)","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/dementus","created_at":"2025-12-22T16:43:34.739741-08:00","updated_at":"2025-12-23T00:05:39.309781-08:00"}
{"id":"gt-j6s8","title":"Refinery startup: bond mol-refinery-patrol on start","description":"Wire up Refinery to automatically bond its patrol molecule on startup.\n\n## Current state\n- mol-refinery-patrol exists in builtin_molecules.go\n- prompts/roles/refinery.md describes the protocol\n- Refinery doesn't auto-bond on startup\n\n## Desired behavior\nOn Refinery session start:\n1. gt prime detects RoleRefinery\n2. Check for existing in-progress patrol: bd list --status=in_progress --assignee=refinery\n3. If found: resume from current step\n4. If not found: bd mol bond mol-refinery-patrol --wisp\n5. Output patrol context to agent\n\n## Implementation options\nA) Add to gt prime (outputRefineryPatrolContext)\nB) Add startup hook in refinery CLAUDE.md\nC) Both (prime detects, template reinforces)\n\n## Testing\n- Start refinery session\n- Verify patrol bonds automatically\n- Kill mid-patrol, restart, verify resumes\n\n## Depends on\n- gt-3x0z.10 (existing issue for Refinery patrol)","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-22T16:43:34.739741-08:00","updated_at":"2025-12-22T16:43:34.739741-08:00"}
{"id":"gt-j87","title":"Design: Work flow simulation and validation","description":"Validate GGT designs through simulation before implementation.\n\n## Validation Approaches\n\n### 1. Dry-Run Simulation (Recommended First)\nMayor walks through scenarios mentally/on paper:\n- \"If polecat Toast signals done with dirty git state, what happens?\"\n- \"If Witness context fills mid-verification, what state is lost?\"\n- \"If two polecats try to close same issue, what happens?\"\n\nCreate beads for any gaps discovered.\n\n### 2. Real Work in gastown-py\nUse Python Gas Town to stress-test assumptions:\n- Run actual batch work on test repos\n- Observe edge cases in practice\n- Document issues found\n\n### 3. Edge Case Analysis\nSystematic review of failure modes:\n- Agent crashes mid-operation\n- Network failures during sync\n- Concurrent access to shared state\n- Context limits hit at bad times\n\n## Key Scenarios to Validate\n\n- [ ] Witness session cycling (state preservation)\n- [ ] Polecat decommission with dirty state\n- [ ] Merge conflicts in queue\n- [ ] Beads sync conflicts between workers\n- [ ] Escalation path (stuck worker -\u003e Mayor)\n- [ ] Cross-rig communication\n- [ ] Federation mail routing (future)\n\n## Success Criteria\n\n- No data loss scenarios identified\n- Clear recovery paths for all failure modes\n- Edge cases either handled or documented as limitations\n- Design improves as model cognition improves\n\n## Output\n\nFor each scenario validated:\n1. Document in relevant bead if issue found\n2. Create new beads for missing functionality\n3. Update architecture.md if design changes","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-15T20:24:11.251841-08:00","updated_at":"2025-12-16T17:25:49.858717-08:00"}
{"id":"gt-jgdx","title":"Digest: mol-deacon-patrol","description":"Test patrol cycle - first run, no actual work done","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-22T02:07:03.388821-08:00","updated_at":"2025-12-22T02:07:03.388821-08:00","closed_at":"2025-12-22T02:07:03.388793-08:00","close_reason":"Squashed from 5 wisps"}
{"id":"gt-jpt","title":"Town-level beads: Real DB for coordination mail","description":"Implement Option A from mail redesign: Town gets real beads DB for coordination.\n\n## Background\n\nMail is now Beads. But currently:\n- Town .beads/redirect points to rig beads\n- mayor/mail/ has legacy JSONL files\n- Cross-rig coordination has no clear home\n\n## Design\n\nTown beads = coordination, cross-rig mail, mayor inbox, handoffs\nRig beads = project issues, work items\n\nMatches HOP hierarchy: platform \u003e project \u003e worker\n\n## Structure\n\n~/gt/\n .beads/ # REAL beads DB (prefix: gm-)\n mayor/\n town.json\n state.json # NO mail/ directory\n gastown/\n .beads/ # Rig beads (prefix: ga-)\n\n## Tasks\n\n1. Delete ~/gt/.beads/redirect\n2. Run bd init --prefix gm at ~/gt/ (town beads)\n3. Delete ~/gt/mayor/mail/ directory\n4. Update gt mail to use beads not JSONL\n5. Add mail fields (thread_id, reply_to, msg_type)\n6. Update gt prime for two-tier model\n7. Update docs/architecture.md\n\n## Addressing\n\n- mayor/ -\u003e town beads\n- rig/agent -\u003e rig beads\n- Cross-rig -\u003e town beads","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-17T19:09:55.855955-08:00","updated_at":"2025-12-19T01:57:17.032558-08:00","closed_at":"2025-12-19T01:57:17.032558-08:00"}
@@ -460,7 +460,7 @@
{"id":"gt-l6r9","title":"Test molecule","description":"Test","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T14:29:43.985973-08:00","updated_at":"2025-12-19T14:30:00.084138-08:00","closed_at":"2025-12-19T14:30:00.084138-08:00"}
{"id":"gt-l7cd","title":"Work on gt-role-template: Refine witness/CLAUDE.md role t...","description":"Work on gt-role-template: Refine witness/CLAUDE.md role template. See issue for details. Run 'bd show gt-role-template' to see the full issue.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-20T03:47:29.414394-08:00","updated_at":"2025-12-20T03:54:13.699528-08:00","closed_at":"2025-12-20T03:54:13.699528-08:00"}
{"id":"gt-l9g","title":"Beads epic templates for batch work patterns","description":"Optional: Define templates for common batch work patterns.\n\n## Concept\n\nA template encodes a workflow pattern that can be instantiated as beads:\n\n```yaml\n# templates/batch-basic.yaml\nname: basic-batch\ndescription: Simple batch work pattern\nphases:\n - name: startup\n issues:\n - title: \"Verify workers ready\"\n - name: working\n # Actual work issues added separately\n - name: cleanup\n issues:\n - title: \"Merge all branches\"\n - title: \"Clean up workers\"\n - title: \"Report to Mayor\"\n```\n\n## Usage\n\n```bash\ngt spawn --template basic-batch --epic gt-u1j --workers 3\n```\n\nCreates beads epic with template phases + actual work from gt-u1j children.\n\n## Decision Point\n\nTemplates are OPTIONAL. The core design (beads as state, multi-wave orchestration) works without templates. Templates are sugar for common patterns.\n\nConsider deferring to P3 or dropping entirely if beads epics with dependencies suffice.\n\n## Note\n\nNo \"swarm IDs\" involved - templates just pre-populate epic/issue structure.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T01:51:24.399235-08:00","updated_at":"2025-12-16T17:26:08.868396-08:00"}
{"id":"gt-ldk8","title":"Witness should verify polecat submitted before stopping session","description":"## Problem\n\nWitness stopped dementus's session before it could call `gt done`, losing the MR submission. I had to manually push and submit the branch.\n\n## Root Cause\n\nWitness cleanup is triggered by nudge/manual check rather than by receiving POLECAT_DONE message. When Witness cleans up based on issue status (closed), it doesn't wait for the polecat to complete its shutdown sequence.\n\n## Expected Behavior\n\nWitness should only stop a polecat session after:\n1. Receiving POLECAT_DONE message from that polecat, OR\n2. Timeout waiting for POLECAT_DONE after issue is closed\n\n## Current Behavior\n\nWitness stops sessions immediately when asked to check for completions, even if polecat hasn't called `gt done` yet.\n\n## Fix\n\nIn mol-witness-patrol inbox-check step:\n- Only clean up polecats that have sent POLECAT_DONE\n- For polecats with closed issues but no DONE message, nudge them to complete\n- Add timeout before force-killing","status":"in_progress","priority":1,"issue_type":"bug","assignee":"gastown/nux","created_at":"2025-12-22T23:54:12.969528-08:00","updated_at":"2025-12-23T00:08:26.711168-08:00"}
{"id":"gt-ldk8","title":"Witness should verify polecat submitted before stopping session","description":"## Problem\n\nWitness stopped dementus's session before it could call `gt done`, losing the MR submission. I had to manually push and submit the branch.\n\n## Root Cause\n\nWitness cleanup is triggered by nudge/manual check rather than by receiving POLECAT_DONE message. When Witness cleans up based on issue status (closed), it doesn't wait for the polecat to complete its shutdown sequence.\n\n## Expected Behavior\n\nWitness should only stop a polecat session after:\n1. Receiving POLECAT_DONE message from that polecat, OR\n2. Timeout waiting for POLECAT_DONE after issue is closed\n\n## Current Behavior\n\nWitness stops sessions immediately when asked to check for completions, even if polecat hasn't called `gt done` yet.\n\n## Fix\n\nIn mol-witness-patrol inbox-check step:\n- Only clean up polecats that have sent POLECAT_DONE\n- For polecats with closed issues but no DONE message, nudge them to complete\n- Add timeout before force-killing","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-22T23:54:12.969528-08:00","updated_at":"2025-12-22T23:54:12.969528-08:00"}
{"id":"gt-ldm4","title":"verify-tests","description":"Run existing tests. Add new tests for new functionality.\nEnsure adequate coverage.\n\ngo test ./...\n\nFix any test failures before proceeding.\n\nDepends: implement","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T21:48:26.322056-08:00","updated_at":"2025-12-21T21:48:26.322056-08:00","dependencies":[{"issue_id":"gt-ldm4","depends_on_id":"gt-i4lo","type":"parent-child","created_at":"2025-12-21T21:48:26.326258-08:00","created_by":"stevey"},{"issue_id":"gt-ldm4","depends_on_id":"gt-vhby","type":"blocks","created_at":"2025-12-21T21:48:26.326815-08:00","created_by":"stevey"}],"wisp":true}
{"id":"gt-le1a","title":"Merge: gt-3x1","description":"branch: polecat/Slit\ntarget: main\nsource_issue: gt-3x1\nrig: gastown","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-19T14:53:47.674479-08:00","updated_at":"2025-12-19T18:30:24.050697-08:00","closed_at":"2025-12-19T18:30:24.0507-08:00"}
{"id":"gt-leeb","title":"load-context","description":"Run gt prime and bd prime. Verify issue assignment.\nCheck inbox for any relevant messages.\n\nRead the assigned issue (gt-qwyu) and understand the requirements.\nIdentify any blockers or missing information.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-21T21:58:52.59974-08:00","updated_at":"2025-12-21T21:59:10.94207-08:00","closed_at":"2025-12-21T21:59:10.94207-08:00","close_reason":"test cleanup","dependencies":[{"issue_id":"gt-leeb","depends_on_id":"gt-q6hl","type":"parent-child","created_at":"2025-12-21T21:58:52.600633-08:00","created_by":"stevey"}],"wisp":true}
@@ -643,7 +643,7 @@
{"id":"gt-upom","title":"Witness patrol: cleanup idle orphan polecats","description":"Add patrol step to find and cleanup polecats that are idle with no assigned issue. These orphans occur when polecats crash before sending DONE or Witness misses the message. Patrol should verify git is clean before removing worktree. Part of gt-rana.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-21T23:09:41.756753-08:00","updated_at":"2025-12-21T23:09:41.756753-08:00"}
{"id":"gt-us8","title":"Daemon: configurable heartbeat interval","description":"Heartbeat interval is hardcoded to 60s. Should be configurable via:\n- town.json config\n- Command line flag\n- Environment variable\n\nDefault 60s is reasonable but some deployments may want faster/slower.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-18T13:38:14.282216-08:00","updated_at":"2025-12-18T13:38:14.282216-08:00","dependencies":[{"issue_id":"gt-us8","depends_on_id":"gt-99m","type":"blocks","created_at":"2025-12-18T13:38:26.704111-08:00","created_by":"daemon"}]}
{"id":"gt-usy0","title":"Merge: gt-3x0z.3","description":"branch: polecat/rictus\ntarget: main\nsource_issue: gt-3x0z.3\nrig: gastown","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-21T16:03:43.535266-08:00","updated_at":"2025-12-21T17:20:27.505696-08:00","closed_at":"2025-12-21T17:20:27.505696-08:00","close_reason":"ORPHANED: Branch never pushed, worktree deleted"}
{"id":"gt-utwc","title":"Self-mail should suppress tmux notification","description":"When sending mail to yourself (e.g., mayor sending to mayor/), the tmux notification shouldn't fire.\n\n**Rationale:**\n- Self-mail is intended for future-you (next session handoff)\n- Present-you just sent it, so you already know about it\n- The notification is redundant/confusing in this case\n\n**Fix:**\nSuppress tmux notification when sender == recipient address.","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-12-22T17:55:39.573705-08:00","updated_at":"2025-12-22T23:58:02.827026-08:00","closed_at":"2025-12-22T23:58:02.827026-08:00","close_reason":"Skip tmux notification when sender == recipient"}
{"id":"gt-utwc","title":"Self-mail should suppress tmux notification","description":"When sending mail to yourself (e.g., mayor sending to mayor/), the tmux notification shouldn't fire.\n\n**Rationale:**\n- Self-mail is intended for future-you (next session handoff)\n- Present-you just sent it, so you already know about it\n- The notification is redundant/confusing in this case\n\n**Fix:**\nSuppress tmux notification when sender == recipient address.","status":"open","priority":3,"issue_type":"bug","created_at":"2025-12-22T17:55:39.573705-08:00","updated_at":"2025-12-22T17:55:39.573705-08:00"}
{"id":"gt-uym5","title":"Implement gt mol status command","description":"Show what's on an agent's hook.\n\n```bash\ngt mol status [target]\n```\n\nOutput:\n- What's slung (molecule name, associated issue)\n- Current phase and progress\n- Whether it's a wisp\n- Next action hint\n\nIf no target, shows current agent's status.\n\nAcceptance:\n- [ ] Read pinned bead attachment\n- [ ] Display molecule/issue info\n- [ ] Show phase progress\n- [ ] Indicate wisp vs durable","status":"closed","priority":1,"issue_type":"task","assignee":"gastown/nux","created_at":"2025-12-22T03:17:34.679963-08:00","updated_at":"2025-12-22T12:34:19.942265-08:00","closed_at":"2025-12-22T12:34:19.942265-08:00","close_reason":"Implemented gt mol status command with mol alias, auto-detection, progress display, wisp detection, and next action hints"}
{"id":"gt-v5hv","title":"Work on ga-y6b: Implement Refinery as Claude agent. Conve...","description":"Work on ga-y6b: Implement Refinery as Claude agent. Convert from shell to Claude agent that processes MRs in merge queue, runs tests, merges to integration branch. When done, submit MR (not PR) to integration branch for Refinery.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:58:17.576892-08:00","updated_at":"2025-12-19T23:23:22.778407-08:00","closed_at":"2025-12-19T23:23:22.778407-08:00"}
{"id":"gt-v5k","title":"Design: Failure modes and recovery","description":"Document failure modes and recovery strategies for Gas Town operations.\n\n## Critical Failure Modes\n\n### 1. Agent Crash Mid-Operation\n\n**Scenario**: Polecat crashes while committing, Witness crashes while verifying\n\n**Detection**:\n- Session suddenly gone (tmux check fails)\n- State shows 'working' but no session\n- Heartbeat stops (for Witness)\n\n**Recovery**:\n- Doctor detects via ZombieSessionCheck\n- Capture any recoverable state\n- Reset agent state to 'idle'\n- For Witness: auto-restart via supervisor or manual gt witness start\n\n### 2. Git State Corruption\n\n**Scenario**: Merge conflict, failed rebase, detached HEAD\n\n**Detection**:\n- Git commands fail\n- Dirty state that won't commit\n- Branch diverged from origin\n\n**Recovery**:\n- gt doctor reports git health issues\n- Manual intervention recommended\n- Severe cases: remove clone, re-clone\n\n### 3. Beads Sync Conflict\n\n**Scenario**: Two polecats modify same issue\n\n**Detection**:\n- bd sync fails with conflict\n- Beads tombstone mechanism handles most cases\n\n**Recovery**:\n- Beads has last-write-wins semantics\n- bd sync --force in extreme cases\n- Issues may need manual dedup\n\n### 4. Tmux Failure\n\n**Scenario**: Tmux server crashes, socket issues\n\n**Detection**:\n- All sessions inaccessible\n- \"no server running\" errors\n\n**Recovery**:\n- Kill any orphan processes\n- tmux kill-server \u0026\u0026 tmux start-server\n- All agent states reset to idle\n- Re-spawn active work\n\n### 5. Claude API Issues\n\n**Scenario**: Rate limits, outages, context limits\n\n**Detection**:\n- Sessions hang or produce errors\n- Repeated failure patterns\n\n**Recovery**:\n- Exponential backoff (handled by Claude Code)\n- For context limits: session cycling (mail-to-self)\n- For outages: wait and retry\n\n### 6. Disk Full\n\n**Scenario**: Clones, logs, or beads fill disk\n\n**Detection**:\n- Write operations fail\n- git/bd commands error\n\n**Recovery**:\n- Clean up logs: rm ~/.gastown/logs/*\n- Remove old polecat clones\n- gt doctor --fix can clean some cruft\n\n### 7. Network Failure\n\n**Scenario**: Can't reach GitHub, API servers\n\n**Detection**:\n- git fetch/push fails\n- Claude sessions hang\n\n**Recovery**:\n- Work continues locally\n- Queue pushes for later\n- Sync when connectivity restored\n\n## Recovery Principles\n\n1. **Fail safe**: Prefer stopping over corrupting\n2. **State is recoverable**: Git and beads have recovery mechanisms\n3. **Doctor heals**: gt doctor --fix handles common issues\n4. **Emergency stop**: gt stop --all as last resort\n5. **Human escalation**: Some failures need Overseer intervention\n\n## Implementation\n\n- Document each failure mode in architecture.md\n- Ensure doctor checks cover detection\n- Add recovery hints to error messages\n- Log all failures for debugging","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T23:19:07.198289-08:00","updated_at":"2025-12-15T23:19:28.171942-08:00"}

View File

@@ -23,6 +23,13 @@ var doctorCmd = &cobra.Command{
Doctor checks for common configuration issues, missing files,
and other problems that could affect workspace operation.
Patrol checks:
- patrol-molecules-exist Verify patrol molecules exist
- patrol-hooks-wired Verify daemon triggers patrols
- patrol-not-stuck Detect stale wisps (>1h)
- patrol-plugins-accessible Verify plugin directories
- patrol-roles-have-prompts Verify role prompts exist
Use --fix to attempt automatic fixes for issues that support it.
Use --rig to check a specific rig instead of the entire workspace.`,
RunE: runDoctor,
@@ -71,6 +78,13 @@ func runDoctor(cmd *cobra.Command, args []string) error {
d.Register(doctor.NewWispSizeCheck())
d.Register(doctor.NewWispStaleCheck())
// Patrol system checks
d.Register(doctor.NewPatrolMoleculesExistCheck())
d.Register(doctor.NewPatrolHooksWiredCheck())
d.Register(doctor.NewPatrolNotStuckCheck())
d.Register(doctor.NewPatrolPluginsAccessibleCheck())
d.Register(doctor.NewPatrolRolesHavePromptsCheck())
// Config architecture checks
d.Register(doctor.NewSettingsCheck())
d.Register(doctor.NewRuntimeGitignoreCheck())

View File

@@ -45,12 +45,18 @@ This creates a rig container with:
- config.json Rig configuration
- .beads/ Rig-level issue tracking (initialized)
- .beads-wisp/ Local wisp/molecule tracking (gitignored)
- plugins/ Rig-level plugin directory
- refinery/rig/ Canonical main clone
- mayor/rig/ Mayor's working clone
- crew/main/ Default human workspace
- witness/ Witness agent directory
- polecats/ Worker directory (empty)
The command also:
- Seeds patrol molecules (Deacon, Witness, Refinery)
- Creates ~/gt/plugins/ (town-level) if it doesn't exist
- Creates <rig>/plugins/ (rig-level)
Example:
gt rig add gastown https://github.com/steveyegge/gastown
gt rig add my-project git@github.com:user/repo.git --prefix mp`,
@@ -201,6 +207,7 @@ func runRigAdd(cmd *cobra.Command, args []string) error {
fmt.Printf(" ├── config.json\n")
fmt.Printf(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix)
fmt.Printf(" ├── .beads-wisp/ (local wisp/molecule tracking)\n")
fmt.Printf(" ├── plugins/ (rig-level plugins)\n")
fmt.Printf(" ├── refinery/rig/ (canonical main)\n")
fmt.Printf(" ├── mayor/rig/ (mayor's clone)\n")
fmt.Printf(" ├── crew/%s/ (your workspace)\n", rigAddCrew)

View File

@@ -0,0 +1,459 @@
package doctor
import (
"bufio"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// PatrolMoleculesExistCheck verifies that patrol molecules exist for each rig.
type PatrolMoleculesExistCheck struct {
FixableCheck
missingMols map[string][]string // rig -> missing molecule titles
}
// NewPatrolMoleculesExistCheck creates a new patrol molecules exist check.
func NewPatrolMoleculesExistCheck() *PatrolMoleculesExistCheck {
return &PatrolMoleculesExistCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-molecules-exist",
CheckDescription: "Check if patrol molecules exist for each rig",
},
},
}
}
// patrolMolecules are the required patrol molecule titles.
var patrolMolecules = []string{
"Deacon Patrol",
"Witness Patrol",
"Refinery Patrol",
}
// Run checks if patrol molecules exist.
func (c *PatrolMoleculesExistCheck) Run(ctx *CheckContext) *CheckResult {
c.missingMols = make(map[string][]string)
rigs, err := discoverRigs(ctx.TownRoot)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Failed to discover rigs",
Details: []string{err.Error()},
}
}
if len(rigs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs configured",
}
}
var details []string
for _, rigName := range rigs {
rigPath := filepath.Join(ctx.TownRoot, rigName)
missing := c.checkPatrolMolecules(rigPath)
if len(missing) > 0 {
c.missingMols[rigName] = missing
details = append(details, fmt.Sprintf("%s: missing %v", rigName, missing))
}
}
if len(details) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d rig(s) missing patrol molecules", len(c.missingMols)),
Details: details,
FixHint: "Run 'gt doctor --fix' to create missing patrol molecules",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("All %d rig(s) have patrol molecules", len(rigs)),
}
}
// checkPatrolMolecules returns missing patrol molecule titles for a rig.
func (c *PatrolMoleculesExistCheck) checkPatrolMolecules(rigPath string) []string {
// List molecules using bd
cmd := exec.Command("bd", "list", "--type=molecule")
cmd.Dir = rigPath
output, err := cmd.Output()
if err != nil {
return patrolMolecules // Can't check, assume all missing
}
outputStr := string(output)
var missing []string
for _, mol := range patrolMolecules {
if !strings.Contains(outputStr, mol) {
missing = append(missing, mol)
}
}
return missing
}
// Fix creates missing patrol molecules.
func (c *PatrolMoleculesExistCheck) Fix(ctx *CheckContext) error {
for rigName, missing := range c.missingMols {
rigPath := filepath.Join(ctx.TownRoot, rigName)
for _, mol := range missing {
desc := getPatrolMoleculeDesc(mol)
cmd := exec.Command("bd", "create",
"--type=molecule",
"--title="+mol,
"--description="+desc,
"--priority=2",
)
cmd.Dir = rigPath
if err := cmd.Run(); err != nil {
return fmt.Errorf("creating %s in %s: %w", mol, rigName, err)
}
}
}
return nil
}
func getPatrolMoleculeDesc(title string) string {
switch title {
case "Deacon Patrol":
return "Mayor's daemon patrol loop for handling callbacks, health checks, and cleanup."
case "Witness Patrol":
return "Per-rig worker monitor patrol loop with progressive nudging."
case "Refinery Patrol":
return "Merge queue processor patrol loop with verification gates."
default:
return "Patrol molecule"
}
}
// PatrolHooksWiredCheck verifies that hooks trigger patrol execution.
type PatrolHooksWiredCheck struct {
BaseCheck
}
// NewPatrolHooksWiredCheck creates a new patrol hooks wired check.
func NewPatrolHooksWiredCheck() *PatrolHooksWiredCheck {
return &PatrolHooksWiredCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-hooks-wired",
CheckDescription: "Check if hooks trigger patrol execution",
},
}
}
// Run checks if patrol hooks are wired.
func (c *PatrolHooksWiredCheck) Run(ctx *CheckContext) *CheckResult {
// Check for daemon config which manages patrols
daemonConfigPath := filepath.Join(ctx.TownRoot, "mayor", "daemon.json")
if _, err := os.Stat(daemonConfigPath); os.IsNotExist(err) {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: "Daemon config not found",
FixHint: "Run 'gt daemon init' to configure daemon",
}
}
// Check daemon config for patrol configuration
data, err := os.ReadFile(daemonConfigPath)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Failed to read daemon config",
Details: []string{err.Error()},
}
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: "Invalid daemon config format",
Details: []string{err.Error()},
}
}
// Check for patrol entries
if patrols, ok := config["patrols"]; ok {
if patrolMap, ok := patrols.(map[string]interface{}); ok && len(patrolMap) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("Daemon configured with %d patrol(s)", len(patrolMap)),
}
}
}
// Check if heartbeat is enabled (triggers deacon patrol)
if heartbeat, ok := config["heartbeat"]; ok {
if hb, ok := heartbeat.(map[string]interface{}); ok {
if enabled, ok := hb["enabled"].(bool); ok && enabled {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "Daemon heartbeat enabled (triggers patrols)",
}
}
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: "Patrol hooks not configured in daemon",
FixHint: "Configure patrols in mayor/daemon.json or run 'gt daemon init'",
}
}
// PatrolNotStuckCheck detects wisps that have been in_progress too long.
type PatrolNotStuckCheck struct {
BaseCheck
stuckThreshold time.Duration
}
// NewPatrolNotStuckCheck creates a new patrol not stuck check.
func NewPatrolNotStuckCheck() *PatrolNotStuckCheck {
return &PatrolNotStuckCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-not-stuck",
CheckDescription: "Check for stuck patrol wisps (>1h in_progress)",
},
stuckThreshold: 1 * time.Hour,
}
}
// Run checks for stuck patrol wisps.
func (c *PatrolNotStuckCheck) Run(ctx *CheckContext) *CheckResult {
rigs, err := discoverRigs(ctx.TownRoot)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Failed to discover rigs",
Details: []string{err.Error()},
}
}
if len(rigs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs configured",
}
}
var stuckWisps []string
for _, rigName := range rigs {
wispPath := filepath.Join(ctx.TownRoot, rigName, ".beads-wisp", "issues.jsonl")
stuck := c.checkStuckWisps(wispPath, rigName)
stuckWisps = append(stuckWisps, stuck...)
}
if len(stuckWisps) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d stuck patrol wisp(s) found (>1h)", len(stuckWisps)),
Details: stuckWisps,
FixHint: "Manual review required - wisps may need to be burned or sessions restarted",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No stuck patrol wisps found",
}
}
// checkStuckWisps returns descriptions of stuck wisps in a rig.
func (c *PatrolNotStuckCheck) checkStuckWisps(issuesPath string, rigName string) []string {
file, err := os.Open(issuesPath)
if err != nil {
return nil // No issues file
}
defer file.Close()
var stuck []string
cutoff := time.Now().Add(-c.stuckThreshold)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
var issue struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
UpdatedAt time.Time `json:"updated_at"`
}
if err := json.Unmarshal([]byte(line), &issue); err != nil {
continue
}
// Check for in_progress issues older than threshold
if issue.Status == "in_progress" && !issue.UpdatedAt.IsZero() && issue.UpdatedAt.Before(cutoff) {
stuck = append(stuck, fmt.Sprintf("%s: %s (%s) - stale since %s",
rigName, issue.ID, issue.Title, issue.UpdatedAt.Format("2006-01-02 15:04")))
}
}
return stuck
}
// PatrolPluginsAccessibleCheck verifies plugin directories exist and are readable.
type PatrolPluginsAccessibleCheck struct {
FixableCheck
missingDirs []string
}
// NewPatrolPluginsAccessibleCheck creates a new patrol plugins accessible check.
func NewPatrolPluginsAccessibleCheck() *PatrolPluginsAccessibleCheck {
return &PatrolPluginsAccessibleCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-plugins-accessible",
CheckDescription: "Check if plugin directories exist and are readable",
},
},
}
}
// Run checks if plugin directories are accessible.
func (c *PatrolPluginsAccessibleCheck) Run(ctx *CheckContext) *CheckResult {
c.missingDirs = nil
// Check town-level plugins directory
townPluginsDir := filepath.Join(ctx.TownRoot, "plugins")
if _, err := os.Stat(townPluginsDir); os.IsNotExist(err) {
c.missingDirs = append(c.missingDirs, townPluginsDir)
}
// Check rig-level plugins directories
rigs, err := discoverRigs(ctx.TownRoot)
if err == nil {
for _, rigName := range rigs {
rigPluginsDir := filepath.Join(ctx.TownRoot, rigName, "plugins")
if _, err := os.Stat(rigPluginsDir); os.IsNotExist(err) {
c.missingDirs = append(c.missingDirs, rigPluginsDir)
}
}
}
if len(c.missingDirs) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d plugin directory(ies) missing", len(c.missingDirs)),
Details: c.missingDirs,
FixHint: "Run 'gt doctor --fix' to create missing directories",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "All plugin directories accessible",
}
}
// Fix creates missing plugin directories.
func (c *PatrolPluginsAccessibleCheck) Fix(ctx *CheckContext) error {
for _, dir := range c.missingDirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("creating %s: %w", dir, err)
}
}
return nil
}
// PatrolRolesHavePromptsCheck verifies that prompts/roles/*.md exist for each role.
type PatrolRolesHavePromptsCheck struct {
BaseCheck
}
// NewPatrolRolesHavePromptsCheck creates a new patrol roles have prompts check.
func NewPatrolRolesHavePromptsCheck() *PatrolRolesHavePromptsCheck {
return &PatrolRolesHavePromptsCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-roles-have-prompts",
CheckDescription: "Check if prompts/roles/*.md exist for each patrol role",
},
}
}
// requiredRolePrompts are the required role prompt files.
var requiredRolePrompts = []string{
"deacon.md",
"witness.md",
"refinery.md",
}
// Run checks if role prompts exist.
func (c *PatrolRolesHavePromptsCheck) Run(ctx *CheckContext) *CheckResult {
rigs, err := discoverRigs(ctx.TownRoot)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Failed to discover rigs",
Details: []string{err.Error()},
}
}
if len(rigs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs configured",
}
}
var missingPrompts []string
for _, rigName := range rigs {
// Check in mayor's clone (canonical for the rig)
mayorRig := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
promptsDir := filepath.Join(mayorRig, "prompts", "roles")
for _, roleFile := range requiredRolePrompts {
promptPath := filepath.Join(promptsDir, roleFile)
if _, err := os.Stat(promptPath); os.IsNotExist(err) {
missingPrompts = append(missingPrompts, fmt.Sprintf("%s: %s", rigName, roleFile))
}
}
}
if len(missingPrompts) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d role prompt(s) missing", len(missingPrompts)),
Details: missingPrompts,
FixHint: "Role prompts should be in the project repository under prompts/roles/",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "All patrol role prompts found",
}
}

View File

@@ -283,6 +283,18 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
return nil, fmt.Errorf("initializing wisp beads: %w", err)
}
// Seed patrol molecules for this rig
if err := m.seedPatrolMolecules(rigPath); err != nil {
// Non-fatal: log warning but continue
fmt.Printf(" Warning: Could not seed patrol molecules: %v\n", err)
}
// Create plugin directories
if err := m.createPluginDirectories(rigPath); err != nil {
// Non-fatal: log warning but continue
fmt.Printf(" Warning: Could not create plugin directories: %v\n", err)
}
// Register in town config
m.config.Rigs[opts.Name] = config.RigEntry{
GitURL: opts.GitURL,
@@ -491,3 +503,112 @@ func (m *Manager) createRoleCLAUDEmd(workspacePath string, role string, rigName
claudePath := filepath.Join(workspacePath, "CLAUDE.md")
return os.WriteFile(claudePath, []byte(content), 0644)
}
// seedPatrolMolecules creates patrol molecule prototypes in the rig's beads database.
// These molecules define the work loops for Deacon, Witness, and Refinery roles.
func (m *Manager) seedPatrolMolecules(rigPath string) error {
// Use bd command to seed molecules (more reliable than internal API)
// The bd mol seed command creates built-in molecules if they don't exist
cmd := exec.Command("bd", "mol", "seed", "--patrol")
cmd.Dir = rigPath
if err := cmd.Run(); err != nil {
// Fallback: bd mol seed might not support --patrol yet
// Try creating them individually via bd create
return m.seedPatrolMoleculesManually(rigPath)
}
return nil
}
// seedPatrolMoleculesManually creates patrol molecules using bd create commands.
func (m *Manager) seedPatrolMoleculesManually(rigPath string) error {
// Patrol molecule definitions (subset of builtin_molecules.go for seeding)
patrolMols := []struct {
title string
desc string
}{
{
title: "Deacon Patrol",
desc: "Mayor's daemon patrol loop for handling callbacks, health checks, and cleanup.",
},
{
title: "Witness Patrol",
desc: "Per-rig worker monitor patrol loop with progressive nudging.",
},
{
title: "Refinery Patrol",
desc: "Merge queue processor patrol loop with verification gates.",
},
}
for _, mol := range patrolMols {
// Check if already exists by title
checkCmd := exec.Command("bd", "list", "--type=molecule", "--format=json")
checkCmd.Dir = rigPath
output, _ := checkCmd.Output()
if strings.Contains(string(output), mol.title) {
continue // Already exists
}
// Create the molecule
cmd := exec.Command("bd", "create",
"--type=molecule",
"--title="+mol.title,
"--description="+mol.desc,
"--priority=2",
)
cmd.Dir = rigPath
if err := cmd.Run(); err != nil {
// Non-fatal, continue with others
continue
}
}
return nil
}
// createPluginDirectories creates plugin directories at town and rig levels.
// - ~/gt/plugins/ (town-level, shared across all rigs)
// - <rig>/plugins/ (rig-level, rig-specific plugins)
func (m *Manager) createPluginDirectories(rigPath string) error {
// Town-level plugins directory
townPluginsDir := filepath.Join(m.townRoot, "plugins")
if err := os.MkdirAll(townPluginsDir, 0755); err != nil {
return fmt.Errorf("creating town plugins directory: %w", err)
}
// Create a README in town plugins if it doesn't exist
townReadme := filepath.Join(townPluginsDir, "README.md")
if _, err := os.Stat(townReadme); os.IsNotExist(err) {
content := `# Gas Town Plugins
This directory contains town-level plugins that run during Deacon patrol cycles.
## Plugin Structure
Each plugin is a directory containing:
- plugin.md - Plugin definition with YAML frontmatter
## Gate Types
- cooldown: Time since last run (e.g., 24h)
- cron: Schedule-based (e.g., "0 9 * * *")
- condition: Metric threshold
- event: Trigger-based (startup, heartbeat)
See docs/deacon-plugins.md for full documentation.
`
if writeErr := os.WriteFile(townReadme, []byte(content), 0644); writeErr != nil {
// Non-fatal
return nil
}
}
// Rig-level plugins directory
rigPluginsDir := filepath.Join(rigPath, "plugins")
if err := os.MkdirAll(rigPluginsDir, 0755); err != nil {
return fmt.Errorf("creating rig plugins directory: %w", err)
}
// Add plugins/ to rig .gitignore
gitignorePath := filepath.Join(rigPath, ".gitignore")
return m.ensureGitignoreEntry(gitignorePath, "plugins/")
}