feat(doctor): add ephemeral beads health checks
Add gt doctor checks for ephemeral beads repos: - ephemeral-exists: Verify .beads-ephemeral/ exists for each rig (fixable) - ephemeral-git: Verify it's a valid git repo (fixable) - ephemeral-orphans: Detect molecules >24h old (needs manual review) - ephemeral-size: Warn if repo >100MB - ephemeral-stale: Detect molecules with no activity in last hour Implements gt-3x0z.3. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,8 @@
|
||||
{"id":"gt-3x0z.10","title":"Phase 4.2: Witness/Refinery patrol molecules","description":"Extend ephemeral patrol pattern to Witness and Refinery.\n\n## Witness Patrol Cycle\n\nmol-witness-patrol-cycle:\n1. scan-polecats: Check each polecat status\n2. detect-stalls: Find stuck/idle polecats\n3. nudge-or-escalate: Take action on stalls\n4. log-status: Record cycle results\n→ Squash with summary\n\n## Refinery Patrol Cycle\n\nmol-refinery-patrol-cycle:\n1. scan-queue: Check merge queue\n2. process-ready: Merge ready MRs\n3. handle-failures: Deal with conflicts/failures\n4. log-status: Record cycle results\n→ Squash with summary\n\n## Quiescent Mode\n\nWhen no work: just log 'no activity' summary.\nWitness and Refinery can sleep between cycles until triggered.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T14:34:13.103951-08:00","updated_at":"2025-12-21T14:34:13.103951-08:00","dependencies":[{"issue_id":"gt-3x0z.10","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:34:13.104305-08:00","created_by":"daemon"},{"issue_id":"gt-3x0z.10","depends_on_id":"gt-3x0z.9","type":"blocks","created_at":"2025-12-21T14:34:40.742967-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.11","title":"Phase 5.1: Document ephemeral architecture","description":"Update docs with ephemeral molecule architecture.\n\n## Files to Update\n\n### docs/architecture.md\n- Add Ephemeral Molecules section\n- Diagram: Proto → Ephemeral → Digest flow\n- Explain inversion of control for summaries\n\n### docs/molecules.md (new or update)\n- Proto molecule catalog\n- Ephemeral vs main beads\n- Molecule lifecycle\n- Summary generation guidelines\n\n### Agent CLAUDE.md files\n- Polecat: molecule workflow protocol\n- Deacon: patrol cycle pattern\n- Witness: patrol cycle pattern\n- Refinery: patrol cycle pattern\n\n## Key Concepts to Document\n\n1. Everything is a molecule\n2. Orchestration molecules are ephemeral\n3. Only digests reach main beads\n4. Agents generate their own summaries\n5. Crash recovery via ephemeral persistence","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T14:34:30.074147-08:00","updated_at":"2025-12-21T14:34:30.074147-08:00","dependencies":[{"issue_id":"gt-3x0z.11","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:34:30.075854-08:00","created_by":"daemon"},{"issue_id":"gt-3x0z.11","depends_on_id":"gt-3x0z.10","type":"blocks","created_at":"2025-12-21T14:34:40.812635-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.12","title":"Phase 5.2: Ephemeral error handling and recovery","description":"Handle edge cases in ephemeral molecule lifecycle.\n\n## Scenarios\n\n### Agent dies mid-molecule\n- Ephemeral state persists\n- Witness detects stall (no heartbeat)\n- Options:\n a. Nudge agent to resume\n b. Reassign to new agent\n c. Escalate to Mayor\n\n### Squash fails\n- Retry with exponential backoff\n- If persistent failure, escalate\n- Don't lose the ephemeral state until squash succeeds\n\n### Orphaned molecules\n- Molecule started, never completed\n- gt doctor detects (\u003e24h old with no activity)\n- Options:\n a. Manual review\n b. Auto-abandon with 'abandoned' digest\n c. Reassign\n\n### Ephemeral repo corruption\n- Re-init from scratch\n- Active molecules are lost\n- Main beads is source of truth for assigned work\n\n## Implementation\n\nAdd error handling to:\n- bd mol bond (handle init failures)\n- bd squash (retry logic)\n- gt doctor (recovery suggestions)","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T14:34:30.158784-08:00","updated_at":"2025-12-21T14:34:30.158784-08:00","dependencies":[{"issue_id":"gt-3x0z.12","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:34:30.15923-08:00","created_by":"daemon"},{"issue_id":"gt-3x0z.12","depends_on_id":"gt-3x0z.9","type":"blocks","created_at":"2025-12-21T14:34:40.886432-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.2","title":"Phase 1.2: Configure bd for ephemeral molecule bonding","description":"Ensure bd mol bond --ephemeral works with Gas Town setup.\n\n## Questions for Dave\n\n1. Does bd automatically find .beads-ephemeral/ or need explicit path?\n2. How does bd mol bond --ephemeral know which repo to use?\n3. Is there a redirect mechanism for ephemeral like main beads?\n\n## Integration\n\nFrom polecat/crew working directory:\n```bash\nbd mol bond mol-polecat-work --ephemeral --assignee $(gt whoami)\n```\n\nShould create molecule in rig's .beads-ephemeral/, not main beads.","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/nux","created_at":"2025-12-21T14:33:23.777969-08:00","updated_at":"2025-12-21T15:51:49.32717-08:00","dependencies":[{"issue_id":"gt-3x0z.2","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:33:23.77835-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.3","title":"Phase 1.3: gt doctor checks for ephemeral health","description":"Add doctor checks for ephemeral beads repo.\n\n## Checks\n\n1. **ephemeral-exists**: .beads-ephemeral/ directory exists for each rig\n2. **ephemeral-git**: It's a valid git repo\n3. **ephemeral-orphans**: Molecules started but never squashed (\u003e24h old)\n4. **ephemeral-size**: Warn if ephemeral repo is \u003e100MB (should be cleaned)\n5. **ephemeral-stale**: Molecules with no activity in last hour\n\n## Auto-fix\n\n--fix can:\n- Create missing ephemeral repo\n- Clean up old completed molecules (already squashed)\n- NOT auto-squash incomplete molecules (needs AI summary)","status":"closed","priority":2,"issue_type":"task","assignee":"gastown/slit","created_at":"2025-12-21T14:33:23.864314-08:00","updated_at":"2025-12-21T15:41:48.833942-08:00","closed_at":"2025-12-21T15:41:48.833942-08:00","close_reason":"Implemented 5 wisp doctor checks with auto-fix support","dependencies":[{"issue_id":"gt-3x0z.3","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:33:23.864678-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.2","title":"Phase 1.2: Configure bd for ephemeral molecule bonding","description":"Ensure bd mol bond --ephemeral works with Gas Town setup.\n\n## Questions for Dave\n\n1. Does bd automatically find .beads-ephemeral/ or need explicit path?\n2. How does bd mol bond --ephemeral know which repo to use?\n3. Is there a redirect mechanism for ephemeral like main beads?\n\n## Integration\n\nFrom polecat/crew working directory:\n```bash\nbd mol bond mol-polecat-work --ephemeral --assignee $(gt whoami)\n```\n\nShould create molecule in rig's .beads-ephemeral/, not main beads.","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/nux","created_at":"2025-12-21T14:33:23.777969-08:00","updated_at":"2025-12-21T15:28:17.365238-08:00","dependencies":[{"issue_id":"gt-3x0z.2","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:33:23.77835-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.3","title":"Phase 1.3: gt doctor checks for ephemeral health","description":"Add doctor checks for ephemeral beads repo.\n\n## Checks\n\n1. **ephemeral-exists**: .beads-ephemeral/ directory exists for each rig\n2. **ephemeral-git**: It's a valid git repo\n3. **ephemeral-orphans**: Molecules started but never squashed (\u003e24h old)\n4. **ephemeral-size**: Warn if ephemeral repo is \u003e100MB (should be cleaned)\n5. **ephemeral-stale**: Molecules with no activity in last hour\n\n## Auto-fix\n\n--fix can:\n- Create missing ephemeral repo\n- Clean up old completed molecules (already squashed)\n- NOT auto-squash incomplete molecules (needs AI summary)","status":"in_progress","priority":2,"issue_type":"task","assignee":"gastown/slit","created_at":"2025-12-21T14:33:23.864314-08:00","updated_at":"2025-12-21T15:36:42.258759-08:00","dependencies":[{"issue_id":"gt-3x0z.3","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:33:23.864678-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.4","title":"Phase 2.1: gt spawn --molecule bonds in ephemeral","description":"Make gt spawn molecule-aware with ephemeral bonding.\n\n## New Flag\n\n```bash\ngt spawn --issue gt-xxx --molecule mol-polecat-work\n```\n\n## Behavior\n\n1. Create polecat with fresh worktree (existing)\n2. Bond molecule in ephemeral: `bd mol bond mol-polecat-work --ephemeral`\n3. Link molecule root to source issue\n4. Include molecule context in work assignment mail\n5. Start session\n\n## Work Assignment Mail\n\n```\nSubject: Work Assignment: Fix lifecycle bug [MOLECULE]\n\nYou are working on gt-rixa as part of molecule mol-polecat-work.\n\nMolecule instance: eph-abc123\nCurrent step: read-assignment (1/8)\n\nFollow the molecule workflow. When complete, generate summary and squash.\n```","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-21T14:33:45.082722-08:00","updated_at":"2025-12-21T14:33:45.082722-08:00","dependencies":[{"issue_id":"gt-3x0z.4","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:33:45.084965-08:00","created_by":"daemon"},{"issue_id":"gt-3x0z.4","depends_on_id":"gt-3x0z.1","type":"blocks","created_at":"2025-12-21T14:34:40.385365-08:00","created_by":"daemon"},{"issue_id":"gt-3x0z.4","depends_on_id":"gt-3x0z.2","type":"blocks","created_at":"2025-12-21T14:34:40.457259-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.5","title":"Phase 2.2: gt prime shows ephemeral molecule context","description":"Update gt prime to detect and display ephemeral molecule state.\n\n## Detection\n\n1. Check for active ephemeral molecule assigned to current identity\n2. Parse molecule progress (current step, total steps)\n3. Show in prime output\n\n## Output\n\n```\n🔧 Polecat furiosa, checking in.\n\n📦 Molecule: mol-polecat-work (eph-abc123)\n Step 3/8: do-work\n Source: gt-rixa\n Started: 10 minutes ago\n\nRun 'bd mol status' for full molecule state.\n```","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-21T14:33:45.164431-08:00","updated_at":"2025-12-21T14:33:45.164431-08:00","dependencies":[{"issue_id":"gt-3x0z.5","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:33:45.16487-08:00","created_by":"daemon"},{"issue_id":"gt-3x0z.5","depends_on_id":"gt-3x0z.4","type":"blocks","created_at":"2025-12-21T15:22:46.151626-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-3x0z.6","title":"Phase 2.3: Polecat CLAUDE.md molecule workflow protocol","description":"Update polecat prompting for molecule-based work.\n\n## CLAUDE.md Updates\n\nAdd section on molecule workflow:\n\n```markdown\n## Molecule Workflow\n\nWhen assigned a molecule (check gt prime output):\n\n1. Follow molecule steps in order\n2. Mark steps complete: bd mol step complete \u003cstep-id\u003e\n3. Before signaling done:\n a. Generate summary of work performed\n b. Run: bd squash \u003cmolecule-root\u003e --summary \"\u003cyour summary\u003e\"\n4. Then signal done as normal\n\n### Summary Guidelines\n\nYour summary should include:\n- What was the task?\n- What did you do?\n- What was the outcome?\n- Any issues or follow-ups?\n\nKeep it to 2-4 sentences. This becomes the permanent record.\n```","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-21T14:33:45.24122-08:00","updated_at":"2025-12-21T14:33:45.24122-08:00","dependencies":[{"issue_id":"gt-3x0z.6","depends_on_id":"gt-3x0z","type":"parent-child","created_at":"2025-12-21T14:33:45.241575-08:00","created_by":"daemon"},{"issue_id":"gt-3x0z.6","depends_on_id":"gt-3x0z.4","type":"blocks","created_at":"2025-12-21T15:22:47.355729-08:00","created_by":"daemon"}]}
|
||||
@@ -97,7 +97,6 @@
|
||||
{"id":"gt-5af.8","title":"Add human escalation configuration","description":"Add overseer contact config to mayor/town.json. Deacon uses this for escalation when it cannot resolve issues. Support email and/or other notification methods.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T17:14:02.770713-08:00","updated_at":"2025-12-20T21:00:03.94794-08:00","closed_at":"2025-12-20T20:40:29.515593-08:00","dependencies":[{"issue_id":"gt-5af.8","depends_on_id":"gt-5af","type":"parent-child","created_at":"2025-12-19T17:14:02.772612-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-5ft3","title":"Merge: gt-99a","description":"branch: polecat/Slit\ntarget: main\nsource_issue: gt-99a\nrig: gastown","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-19T16:18:15.329358-08:00","updated_at":"2025-12-19T18:26:14.101606-08:00","closed_at":"2025-12-19T17:48:44.671082-08:00"}
|
||||
{"id":"gt-5gkd","title":"Refinery Engineer: Role prompting and CLAUDE.md","description":"Create refinery/CLAUDE.md with Chief Merge Engineer role context. Include:\n- Role identity and responsibilities\n- Decision authority (merge order, test frequency, binary rebuilds)\n- Communication patterns (Witness, Deacon)\n- Session lifecycle (handoff bead protocol)\n- Key commands reference","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-19T18:09:06.423153-08:00","updated_at":"2025-12-19T18:09:06.423153-08:00","dependencies":[{"issue_id":"gt-5gkd","depends_on_id":"gt-ktal","type":"blocks","created_at":"2025-12-19T18:09:39.217523-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-5gsx","title":"Merge: gt-3x0z.3","description":"branch: polecat/slit\ntarget: main\nsource_issue: gt-3x0z.3\nrig: gastown","status":"open","priority":2,"issue_type":"merge-request","created_at":"2025-12-21T15:42:20.546839-08:00","updated_at":"2025-12-21T15:42:20.546839-08:00"}
|
||||
{"id":"gt-5qc","title":"Document how to configure a Gas Town Harness","description":"Create docs explaining what a harness is (private repo containing GT installation with rigs gitignored), why you'd want one, and how to set it up with beads redirects","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T16:42:43.370167-08:00","updated_at":"2025-12-19T12:00:39.274285-08:00","closed_at":"2025-12-19T12:00:39.274285-08:00","dependencies":[{"issue_id":"gt-5qc","depends_on_id":"gt-l1o","type":"blocks","created_at":"2025-12-17T16:42:54.620984-08:00","created_by":"daemon"},{"issue_id":"gt-5qc","depends_on_id":"gt-cr9","type":"blocks","created_at":"2025-12-17T17:15:59.17545-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-5tp","title":"Test message","description":"Testing GGT mail via beads","status":"closed","priority":2,"issue_type":"message","created_at":"2025-12-16T21:44:27.546781-08:00","updated_at":"2025-12-16T21:45:11.465745-08:00","closed_at":"2025-12-16T21:45:11.465745-08:00"}
|
||||
{"id":"gt-5v29","title":"Add 'wit' alias for witness command","description":"ref works as alias for refinery, but wit doesn't work for witness. Add Aliases: []string{\"wit\"} to witnessCmd.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-20T23:11:53.453692-08:00","updated_at":"2025-12-20T23:11:53.453692-08:00"}
|
||||
@@ -187,7 +186,7 @@
|
||||
{"id":"gt-b3p","title":"Resource Beads: Leases, locks, and quotas","description":"Resource beads represent reserved resources. Types: vm, lock, slot, quota. Fields: holder, expires, renewable. Daemon monitors for expiry and manages contention.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-18T18:08:12.745602-08:00","updated_at":"2025-12-18T18:08:12.745602-08:00"}
|
||||
{"id":"gt-b5sh","title":"test-after-fix","description":"test","status":"open","priority":2,"issue_type":"message","created_at":"2025-12-19T16:05:08.538763-08:00","updated_at":"2025-12-19T16:05:08.538763-08:00"}
|
||||
{"id":"gt-bcwn","title":"Auto-handoff: self-cycling with guaranteed work pickup","description":"## Summary\n\nA mechanism for agents to self-cycle: send handoff mail, request Deacon to kill the process, and automatically restart to pick up the work.\n\n## Use Case\n\nUser says: `auto-handoff: gt-bug3`\n\nThe agent:\n1. Sends handoff mail documenting current state\n2. Requests Deacon to terminate the session\n3. Deacon restarts the agent in background\n4. New session picks up the handoff and continues working\n\nWhen the user returns to terminal, the old session is gone but work continues in background. User can reattach.\n\n## Key Requirements\n\n- **Guaranteed pickup**: Something in the hook and/or handoff must ensure the new session picks up the work (not just a passive \"check inbox\")\n- **Background execution**: Work continues without user presence\n- **Reattachability**: User can reconnect to see progress\n\n## Design Questions\n\n1. Can molecules help coordinate this? (e.g., molecule binds the handoff + auto-restart)\n2. What hook handles the pickup guarantee? \n3. How does Deacon know to restart vs just kill?\n4. How does user reattach to a background agent?\n\n## Related\n\n- Molecules system\n- Deacon lifecycle management\n- Handoff hooks","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-21T11:59:50.352286-08:00","updated_at":"2025-12-21T12:00:06.867846-08:00"}
|
||||
{"id":"gt-bd2l","title":"Witness tmux status: show polecat count under management","description":"Add witness-specific status line showing:\n- Number of polecats under management\n- Active/idle status\n- Maybe: last nudge time, blocked count\n\nImplement in runWitnessStatusLine() in internal/cmd/statusline.go","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-21T15:40:29.482141-08:00","updated_at":"2025-12-21T15:47:49.492436-08:00","closed_at":"2025-12-21T15:47:49.492436-08:00","close_reason":"Implemented status line functions for witness and refinery"}
|
||||
{"id":"gt-bd2l","title":"Witness tmux status: show polecat count under management","description":"Add witness-specific status line showing:\n- Number of polecats under management\n- Active/idle status\n- Maybe: last nudge time, blocked count\n\nImplement in runWitnessStatusLine() in internal/cmd/statusline.go","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-21T15:40:29.482141-08:00","updated_at":"2025-12-21T15:40:29.482141-08:00"}
|
||||
{"id":"gt-beqp","title":"Merge: gt-72so","description":"branch: polecat/Doof\ntarget: main\nsource_issue: gt-72so\nrig: gastown","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-19T16:19:11.814962-08:00","updated_at":"2025-12-19T18:26:14.103556-08:00","closed_at":"2025-12-19T17:47:03.61793-08:00"}
|
||||
{"id":"gt-bfd","title":"Keepalive signal from bd/gt commands","description":"Every bd and gt command should touch a keepalive file to signal 'agent is alive/working'.\n\n## Implementation\n\nTouch `\u003cworkspace\u003e/.gastown/keepalive.json`:\n```json\n{\"last_command\": \"bd show gt-99m\", \"timestamp\": \"2025-12-18T13:45:00Z\"}\n```\n\n## Usage by Daemon\n\n- Fresh (\u003c 2 min) → agent is working, skip heartbeat\n- Stale (2-5 min) → might be thinking, gentle poke\n- Very stale (\u003e 5 min) → likely idle, safe to interrupt\n\n## Benefits\n\n- Zero cost (just file I/O)\n- Works during long tool calls\n- Doesn't require agent cooperation\n- Foundation for smarter backoff strategies","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-18T14:19:26.241957-08:00","updated_at":"2025-12-18T20:03:43.816952-08:00","closed_at":"2025-12-18T20:03:43.816952-08:00","dependencies":[{"issue_id":"gt-bfd","depends_on_id":"gt-99m","type":"blocks","created_at":"2025-12-18T14:19:46.407664-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-bj6f","title":"gt prime: Refinery context detection and output","description":"Update gt prime to detect Refinery role:\n- Detect from directory: refinery/rig/ = Refinery\n- Show handoff bead reference\n- Show merge queue status\n- Show polecat branches with unmerged commits\n- Show last actions summary","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-19T18:09:09.31791-08:00","updated_at":"2025-12-19T18:09:09.31791-08:00","dependencies":[{"issue_id":"gt-bj6f","depends_on_id":"gt-ktal","type":"blocks","created_at":"2025-12-19T18:09:39.462238-08:00","created_by":"daemon"}]}
|
||||
@@ -289,7 +288,6 @@
|
||||
{"id":"gt-h5n.9","title":"Per-epic config overrides: custom merge settings","description":"Allow per-epic merge configuration overrides.\n\nEpic can specify merge_config in its description:\n merge_config:\n run_tests: true\n test_command: 'go test -race ./...'\n on_conflict: assign_back\n\nWhen processing MRs for an epic:\n1. Load rig-level merge_queue config\n2. Check if epic has merge_config\n3. Merge epic config over rig config\n4. Use merged config for processing\n\nUse cases:\n- Risky refactors: more thorough testing\n- Urgent fixes: skip some checks\n- Experimental work: different test suite\n\nReference: docs/merge-queue-design.md#per-epic-overrides","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-17T13:52:02.32832-08:00","updated_at":"2025-12-17T13:52:02.32832-08:00","dependencies":[{"issue_id":"gt-h5n.9","depends_on_id":"gt-h5n","type":"parent-child","created_at":"2025-12-17T13:52:02.32997-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-hcc0","title":"gt polecat remove --all: bulk polecat teardown","description":"Currently gt polecat remove only accepts one polecat at a time. Need bulk operations:\n\n## Requested\n- `gt polecat remove gastown --all` - remove all polecats from a rig\n- `gt polecat remove gastown/A gastown/B ...` - remove multiple by name\n\n## Context\nAfter a swarm completes, tearing down 20 polecats one at a time is tedious.\nEphemeral workers should be easy to create and destroy in bulk.\n\n## Related\n- gt-c92: CLI: all command for batch polecat operations","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-19T14:06:43.892225-08:00","updated_at":"2025-12-20T13:16:06.901839-08:00","closed_at":"2025-12-20T13:16:06.901839-08:00"}
|
||||
{"id":"gt-hgk","title":"Mail system: message types and threading","description":"GGT mail system needs message types and threading like PGT.\n\n## 1. Message Types\nAdd to internal/mail/types.go:\n```go\ntype MessageType string\nconst (\n TypeTask MessageType = \"task\" // Required processing\n TypeScavenge MessageType = \"scavenge\" // Optional first-come work\n TypeNotification MessageType = \"notification\" // Informational\n TypeReply MessageType = \"reply\" // Response to message\n)\n\n// Update Message struct\ntype Message struct {\n // existing fields...\n Type MessageType `json:\"type\"`\n ThreadID string `json:\"thread_id,omitempty\"`\n ReplyTo string `json:\"reply_to,omitempty\"`\n}\n```\n\n## 2. Priority Levels\nExpand from 2 to 4:\n```go\ntype Priority string\nconst (\n PriorityLow Priority = \"low\"\n PriorityNormal Priority = \"normal\"\n PriorityHigh Priority = \"high\"\n PriorityUrgent Priority = \"urgent\"\n)\n```\n\n## 3. CLI Updates\ninternal/cmd/mail.go:\n- Add --type flag to send: `gt mail send ... --type task`\n- Add --reply-to flag: `gt mail send ... --reply-to \u003cmsg-id\u003e`\n- Add thread command: `gt mail thread \u003cthread-id\u003e`\n\n## 4. Threading Logic\nNewMessage() should auto-generate thread_id if not a reply.\nReply messages inherit thread_id from original.\n\n## Files to Modify\n- internal/mail/types.go: Add types, expand Priority\n- internal/mail/mailbox.go: Thread filtering\n- internal/cmd/mail.go: CLI flags and thread command\n\n## PGT Reference\ngastown-py/src/gastown/mail/message.py\n\n## Acceptance Criteria\n- [ ] Messages have type field (default: notification)\n- [ ] 4 priority levels supported\n- [ ] Reply creates thread with shared thread_id\n- [ ] gt mail thread \u003cid\u003e shows conversation","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-16T14:46:55.29463-08:00","updated_at":"2025-12-18T20:14:28.308997-08:00","closed_at":"2025-12-18T20:14:28.308997-08:00"}
|
||||
{"id":"gt-hoyd","title":"Merge: gt-rana.1","description":"branch: polecat/rictus\ntarget: main\nsource_issue: gt-rana.1\nrig: gastown","status":"open","priority":1,"issue_type":"merge-request","created_at":"2025-12-21T15:51:03.089517-08:00","updated_at":"2025-12-21T15:51:03.089517-08:00"}
|
||||
{"id":"gt-htto","title":"Heartbeat convention: simple liveness signal for agents","description":"Lightweight liveness signal extracted from Deacon epic (gt-5af).\n\n**Implementation**: Each agent writes a timestamp file on activity:\n```bash\necho '{\"ts\":\"'$(date -Iseconds)'\"}' \u003e ~/gt/\u003crole\u003e/heartbeat.json\n```\n\n**Integration points**:\n- SessionStart hook writes heartbeat\n- Periodic activity (mail check, work completion) refreshes it\n- `gt status` shows staleness (e.g., 'mayor: 5m ago')\n\n**Weight**: ~5 lines per agent\n**Value**: Quick debugging - see which agents are active at a glance\n\nNo monitoring daemon needed - human checks `gt status` when curious.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-20T20:40:45.459903-08:00","updated_at":"2025-12-20T20:40:45.459903-08:00"}
|
||||
{"id":"gt-hw6","title":"GGT Command Parity: Complete gt command coverage","description":"Complete gt command set to match/exceed PGT town commands.\n\nCovers: uninstall, rig info, refinery attach, witness, session mgmt, mail UX, daemon.","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-17T22:22:21.720078-08:00","updated_at":"2025-12-19T12:05:36.723692-08:00","closed_at":"2025-12-19T12:05:36.723692-08:00"}
|
||||
{"id":"gt-hzr","title":"gt witness: Witness management commands","description":"Add 'gt witness' command group for witness lifecycle management.\n\nSubcommands:\n- gt witness start [rig] - Start witness for a rig\n- gt witness stop [rig] - Stop witness\n- gt witness status [rig] - Show witness status\n- gt witness attach [rig] - Attach to witness session\n\nWitness monitors polecats and handles:\n- Idle detection and cleanup\n- Session health checks\n- Nudging stuck agents","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:47:32.210917-08:00","updated_at":"2025-12-19T12:05:27.343254-08:00","closed_at":"2025-12-19T12:05:27.343254-08:00","dependencies":[{"issue_id":"gt-hzr","depends_on_id":"gt-hw6","type":"blocks","created_at":"2025-12-17T22:23:42.955006-08:00","created_by":"daemon"}]}
|
||||
@@ -383,7 +381,7 @@
|
||||
{"id":"gt-r01","title":"EXTERNAL: Beads Messaging \u0026 Knowledge Graph (bd-kwro)","description":"Tracking issue for external dependency on Beads v0.30.2 messaging features.\n\nBeads epic: bd-kwro in ~/src/beads (steveyegge/beads repo)\n\nThis blocks GGT work that depends on:\n- bd mail send/inbox/read/ack commands\n- message issue type\n- replies_to threading\n- Hooks system for notifications\n- Identity configuration\n\nGGT mail commands will be thin wrappers around bd mail once available.\n\nWhen bd-kwro ships in Beads v0.30.2, close this and unblock dependent work.","status":"closed","issue_type":"task","created_at":"2025-12-16T13:12:02.676883-08:00","updated_at":"2025-12-16T21:35:05.795776-08:00","closed_at":"2025-12-16T21:35:05.795776-08:00"}
|
||||
{"id":"gt-r8ej","title":"Implement pinned beads for handoff state","description":"Add pinned bead support to beads:\n- Pinned beads never close, only update\n- Use for persistent state like Refinery handoffs\n- bd create --pinned flag\n- bd list --pinned to find them\n- Update description to change state","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-19T18:09:08.019293-08:00","updated_at":"2025-12-19T18:09:08.019293-08:00","dependencies":[{"issue_id":"gt-r8ej","depends_on_id":"gt-ktal","type":"blocks","created_at":"2025-12-19T18:09:39.340915-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-rana","title":"Patrol System: Agent lifecycle loops with attachments","description":"Enable Gas Town agents to run continuous patrols, survive crashes, and hand off work across sessions.\n\n## Core Concepts\n- **Attachments**: Molecules bound to agent's pinned bead until complete\n- **Patrols**: Cyclic molecules that loop (deacon, witness, refinery)\n- **Quiescent**: Agents that sleep until triggered (witness, refinery)\n\n## Design Doc\nSee docs/patrol-system-design.md\n\n## Phases\nPhase 1: Foundation (attachment field, daemon detection, mol-deacon-patrol)\nPhase 2: Quiescent Agents (wake triggers, witness/refinery patrols)\nPhase 3: Callbacks and Plugins (mail protocol, plugin runner)\nPhase 4: Polish (gt patrol status, metrics, tuning)\n\n## Key Decisions\n- Attachment as field on pinned bead (not edge type, for now)\n- Mail-based orchestration for all callbacks\n- Queue replacement for heartbeat delivery (not pile-up)\n- Burn and respawn for patrol loops (not in-place reset)","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-21T13:38:23.416949-08:00","updated_at":"2025-12-21T13:38:23.416949-08:00"}
|
||||
{"id":"gt-rana.1","title":"Phase 1.1: Attachment field on pinned beads","description":"Add attached_molecule field to pinned bead schema.\n\n## Schema Change\nAdd to handoff/pinned bead issues:\n- attached_molecule: string (root issue ID of attached mol)\n- attached_at: timestamp\n\n## Implementation\n- Update beads Issue struct if needed\n- Or: use labels/metadata field\n- Ensure bd show displays attachment info\n\n## Acceptance\n- Can set/clear attachment on a bead\n- bd show displays attachment status","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-21T13:38:34.251531-08:00","updated_at":"2025-12-21T15:51:01.902014-08:00","closed_at":"2025-12-21T15:51:01.902014-08:00","close_reason":"Implemented attachment fields: AttachMolecule, DetachMolecule, GetAttachment APIs","dependencies":[{"issue_id":"gt-rana.1","depends_on_id":"gt-rana","type":"parent-child","created_at":"2025-12-21T13:38:34.253378-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-rana.1","title":"Phase 1.1: Attachment field on pinned beads","description":"Add attached_molecule field to pinned bead schema.\n\n## Schema Change\nAdd to handoff/pinned bead issues:\n- attached_molecule: string (root issue ID of attached mol)\n- attached_at: timestamp\n\n## Implementation\n- Update beads Issue struct if needed\n- Or: use labels/metadata field\n- Ensure bd show displays attachment info\n\n## Acceptance\n- Can set/clear attachment on a bead\n- bd show displays attachment status","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/rictus","created_at":"2025-12-21T13:38:34.251531-08:00","updated_at":"2025-12-21T15:36:39.333816-08:00","dependencies":[{"issue_id":"gt-rana.1","depends_on_id":"gt-rana","type":"parent-child","created_at":"2025-12-21T13:38:34.253378-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-rana.2","title":"Phase 1.2: Daemon attachment detection","description":"Daemon polls Deacon's pinned bead to detect naked state.\n\n## Implementation\n- Daemon knows Deacon's pinned bead ID\n- Every heartbeat cycle: bd show \u003cpinned\u003e --json\n- Parse attached_molecule field\n- If null/empty: Deacon is naked\n\n## Actions on Naked\n- Spawn mol-deacon-patrol\n- Attach to Deacon's pinned\n- Nudge Deacon to start\n\n## Failsafes\n- Keepalive file monitoring\n- Stale detection (\u003e10min no progress)\n- Force nudge on stale\n\nDepends: gt-rana.1","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-21T13:38:53.520213-08:00","updated_at":"2025-12-21T13:38:53.520213-08:00","dependencies":[{"issue_id":"gt-rana.2","depends_on_id":"gt-rana","type":"parent-child","created_at":"2025-12-21T13:38:53.521672-08:00","created_by":"daemon"},{"issue_id":"gt-rana.2","depends_on_id":"gt-rana.1","type":"blocks","created_at":"2025-12-21T13:39:11.333008-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-rana.3","title":"Phase 1.3: mol-deacon-patrol definition","description":"Define the Deacon patrol molecule in builtin_molecules.go.\n\n## Steps\n1. inbox-check - Handle callbacks from agents\n2. health-scan - Ping Witnesses and Refineries\n3. plugin-run - Execute registered plugins\n4. orphan-check - Find abandoned work\n5. session-gc - Clean dead sessions\n6. context-check - Check own context limit\n7. loop-or-exit - Burn and let daemon respawn, or exit if context high\n\n## Implementation\n- Add DeaconPatrolMolecule() to builtin_molecules.go\n- Add to BuiltinMolecules() list\n- Test with bd mol show mol-deacon-patrol","status":"closed","priority":1,"issue_type":"task","assignee":"gastown/dementus","created_at":"2025-12-21T13:38:54.555938-08:00","updated_at":"2025-12-21T15:39:16.769062-08:00","closed_at":"2025-12-21T15:39:16.769062-08:00","close_reason":"Closed","dependencies":[{"issue_id":"gt-rana.3","depends_on_id":"gt-rana","type":"parent-child","created_at":"2025-12-21T13:38:54.55744-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-rana.4","title":"Phase 1.4: Basic patrol runner in Deacon","description":"Deacon CLAUDE.md and prime context for patrol execution.\n\n## Deacon Context\n- Update Deacon CLAUDE.md with patrol instructions\n- On wake: check pinned bead for attachment\n- If attached: resume molecule from current step\n- Execute steps, close each when done\n- On final step: burn molecule, go naked\n\n## gt prime Enhancement\n- Detect if agent has attached molecule\n- Show molecule progress in prime output\n- Include patrol-specific instructions\n\nDepends: gt-rana.3","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-21T13:38:55.457748-08:00","updated_at":"2025-12-21T13:38:55.457748-08:00","dependencies":[{"issue_id":"gt-rana.4","depends_on_id":"gt-rana","type":"parent-child","created_at":"2025-12-21T13:38:55.459852-08:00","created_by":"daemon"},{"issue_id":"gt-rana.4","depends_on_id":"gt-rana.3","type":"blocks","created_at":"2025-12-21T13:39:11.402784-08:00","created_by":"daemon"},{"issue_id":"gt-rana.4","depends_on_id":"gt-3x0z.8","type":"blocks","created_at":"2025-12-21T15:20:16.297449-08:00","created_by":"daemon"}]}
|
||||
@@ -479,7 +477,7 @@
|
||||
{"id":"gt-yls","title":"Document merge queue architecture","description":"Update docs/architecture.md with:\n\n- Merge Queue section explaining Beads-native approach\n- Engineer role (renamed from Refinery)\n- Session restart protocol\n- gt mq command reference\n- Federation considerations for queue\n\nAlso update any references to .gastown/ → config/.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-16T23:02:41.533065-08:00","updated_at":"2025-12-17T13:43:41.657433-08:00","closed_at":"2025-12-17T13:43:41.657433-08:00","dependencies":[{"issue_id":"gt-yls","depends_on_id":"gt-h5n","type":"blocks","created_at":"2025-12-16T23:02:56.043373-08:00","created_by":"daemon"}]}
|
||||
{"id":"gt-yx4","title":"Town root .beads has schema mismatch with bd","description":"The .beads directory at town root (/Users/stevey/gt/.beads) has an incompatible schema:\n\n```\nError: failed to open database: failed to initialize schema: sqlite3: SQL logic error: no such column: thread_id\n```\n\nMeanwhile, gastown/.beads (symlinked to mayor/rig/.beads) works fine.\n\n## Impact\n\n- gt mail inbox fails at town root\n- gt handoff sends mail to broken db\n- Daemon can't check its inbox\n\n## Options\n\n1. Delete town root .beads/beads.db and let it recreate\n2. Symlink town root .beads to gastown/.beads\n3. Run schema migration on existing db\n\n## Root Cause\n\nLikely a beads version upgrade that added thread_id column, but the town root db was created before that.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-18T14:31:35.559042-08:00","updated_at":"2025-12-19T00:39:32.211083-08:00","closed_at":"2025-12-19T00:39:32.211083-08:00"}
|
||||
{"id":"gt-z4g","title":"Plugin: Plan-to-Epic converter","description":"## Purpose\n\nHelp users create beads epics from various planning inputs.\n\n## Inputs\n- Markdown task lists\n- GitHub issues\n- Linear/Jira exports\n- Free-form descriptions\n- Existing beads epics\n\n## Output\n- Beads epic with properly structured children\n- Dependencies set for wave ordering\n- Priorities assigned\n- Ready for `gt spawn --epic \u003cid\u003e`\n\n## Implementation Options\n\n### Option A: CLI Tool\n```bash\ngt plan import --from github --repo owner/repo --label batch-candidate\ngt plan import --from markdown tasks.md\ngt plan structure \u003cepic-id\u003e # analyze and add dependencies\n```\n\n### Option B: Plugin Agent\nA plugin at `\u003crig\u003e/plugins/plan-oracle/` that:\n- Receives planning requests via mail\n- Analyzes scope and requirements\n- Creates structured beads epic\n- Sets dependencies based on analysis\n\n### Option C: Interactive Mode\n```bash\ngt plan create\n# Walks through questions, creates epic interactively\n```\n\n## Axiom\n\nAs stated: 'The Planning phase should end in the creation of a workable Beads plan.'\n\nThis plugin bridges the gap between human planning and machine-executable work.\n\n## Priority\n\nP2 - Nice to have for MVP. Manual epic creation works for now.\n\n## Note\n\nNo \"swarm IDs\" - output is just a beads epic with children. Workers process it independently.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T02:10:20.663549-08:00","updated_at":"2025-12-16T17:26:41.087304-08:00"}
|
||||
{"id":"gt-zayu","title":"Refinery tmux status: show merge queue length","description":"Add refinery-specific status line showing:\n- MQ length (pending merges)\n- Currently processing item (if any)\n- Maybe: success/failure counts\n\nImplement in runRefineryStatusLine() in internal/cmd/statusline.go","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-21T15:40:30.569547-08:00","updated_at":"2025-12-21T15:47:49.493735-08:00","closed_at":"2025-12-21T15:47:49.493735-08:00","close_reason":"Implemented status line functions for witness and refinery"}
|
||||
{"id":"gt-zayu","title":"Refinery tmux status: show merge queue length","description":"Add refinery-specific status line showing:\n- MQ length (pending merges)\n- Currently processing item (if any)\n- Maybe: success/failure counts\n\nImplement in runRefineryStatusLine() in internal/cmd/statusline.go","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-21T15:40:30.569547-08:00","updated_at":"2025-12-21T15:40:30.569547-08:00"}
|
||||
{"id":"gt-zhpa","title":"VC Pattern Integration: Bring validated ideas to Gas Town","description":"Analysis of ~/src/vc identified 6 validated patterns from the 2nd orchestrator attempt that map cleanly to Gas Town primitives. VC achieved 254 issues closed, 90.9% gate pass rate, and 24 successful missions.\n\nKey insight: VC built ~4300 lines of Go for features that become ~65 lines of YAML + CLI flags in Gas Town's architecture.\n\nChild tasks track each pattern to integrate.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-20T20:29:30.994181-08:00","updated_at":"2025-12-20T20:29:30.994181-08:00"}
|
||||
{"id":"gt-zivp","title":"mol-outpost-assign: Intelligent work routing to outposts","description":"The federation design shows outposts.yaml with a static policy section. Simple preference ordering is fine as config, but intelligent work routing should be a molecule.\n\nCurrent static approach:\n```yaml\npolicy:\n default_preference: [local, gce-burst, cloudrun-burst]\n```\n\nCases requiring cognition:\n- \"This is a long-running research task, route to VM not CloudRun\"\n- \"This touches sensitive code, keep local\"\n- \"These 5 issues are related, batch them to same outpost\"\n- \"CloudRun cost is high today, prefer local even if slower\"\n\n## Molecule: outpost-assign\nIntelligent work-to-outpost routing.\n\n## Step: classify-work\nAnalyze the issue/work item:\n- Expected duration (quick fix vs multi-hour)\n- Resource requirements\n- Sensitivity/security tier\n- Related work (same epic?)\n\n## Step: check-capacity\nQuery outpost status:\n- Current load on each\n- Cost accrued today\n- Health status\n\n## Step: select-outpost\nChoose optimal outpost based on:\n- Work classification\n- Capacity/cost\n- Policy constraints\nNeeds: classify-work, check-capacity\n\n## Step: emit-assignment\nRecord decision in beads for audit.\nNeeds: select-outpost\n\n## Notes\n- This molecule is invoked by Mayor/Witness when spawning\n- Simple cases can short-circuit to static policy\n- Full analysis only for ambiguous cases","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-20T03:26:17.964834-08:00","updated_at":"2025-12-20T03:26:17.964834-08:00"}
|
||||
{"id":"gt-zko","title":"gt rig info: Show detailed rig information","description":"Add 'gt rig info \u003crig\u003e' command to show detailed rig status.\n\nShould show:\n- Rig path and git URL\n- Active polecats with status\n- Refinery status\n- Witness status\n- Recent activity\n- Beads summary (open issues count)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:47:17.879255-08:00","updated_at":"2025-12-17T22:29:46.970197-08:00","closed_at":"2025-12-17T22:29:46.970197-08:00","dependencies":[{"issue_id":"gt-zko","depends_on_id":"gt-hw6","type":"blocks","created_at":"2025-12-17T22:22:47.502099-08:00","created_by":"daemon"}]}
|
||||
|
||||
@@ -61,6 +61,13 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
||||
d.Register(doctor.NewBranchCheck())
|
||||
d.Register(doctor.NewBeadsSyncOrphanCheck())
|
||||
|
||||
// Ephemeral beads checks
|
||||
d.Register(doctor.NewEphemeralExistsCheck())
|
||||
d.Register(doctor.NewEphemeralGitCheck())
|
||||
d.Register(doctor.NewEphemeralOrphansCheck())
|
||||
d.Register(doctor.NewEphemeralSizeCheck())
|
||||
d.Register(doctor.NewEphemeralStaleCheck())
|
||||
|
||||
// Run checks
|
||||
var report *doctor.Report
|
||||
if doctorFix {
|
||||
|
||||
519
internal/doctor/ephemeral_check.go
Normal file
519
internal/doctor/ephemeral_check.go
Normal file
@@ -0,0 +1,519 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
)
|
||||
|
||||
// EphemeralExistsCheck verifies that .beads-ephemeral/ exists for each rig.
|
||||
type EphemeralExistsCheck struct {
|
||||
FixableCheck
|
||||
missingRigs []string // Cached for fix
|
||||
}
|
||||
|
||||
// NewEphemeralExistsCheck creates a new ephemeral exists check.
|
||||
func NewEphemeralExistsCheck() *EphemeralExistsCheck {
|
||||
return &EphemeralExistsCheck{
|
||||
FixableCheck: FixableCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "ephemeral-exists",
|
||||
CheckDescription: "Check if ephemeral beads directory exists for each rig",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run checks if .beads-ephemeral/ exists for each rig.
|
||||
func (c *EphemeralExistsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
c.missingRigs = nil // Reset cache
|
||||
|
||||
// Find all rigs
|
||||
rigs, err := c.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",
|
||||
}
|
||||
}
|
||||
|
||||
// Check each rig
|
||||
var missing []string
|
||||
for _, rigName := range rigs {
|
||||
ephemeralPath := filepath.Join(ctx.TownRoot, rigName, ".beads-ephemeral")
|
||||
if _, err := os.Stat(ephemeralPath); os.IsNotExist(err) {
|
||||
missing = append(missing, rigName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
c.missingRigs = missing
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("%d rig(s) missing ephemeral beads directory", len(missing)),
|
||||
Details: missing,
|
||||
FixHint: "Run 'gt doctor --fix' to create missing directories",
|
||||
}
|
||||
}
|
||||
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("All %d rig(s) have ephemeral beads directory", len(rigs)),
|
||||
}
|
||||
}
|
||||
|
||||
// Fix creates missing .beads-ephemeral/ directories.
|
||||
func (c *EphemeralExistsCheck) Fix(ctx *CheckContext) error {
|
||||
for _, rigName := range c.missingRigs {
|
||||
ephemeralPath := filepath.Join(ctx.TownRoot, rigName, ".beads-ephemeral")
|
||||
if err := os.MkdirAll(ephemeralPath, 0755); err != nil {
|
||||
return fmt.Errorf("creating %s: %w", ephemeralPath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// discoverRigs finds all registered rigs.
|
||||
func (c *EphemeralExistsCheck) discoverRigs(townRoot string) ([]string, error) {
|
||||
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
||||
data, err := os.ReadFile(rigsPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil // No rigs configured
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rigsConfig config.RigsConfig
|
||||
if err := json.Unmarshal(data, &rigsConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rigs []string
|
||||
for name := range rigsConfig.Rigs {
|
||||
rigs = append(rigs, name)
|
||||
}
|
||||
return rigs, nil
|
||||
}
|
||||
|
||||
// EphemeralGitCheck verifies that .beads-ephemeral/ is a valid git repo.
|
||||
type EphemeralGitCheck struct {
|
||||
FixableCheck
|
||||
invalidRigs []string // Cached for fix
|
||||
}
|
||||
|
||||
// NewEphemeralGitCheck creates a new ephemeral git check.
|
||||
func NewEphemeralGitCheck() *EphemeralGitCheck {
|
||||
return &EphemeralGitCheck{
|
||||
FixableCheck: FixableCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "ephemeral-git",
|
||||
CheckDescription: "Check if ephemeral beads directories are valid git repos",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run checks if .beads-ephemeral/ directories are valid git repos.
|
||||
func (c *EphemeralGitCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
c.invalidRigs = nil // Reset cache
|
||||
|
||||
// Find all rigs
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
// Check each rig that has an ephemeral dir
|
||||
var invalid []string
|
||||
var checked int
|
||||
for _, rigName := range rigs {
|
||||
ephemeralPath := filepath.Join(ctx.TownRoot, rigName, ".beads-ephemeral")
|
||||
if _, err := os.Stat(ephemeralPath); os.IsNotExist(err) {
|
||||
continue // Skip if directory doesn't exist (handled by ephemeral-exists)
|
||||
}
|
||||
checked++
|
||||
|
||||
// Check if it's a valid git repo
|
||||
gitDir := filepath.Join(ephemeralPath, ".git")
|
||||
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
|
||||
invalid = append(invalid, rigName)
|
||||
}
|
||||
}
|
||||
|
||||
if checked == 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "No ephemeral beads directories to check",
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalid) > 0 {
|
||||
c.invalidRigs = invalid
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("%d ephemeral beads directory(ies) not initialized as git", len(invalid)),
|
||||
Details: invalid,
|
||||
FixHint: "Run 'gt doctor --fix' to initialize git repos",
|
||||
}
|
||||
}
|
||||
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("All %d ephemeral beads directories are valid git repos", checked),
|
||||
}
|
||||
}
|
||||
|
||||
// Fix initializes git repos in ephemeral directories.
|
||||
func (c *EphemeralGitCheck) Fix(ctx *CheckContext) error {
|
||||
for _, rigName := range c.invalidRigs {
|
||||
ephemeralPath := filepath.Join(ctx.TownRoot, rigName, ".beads-ephemeral")
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = ephemeralPath
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("initializing git in %s: %w", ephemeralPath, err)
|
||||
}
|
||||
|
||||
// Create config.yaml for ephemeral beads
|
||||
configPath := filepath.Join(ephemeralPath, "config.yaml")
|
||||
configContent := "ephemeral: true\n# No sync-branch - ephemeral is local only\n"
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
return fmt.Errorf("creating config.yaml in %s: %w", ephemeralPath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EphemeralOrphansCheck detects molecules started but never squashed (>24h old).
|
||||
type EphemeralOrphansCheck struct {
|
||||
BaseCheck
|
||||
}
|
||||
|
||||
// NewEphemeralOrphansCheck creates a new ephemeral orphans check.
|
||||
func NewEphemeralOrphansCheck() *EphemeralOrphansCheck {
|
||||
return &EphemeralOrphansCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "ephemeral-orphans",
|
||||
CheckDescription: "Check for orphaned molecules (>24h old, never squashed)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run checks for orphaned molecules.
|
||||
func (c *EphemeralOrphansCheck) 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 orphans []string
|
||||
cutoff := time.Now().Add(-24 * time.Hour)
|
||||
|
||||
for _, rigName := range rigs {
|
||||
ephemeralPath := filepath.Join(ctx.TownRoot, rigName, ".beads-ephemeral")
|
||||
if _, err := os.Stat(ephemeralPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for molecule directories or issue files older than 24h
|
||||
issuesPath := filepath.Join(ephemeralPath, "issues.jsonl")
|
||||
info, err := os.Stat(issuesPath)
|
||||
if err != nil {
|
||||
continue // No issues file
|
||||
}
|
||||
|
||||
// Check if the issues file is old and non-empty
|
||||
if info.ModTime().Before(cutoff) && info.Size() > 0 {
|
||||
orphans = append(orphans, fmt.Sprintf("%s: issues.jsonl last modified %s",
|
||||
rigName, info.ModTime().Format("2006-01-02 15:04")))
|
||||
}
|
||||
}
|
||||
|
||||
if len(orphans) > 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("%d rig(s) have stale ephemeral data (>24h old)", len(orphans)),
|
||||
Details: orphans,
|
||||
FixHint: "Manual review required - these may contain unsquashed work",
|
||||
}
|
||||
}
|
||||
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "No orphaned molecules found",
|
||||
}
|
||||
}
|
||||
|
||||
// EphemeralSizeCheck warns if ephemeral repo is too large (>100MB).
|
||||
type EphemeralSizeCheck struct {
|
||||
BaseCheck
|
||||
}
|
||||
|
||||
// NewEphemeralSizeCheck creates a new ephemeral size check.
|
||||
func NewEphemeralSizeCheck() *EphemeralSizeCheck {
|
||||
return &EphemeralSizeCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "ephemeral-size",
|
||||
CheckDescription: "Check if ephemeral beads directories are too large (>100MB)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run checks the size of ephemeral beads directories.
|
||||
func (c *EphemeralSizeCheck) 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",
|
||||
}
|
||||
}
|
||||
|
||||
const maxSize = 100 * 1024 * 1024 // 100MB
|
||||
var oversized []string
|
||||
|
||||
for _, rigName := range rigs {
|
||||
ephemeralPath := filepath.Join(ctx.TownRoot, rigName, ".beads-ephemeral")
|
||||
if _, err := os.Stat(ephemeralPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
size, err := dirSize(ephemeralPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if size > maxSize {
|
||||
oversized = append(oversized, fmt.Sprintf("%s: %s",
|
||||
rigName, formatSize(size)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(oversized) > 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("%d rig(s) have oversized ephemeral directories", len(oversized)),
|
||||
Details: oversized,
|
||||
FixHint: "Consider cleaning up old completed molecules",
|
||||
}
|
||||
}
|
||||
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "All ephemeral directories within size limits",
|
||||
}
|
||||
}
|
||||
|
||||
// EphemeralStaleCheck detects molecules with no activity in the last hour.
|
||||
type EphemeralStaleCheck struct {
|
||||
BaseCheck
|
||||
}
|
||||
|
||||
// NewEphemeralStaleCheck creates a new ephemeral stale check.
|
||||
func NewEphemeralStaleCheck() *EphemeralStaleCheck {
|
||||
return &EphemeralStaleCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "ephemeral-stale",
|
||||
CheckDescription: "Check for stale molecules (no activity in last hour)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run checks for stale molecules.
|
||||
func (c *EphemeralStaleCheck) 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 stale []string
|
||||
cutoff := time.Now().Add(-1 * time.Hour)
|
||||
|
||||
for _, rigName := range rigs {
|
||||
ephemeralPath := filepath.Join(ctx.TownRoot, rigName, ".beads-ephemeral")
|
||||
if _, err := os.Stat(ephemeralPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for any recent activity in the ephemeral directory
|
||||
// We look at the most recent modification time of any file
|
||||
var mostRecent time.Time
|
||||
_ = filepath.Walk(ephemeralPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() && info.ModTime().After(mostRecent) {
|
||||
mostRecent = info.ModTime()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// If there are files and the most recent is older than 1 hour
|
||||
if !mostRecent.IsZero() && mostRecent.Before(cutoff) {
|
||||
stale = append(stale, fmt.Sprintf("%s: last activity %s ago",
|
||||
rigName, formatDuration(time.Since(mostRecent))))
|
||||
}
|
||||
}
|
||||
|
||||
if len(stale) > 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("%d rig(s) have stale ephemeral activity", len(stale)),
|
||||
Details: stale,
|
||||
FixHint: "Check if polecats are stuck or crashed",
|
||||
}
|
||||
}
|
||||
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "No stale ephemeral activity detected",
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// discoverRigs finds all registered rigs (shared helper).
|
||||
func discoverRigs(townRoot string) ([]string, error) {
|
||||
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
||||
data, err := os.ReadFile(rigsPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rigsConfig config.RigsConfig
|
||||
if err := json.Unmarshal(data, &rigsConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rigs []string
|
||||
for name := range rigsConfig.Rigs {
|
||||
rigs = append(rigs, name)
|
||||
}
|
||||
return rigs, nil
|
||||
}
|
||||
|
||||
// dirSize calculates the total size of a directory.
|
||||
func dirSize(path string) (int64, error) {
|
||||
var size int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return size, err
|
||||
}
|
||||
|
||||
// formatSize formats bytes as human-readable size.
|
||||
func formatSize(bytes int64) string {
|
||||
const (
|
||||
KB = 1024
|
||||
MB = KB * 1024
|
||||
GB = MB * 1024
|
||||
)
|
||||
switch {
|
||||
case bytes >= GB:
|
||||
return fmt.Sprintf("%.1f GB", float64(bytes)/GB)
|
||||
case bytes >= MB:
|
||||
return fmt.Sprintf("%.1f MB", float64(bytes)/MB)
|
||||
case bytes >= KB:
|
||||
return fmt.Sprintf("%.1f KB", float64(bytes)/KB)
|
||||
default:
|
||||
return fmt.Sprintf("%d bytes", bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// formatDuration formats a duration as human-readable string.
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%.0f seconds", d.Seconds())
|
||||
}
|
||||
if d < time.Hour {
|
||||
return fmt.Sprintf("%.0f minutes", d.Minutes())
|
||||
}
|
||||
if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%.1f hours", d.Hours())
|
||||
}
|
||||
return fmt.Sprintf("%.1f days", d.Hours()/24)
|
||||
}
|
||||
Reference in New Issue
Block a user