Compare commits
259 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1701474b3d | ||
|
|
a7e9fbf699 | ||
|
|
358fcaf935 | ||
|
|
f19ddc5400 | ||
|
|
64b58b31ab | ||
|
|
afff85cdff | ||
|
|
a91e6cd643 | ||
|
|
9b2f4a7652 | ||
|
|
c8c97fdf64 | ||
|
|
43272f6fbb | ||
|
|
65c3e90374 | ||
|
|
0eacdd367b | ||
|
|
9fe9323b9c | ||
|
|
bfafb9c179 | ||
|
|
677a6ed84f | ||
|
|
da2d71c3fe | ||
|
|
e124402b7b | ||
|
|
705a7c2137 | ||
|
|
c2c6ddeaf9 | ||
|
|
b509107100 | ||
|
|
34cb28e0b9 | ||
|
|
1da3e18e60 | ||
|
|
5adb096d9d | ||
|
|
81bfe48ed3 | ||
|
|
41a758d6d8 | ||
|
|
5250e9e12a | ||
|
|
b3407759d2 | ||
|
|
c8c765a239 | ||
|
|
775af2973d | ||
|
|
da906847dd | ||
|
|
0a649e6faa | ||
|
|
fb40fa1405 | ||
|
|
7bfc2fcb76 | ||
|
|
376305e9d9 | ||
|
|
73f5b4025b | ||
|
|
c756f12d00 | ||
|
|
8d5611f14e | ||
|
|
98e154b18e | ||
|
|
38adfa4d8b | ||
|
|
03b0f7ff52 | ||
|
|
3b628150c2 | ||
|
|
1afe3fb823 | ||
|
|
caa88d96c5 | ||
|
|
4c9e8b8b99 | ||
|
|
c699e3e2ed | ||
|
|
65ecb6cafd | ||
|
|
540e33dbe9 | ||
|
|
85dd150d75 | ||
|
|
45634059dd | ||
|
|
d4da2b325d | ||
|
|
4985bdfbcc | ||
|
|
f4cbcb4ce9 | ||
|
|
c4d956ebe7 | ||
|
|
7f6fe53c6f | ||
|
|
19f4fa3ddb | ||
|
|
e648edce8c | ||
|
|
8a8b56e9e6 | ||
|
|
c91ab85457 | ||
|
|
00a59dec44 | ||
|
|
2de2d6b7e4 | ||
|
|
f30178265c | ||
|
|
5141facb21 | ||
|
|
15caf62b9f | ||
|
|
28a9de64d5 | ||
|
|
a9ed342be6 | ||
|
|
f9e788ccfb | ||
|
|
c220678162 | ||
|
|
b649635f48 | ||
|
|
117b91b87f | ||
|
|
ffa8dd56cb | ||
|
|
92042d679c | ||
|
|
585c204648 | ||
|
|
6209a49d54 | ||
|
|
ffeff97d9f | ||
|
|
9b5c889795 | ||
|
|
a07fa8bf7f | ||
|
|
06d40925d1 | ||
|
|
e2a211e295 | ||
|
|
b834bf5858 | ||
|
|
11d469edc3 | ||
|
|
de376007e0 | ||
|
|
5855b525fd | ||
|
|
f89c6f3693 | ||
|
|
ee167ae1f1 | ||
|
|
a6157829d7 | ||
|
|
4a94187068 | ||
|
|
93bdf88f6e | ||
|
|
59799f551c | ||
|
|
85d1e783b0 | ||
|
|
bf16f7894b | ||
|
|
0dc0174b26 | ||
|
|
5f8690dbda | ||
|
|
5f206fb658 | ||
|
|
d6add3f9b4 | ||
|
|
c25368cbe1 | ||
|
|
52ef89c559 | ||
|
|
541e1ac2a3 | ||
|
|
2922affa02 | ||
|
|
ab0d56dec9 | ||
|
|
b095b9c04c | ||
|
|
feeee3912a | ||
|
|
29e2c6ed9c | ||
|
|
454b2f76e7 | ||
|
|
21c1bbc118 | ||
|
|
ea8bef2029 | ||
|
|
432d14d9df | ||
|
|
b7b8e141b1 | ||
|
|
72544cc06d | ||
|
|
81a7d04239 | ||
|
|
fc4b9de02c | ||
|
|
9729e05f86 | ||
|
|
3f920048cb | ||
|
|
d00e73f110 | ||
|
|
87169a3fc7 | ||
|
|
6e84489ca3 | ||
|
|
29aed4b42f | ||
|
|
805ac7c17a | ||
|
|
ec53dfbb40 | ||
|
|
1f44482ad0 | ||
|
|
950e35317e | ||
|
|
6dbb841e22 | ||
|
|
d89aae5b5c | ||
|
|
11e3e85e9d | ||
|
|
99aae0bf02 | ||
|
|
22693c1dcc | ||
|
|
02ca9e43fa | ||
|
|
6afd85df4b | ||
|
|
3b9ca71fc4 | ||
|
|
93b19a7e72 | ||
|
|
c2451b85e7 | ||
|
|
ae88c12e07 | ||
|
|
e7a8e0a3db | ||
|
|
56742d95da | ||
|
|
60e7471cea | ||
|
|
7edd75021b | ||
|
|
a787d60add | ||
|
|
a3bccc881b | ||
|
|
74409dc32b | ||
|
|
ac63b10aa8 | ||
|
|
c306879a31 | ||
|
|
ac4649ba7d | ||
|
|
63af29284b | ||
|
|
b79e4a7c3b | ||
|
|
6fe25c757c | ||
|
|
9cb14cc41a | ||
|
|
201ef3a9c8 | ||
|
|
9e416e9ff5 | ||
|
|
83c47df980 | ||
|
|
7fe505d673 | ||
|
|
9d7dcde1e2 | ||
|
|
16fb45bb2a | ||
|
|
87a2e27fcc | ||
|
|
ad6169201a | ||
|
|
09bbb0f430 | ||
|
|
be815db5e4 | ||
|
|
31a32c084b | ||
|
|
f6f6acdb2d | ||
|
|
4799cb086f | ||
|
|
6e4f2bea29 | ||
|
|
c8150ab017 | ||
|
|
637df1d289 | ||
|
|
cf1eac8521 | ||
|
|
296440579a | ||
|
|
03fef16748 | ||
|
|
e8d27e7212 | ||
|
|
fc0b506253 | ||
|
|
5224dfb50d | ||
|
|
b33df5fa36 | ||
|
|
5ae89b3a27 | ||
|
|
2ed8de0e20 | ||
|
|
155e7dd438 | ||
|
|
8249e8a7f6 | ||
|
|
2ec66214e1 | ||
|
|
c199f7e940 | ||
|
|
b9d1813301 | ||
|
|
362917f52e | ||
|
|
0607c3a749 | ||
|
|
c073125b3b | ||
|
|
86c79e750c | ||
|
|
43cca06460 | ||
|
|
b88d3e8ee7 | ||
|
|
97564dfc13 | ||
|
|
688624ca6b | ||
|
|
c529d09e77 | ||
|
|
0c5cfcea2a | ||
|
|
c24c3ba873 | ||
|
|
8110aab257 | ||
|
|
d34e9b006c | ||
|
|
85a522f725 | ||
|
|
5be232ff8c | ||
|
|
eb6fb3c73b | ||
|
|
52533c354d | ||
|
|
7ae08ed219 | ||
|
|
25a49f80c3 | ||
|
|
6b8c897e37 | ||
|
|
a459cd9fd6 | ||
|
|
ca71f9b8de | ||
|
|
a5ff31428b | ||
|
|
f49197243d | ||
|
|
904a773ade | ||
|
|
ef248a1824 | ||
|
|
4ebb96fbbc | ||
|
|
168e805d0c | ||
|
|
c678d2e3d4 | ||
|
|
8c91ff22db | ||
|
|
2141be7672 | ||
|
|
1be9edc272 | ||
|
|
bdaff31117 | ||
|
|
e30ebaf8ac | ||
|
|
59414834ec | ||
|
|
18578b3030 | ||
|
|
b50d2a6fdb | ||
|
|
d9962c54d6 | ||
|
|
7a7d558116 | ||
|
|
325f818e11 | ||
|
|
117e6a1852 | ||
|
|
820ff17f9a | ||
|
|
2c6654b5b2 | ||
|
|
7e591ec0a1 | ||
|
|
59484b2af7 | ||
|
|
39d904e125 | ||
|
|
254288800d | ||
|
|
e0e6473556 | ||
|
|
afe5cab0ad | ||
|
|
ff670c5bd4 | ||
|
|
e0ba057821 | ||
|
|
dd815e80d1 | ||
|
|
252dcc41f8 | ||
|
|
7507cd85c4 | ||
|
|
32cc3e42bc | ||
|
|
fd3cb6133e | ||
|
|
b207d2976b | ||
|
|
1e76bfd7ce | ||
|
|
4ffdc4fe40 | ||
|
|
97e06be2b4 | ||
|
|
92c9f544fe | ||
|
|
c1897f5843 | ||
|
|
a455016361 | ||
|
|
cfdce55770 | ||
|
|
7a0143cedf | ||
|
|
7404eb6b94 | ||
|
|
b6dd6f005a | ||
|
|
4cef25c4cb | ||
|
|
1508177d9a | ||
|
|
5787a16067 | ||
|
|
2d56b6c02b | ||
|
|
8f03b44771 | ||
|
|
e11bcb931e | ||
|
|
f0c94db99e | ||
|
|
8592098036 | ||
|
|
3b9d1a113c | ||
|
|
84009a3ee8 | ||
|
|
3d0183a3bb | ||
|
|
fec51d60e0 | ||
|
|
569cb182a6 | ||
|
|
12236f24e3 | ||
|
|
717a82753c | ||
|
|
c3269ec841 | ||
|
|
b8eca6c04a |
16
.beads/.gitignore
vendored
16
.beads/.gitignore
vendored
@@ -10,6 +10,8 @@ daemon.lock
|
||||
daemon.log
|
||||
daemon.pid
|
||||
bd.sock
|
||||
sync-state.json
|
||||
last-touched
|
||||
|
||||
# Local version tracking (prevents upgrade notification spam after git ops)
|
||||
.local_version
|
||||
@@ -18,6 +20,10 @@ bd.sock
|
||||
db.sqlite
|
||||
bd.db
|
||||
|
||||
# Worktree redirect file (contains relative path to main repo's .beads/)
|
||||
# Must not be committed as paths would be wrong in other clones
|
||||
redirect
|
||||
|
||||
# Merge artifacts (temporary files from 3-way merge)
|
||||
beads.base.jsonl
|
||||
beads.base.meta.json
|
||||
@@ -26,8 +32,8 @@ beads.left.meta.json
|
||||
beads.right.jsonl
|
||||
beads.right.meta.json
|
||||
|
||||
# Keep JSONL exports and config (source of truth for git)
|
||||
!issues.jsonl
|
||||
!interactions.jsonl
|
||||
!metadata.json
|
||||
!config.json
|
||||
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
|
||||
# They would override fork protection in .git/info/exclude, allowing
|
||||
# contributors to accidentally commit upstream issue databases.
|
||||
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
|
||||
# are tracked by git by default since no pattern above ignores them.
|
||||
|
||||
Binary file not shown.
@@ -27,7 +27,7 @@ Observe the current system state to inform triage decisions.
|
||||
**Step 1: Check Deacon state**
|
||||
```bash
|
||||
# Is Deacon session alive?
|
||||
tmux has-session -t gt-deacon 2>/dev/null && echo "alive" || echo "dead"
|
||||
tmux has-session -t hq-deacon 2>/dev/null && echo "alive" || echo "dead"
|
||||
|
||||
# If alive, what's the pane output showing?
|
||||
gt peek deacon --lines 20
|
||||
@@ -125,7 +125,7 @@ gt nudge deacon "Boot check-in: you have pending work"
|
||||
**WAKE**
|
||||
```bash
|
||||
# Send escape to break any tool waiting
|
||||
tmux send-keys -t gt-deacon Escape
|
||||
tmux send-keys -t hq-deacon Escape
|
||||
|
||||
# Brief pause
|
||||
sleep 1
|
||||
|
||||
@@ -23,7 +23,7 @@ Witnesses detect it and escalate to the Mayor.
|
||||
The Deacon's agent bead last_activity timestamp is updated during each patrol
|
||||
cycle. Witnesses check this timestamp to verify health."""
|
||||
formula = "mol-deacon-patrol"
|
||||
version = 4
|
||||
version = 6
|
||||
|
||||
[[steps]]
|
||||
id = "inbox-check"
|
||||
@@ -148,6 +148,49 @@ bd gate list --json
|
||||
After closing a gate, the Waiters field contains mail addresses to notify.
|
||||
Send a brief notification to each waiter that the gate has cleared."""
|
||||
|
||||
[[steps]]
|
||||
id = "dispatch-gated-molecules"
|
||||
title = "Dispatch molecules with resolved gates"
|
||||
needs = ["gate-evaluation"]
|
||||
description = """
|
||||
Find molecules blocked on gates that have now closed and dispatch them.
|
||||
|
||||
This completes the async resume cycle without explicit waiter tracking.
|
||||
The molecule state IS the waiter - patrol discovers reality each cycle.
|
||||
|
||||
**Step 1: Find gate-ready molecules**
|
||||
```bash
|
||||
bd mol ready --gated --json
|
||||
```
|
||||
|
||||
This returns molecules where:
|
||||
- Status is in_progress
|
||||
- Current step has a gate dependency
|
||||
- The gate bead is now closed
|
||||
- No polecat currently has it hooked
|
||||
|
||||
**Step 2: For each ready molecule, dispatch to the appropriate rig**
|
||||
```bash
|
||||
# Determine target rig from molecule metadata
|
||||
bd mol show <mol-id> --json
|
||||
# Look for rig field or infer from prefix
|
||||
|
||||
# Dispatch to that rig's polecat pool
|
||||
gt sling <mol-id> <rig>/polecats
|
||||
```
|
||||
|
||||
**Step 3: Log dispatch**
|
||||
Note which molecules were dispatched for observability:
|
||||
```bash
|
||||
# Molecule <mol-id> dispatched to <rig>/polecats (gate <gate-id> cleared)
|
||||
```
|
||||
|
||||
**If no gate-ready molecules:**
|
||||
Skip - nothing to dispatch. Gates haven't closed yet or molecules
|
||||
already have active polecats working on them.
|
||||
|
||||
**Exit criteria:** All gate-ready molecules dispatched to polecats."""
|
||||
|
||||
[[steps]]
|
||||
id = "check-convoy-completion"
|
||||
title = "Check convoy completion"
|
||||
@@ -258,7 +301,7 @@ Keep notifications brief and actionable. The recipient can run bd show for detai
|
||||
[[steps]]
|
||||
id = "health-scan"
|
||||
title = "Check Witness and Refinery health"
|
||||
needs = ["trigger-pending-spawns", "gate-evaluation", "fire-notifications"]
|
||||
needs = ["trigger-pending-spawns", "dispatch-gated-molecules", "fire-notifications"]
|
||||
description = """
|
||||
Check Witness and Refinery health for each rig.
|
||||
|
||||
@@ -342,14 +385,21 @@ Reset unresponsive_cycles to 0 when component responds normally."""
|
||||
|
||||
[[steps]]
|
||||
id = "zombie-scan"
|
||||
title = "Backup check for zombie polecats"
|
||||
title = "Detect zombie polecats (NO KILL AUTHORITY)"
|
||||
needs = ["health-scan"]
|
||||
description = """
|
||||
Defense-in-depth check for zombie polecats that Witness should have cleaned.
|
||||
Defense-in-depth DETECTION of zombie polecats that Witness should have cleaned.
|
||||
|
||||
**⚠️ CRITICAL: The Deacon has NO kill authority.**
|
||||
|
||||
These are workers with context, mid-task progress, unsaved state. Every kill
|
||||
destroys work. File the warrant and let Boot handle interrogation and execution.
|
||||
You do NOT have kill authority.
|
||||
|
||||
**Why this exists:**
|
||||
The Witness is responsible for nuking polecats after they complete work (via POLECAT_DONE).
|
||||
This step provides backup detection in case the Witness fails to clean up.
|
||||
The Witness is responsible for cleaning up polecats after they complete work.
|
||||
This step provides backup DETECTION in case the Witness fails to clean up.
|
||||
Detection only - Boot handles termination.
|
||||
|
||||
**Zombie criteria:**
|
||||
- State: idle or done (no active work assigned)
|
||||
@@ -357,26 +407,34 @@ This step provides backup detection in case the Witness fails to clean up.
|
||||
- No hooked work (nothing pending for this polecat)
|
||||
- Last activity: older than 10 minutes
|
||||
|
||||
**Run the zombie scan:**
|
||||
**Run the zombie scan (DRY RUN ONLY):**
|
||||
```bash
|
||||
gt deacon zombie-scan --dry-run
|
||||
```
|
||||
|
||||
**NEVER run:**
|
||||
- `gt deacon zombie-scan` (without --dry-run)
|
||||
- `tmux kill-session`
|
||||
- `gt polecat nuke`
|
||||
- Any command that terminates a session
|
||||
|
||||
**If zombies detected:**
|
||||
1. Review the output to confirm they are truly abandoned
|
||||
2. Run without --dry-run to nuke them:
|
||||
2. File a death warrant for each detected zombie:
|
||||
```bash
|
||||
gt deacon zombie-scan
|
||||
gt warrant file <polecat> --reason "Zombie detected: no session, no hook, idle >10m"
|
||||
```
|
||||
3. Boot will handle interrogation and execution
|
||||
4. Notify the Mayor about Witness failure:
|
||||
```bash
|
||||
gt mail send mayor/ -s "Witness cleanup failure" \
|
||||
-m "Filed death warrant for <polecat>. Witness failed to clean up."
|
||||
```
|
||||
3. This will:
|
||||
- Nuke each zombie polecat
|
||||
- Notify the Mayor about Witness failure
|
||||
- Log the cleanup action
|
||||
|
||||
**If no zombies:**
|
||||
No action needed - Witness is doing its job.
|
||||
|
||||
**Note:** This is a backup mechanism. If you frequently find zombies,
|
||||
**Note:** This is a backup mechanism. If you frequently detect zombies,
|
||||
investigate why the Witness isn't cleaning up properly."""
|
||||
|
||||
[[steps]]
|
||||
@@ -505,10 +563,48 @@ Skip dispatch - system is healthy.
|
||||
|
||||
**Exit criteria:** Session GC dispatched to dog (if needed)."""
|
||||
|
||||
[[steps]]
|
||||
id = "costs-digest"
|
||||
title = "Aggregate daily costs"
|
||||
needs = ["session-gc"]
|
||||
description = """
|
||||
**DAILY DIGEST** - Aggregate yesterday's session cost wisps.
|
||||
|
||||
Session costs are recorded as ephemeral wisps (not exported to JSONL) to avoid
|
||||
log-in-database pollution. This step aggregates them into a permanent daily
|
||||
"Cost Report YYYY-MM-DD" bead for audit purposes.
|
||||
|
||||
**Step 1: Check if digest is needed**
|
||||
```bash
|
||||
# Preview yesterday's costs (dry run)
|
||||
gt costs digest --yesterday --dry-run
|
||||
```
|
||||
|
||||
If output shows "No session cost wisps found", skip to Step 3.
|
||||
|
||||
**Step 2: Create the digest**
|
||||
```bash
|
||||
gt costs digest --yesterday
|
||||
```
|
||||
|
||||
This:
|
||||
- Queries all session.ended wisps from yesterday
|
||||
- Creates a single "Cost Report YYYY-MM-DD" bead with aggregated data
|
||||
- Deletes the source wisps
|
||||
|
||||
**Step 3: Verify**
|
||||
The digest appears in `gt costs --week` queries.
|
||||
Daily digests preserve audit trail without per-session pollution.
|
||||
|
||||
**Timing**: Run once per morning patrol cycle. The --yesterday flag ensures
|
||||
we don't try to digest today's incomplete data.
|
||||
|
||||
**Exit criteria:** Yesterday's costs digested (or no wisps to digest)."""
|
||||
|
||||
[[steps]]
|
||||
id = "log-maintenance"
|
||||
title = "Rotate logs and prune state"
|
||||
needs = ["session-gc"]
|
||||
needs = ["costs-digest"]
|
||||
description = """
|
||||
Maintain daemon logs and state files.
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"formula": "mol-gastown-boot",
|
||||
"description": "Mayor bootstraps Gas Town via a verification-gated lifecycle molecule.\n\n## Purpose\nWhen Mayor executes \"boot up gas town\", this proto provides the workflow.\nEach step has action + verification - steps stay open until outcome is confirmed.\n\n## Key Principles\n1. **Verification-gated steps** - Not \"command ran\" but \"outcome confirmed\"\n2. **gt peek for verification** - Capture session output to detect stalls\n3. **gt nudge for recovery** - Reliable message delivery to unstick agents\n4. **Parallel where possible** - Witnesses and refineries can start in parallel\n5. **Ephemeral execution** - Boot is a wisp, squashed to digest after completion\n\n## Execution\n```bash\nbd mol wisp mol-gastown-boot # Create wisp\n```",
|
||||
"version": 1,
|
||||
"steps": [
|
||||
{
|
||||
"id": "ensure-daemon",
|
||||
"title": "Ensure daemon",
|
||||
"description": "Verify the Gas Town daemon is running.\n\n## Action\n```bash\ngt daemon status || gt daemon start\n```\n\n## Verify\n1. Daemon PID file exists: `~/.gt/daemon.pid`\n2. Process is alive: `kill -0 $(cat ~/.gt/daemon.pid)`\n3. Daemon responds: `gt daemon status` returns success\n\n## OnFail\nCannot start daemon. Log error and continue - some commands work without daemon."
|
||||
},
|
||||
{
|
||||
"id": "ensure-deacon",
|
||||
"title": "Ensure deacon",
|
||||
"needs": ["ensure-daemon"],
|
||||
"description": "Start the Deacon and verify patrol mode is active.\n\n## Action\n```bash\ngt deacon start\n```\n\n## Verify\n1. Session exists: `tmux has-session -t gt-deacon 2>/dev/null`\n2. Not stalled: `gt peek deacon/` does NOT show \"> Try\" prompt\n3. Heartbeat fresh: `deacon/heartbeat.json` modified < 2 min ago\n\n## OnStall\n```bash\ngt nudge deacon/ \"Start patrol.\"\nsleep 30\n# Re-verify\n```"
|
||||
},
|
||||
{
|
||||
"id": "ensure-witnesses",
|
||||
"title": "Ensure witnesses",
|
||||
"needs": ["ensure-deacon"],
|
||||
"type": "parallel",
|
||||
"description": "Parallel container: Start all rig witnesses.\n\nChildren execute in parallel. Container completes when all children complete.",
|
||||
"children": [
|
||||
{
|
||||
"id": "ensure-gastown-witness",
|
||||
"title": "Ensure gastown witness",
|
||||
"description": "Start the gastown rig Witness.\n\n## Action\n```bash\ngt witness start gastown\n```\n\n## Verify\n1. Session exists: `tmux has-session -t gastown-witness 2>/dev/null`\n2. Not stalled: `gt peek gastown/witness` does NOT show \"> Try\" prompt\n3. Heartbeat fresh: Last patrol cycle < 5 min ago"
|
||||
},
|
||||
{
|
||||
"id": "ensure-beads-witness",
|
||||
"title": "Ensure beads witness",
|
||||
"description": "Start the beads rig Witness.\n\n## Action\n```bash\ngt witness start beads\n```\n\n## Verify\n1. Session exists: `tmux has-session -t beads-witness 2>/dev/null`\n2. Not stalled: `gt peek beads/witness` does NOT show \"> Try\" prompt\n3. Heartbeat fresh: Last patrol cycle < 5 min ago"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ensure-refineries",
|
||||
"title": "Ensure refineries",
|
||||
"needs": ["ensure-deacon"],
|
||||
"type": "parallel",
|
||||
"description": "Parallel container: Start all rig refineries.\n\nChildren execute in parallel. Container completes when all children complete.",
|
||||
"children": [
|
||||
{
|
||||
"id": "ensure-gastown-refinery",
|
||||
"title": "Ensure gastown refinery",
|
||||
"description": "Start the gastown rig Refinery.\n\n## Action\n```bash\ngt refinery start gastown\n```\n\n## Verify\n1. Session exists: `tmux has-session -t gastown-refinery 2>/dev/null`\n2. Not stalled: `gt peek gastown/refinery` does NOT show \"> Try\" prompt\n3. Queue processing: Refinery can receive merge requests"
|
||||
},
|
||||
{
|
||||
"id": "ensure-beads-refinery",
|
||||
"title": "Ensure beads refinery",
|
||||
"description": "Start the beads rig Refinery.\n\n## Action\n```bash\ngt refinery start beads\n```\n\n## Verify\n1. Session exists: `tmux has-session -t beads-refinery 2>/dev/null`\n2. Not stalled: `gt peek beads/refinery` does NOT show \"> Try\" prompt\n3. Queue processing: Refinery can receive merge requests"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "verify-town-health",
|
||||
"title": "Verify town health",
|
||||
"needs": ["ensure-witnesses", "ensure-refineries"],
|
||||
"description": "Final verification that Gas Town is healthy.\n\n## Action\n```bash\ngt status\n```\n\n## Verify\n1. Daemon running: Shows daemon status OK\n2. Deacon active: Shows deacon in patrol mode\n3. All witnesses: Each rig witness shows active\n4. All refineries: Each rig refinery shows active\n\n## OnFail\nLog degraded state but consider boot complete. Some agents may need manual recovery.\nRun `gt doctor` for detailed diagnostics."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -48,7 +48,7 @@ gt deacon start
|
||||
```
|
||||
|
||||
## Verify
|
||||
1. Session exists: `tmux has-session -t gt-deacon 2>/dev/null`
|
||||
1. Session exists: `tmux has-session -t hq-deacon 2>/dev/null`
|
||||
2. Not stalled: `gt peek deacon/` does NOT show \"> Try\" prompt
|
||||
3. Heartbeat fresh: `deacon/heartbeat.json` modified < 2 min ago
|
||||
|
||||
|
||||
519
.beads/formulas/mol-shutdown-dance.formula.toml
Normal file
519
.beads/formulas/mol-shutdown-dance.formula.toml
Normal file
@@ -0,0 +1,519 @@
|
||||
description = """
|
||||
Death warrant execution state machine for Dogs.
|
||||
|
||||
Dogs execute this molecule to process death warrants. Each Dog is a lightweight
|
||||
goroutine (NOT a Claude session) that runs the interrogation state machine.
|
||||
|
||||
## Architecture Context
|
||||
|
||||
Dogs are lightweight workers in Boot's pool (see dog-pool-architecture.md):
|
||||
- Fixed pool of 5 goroutines (configurable via GT_DOG_POOL_SIZE)
|
||||
- State persisted to ~/gt/deacon/dogs/active/<id>.json
|
||||
- Recovery on Boot restart via orphan state files
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────────────────────────┐ │
|
||||
│ INTERROGATING │ │
|
||||
│ │ │
|
||||
│ 1. Send health check │ │
|
||||
│ 2. Open timeout gate │ │
|
||||
└───────────┬───────────────┘ │
|
||||
│ │
|
||||
│ gate closes (timeout or response) │
|
||||
▼ │
|
||||
┌───────────────────────────┐ │
|
||||
│ EVALUATING │ │
|
||||
│ │ │
|
||||
│ Check tmux output for │ │
|
||||
│ ALIVE keyword │ │
|
||||
└───────────┬───────────────┘ │
|
||||
│ │
|
||||
┌───────┴───────┐ │
|
||||
│ │ │
|
||||
▼ ▼ │
|
||||
[ALIVE found] [No ALIVE] │
|
||||
│ │ │
|
||||
│ │ attempt < 3? │
|
||||
│ ├──────────────────────────────────→─┘
|
||||
│ │ yes: attempt++, longer timeout
|
||||
│ │
|
||||
│ │ no: attempt == 3
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────────┐
|
||||
│ PARDONED│ │ EXECUTING │
|
||||
│ │ │ │
|
||||
│ Cancel │ │ Kill tmux │
|
||||
│ warrant │ │ session │
|
||||
└────┬────┘ └──────┬──────┘
|
||||
│ │
|
||||
└────────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ EPITAPH │
|
||||
│ │
|
||||
│ Log outcome │
|
||||
│ Release dog │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
## Timeout Gates
|
||||
|
||||
| Attempt | Timeout | Cumulative Wait |
|
||||
|---------|---------|-----------------|
|
||||
| 1 | 60s | 60s |
|
||||
| 2 | 120s | 180s (3 min) |
|
||||
| 3 | 240s | 420s (7 min) |
|
||||
|
||||
Timeout gates work like this:
|
||||
- Gate opens when interrogation message is sent
|
||||
- Gate closes when EITHER:
|
||||
a) Timeout expires (proceed to evaluate)
|
||||
b) Response detected (early close, proceed to evaluate)
|
||||
- The gate state determines the evaluation outcome
|
||||
|
||||
## Interrogation Message Format
|
||||
|
||||
```
|
||||
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within {timeout}s or face termination.
|
||||
Warrant reason: {reason}
|
||||
Filed by: {requester}
|
||||
Attempt: {attempt}/3
|
||||
```
|
||||
|
||||
## Response Detection
|
||||
|
||||
The Dog checks tmux output for:
|
||||
1. The ALIVE keyword (explicit response)
|
||||
2. Any Claude output after the health check (implicit activity)
|
||||
|
||||
```go
|
||||
func (d *Dog) CheckForResponse() bool {
|
||||
output := tmux.CapturePane(d.Warrant.Target, 50) // Last 50 lines
|
||||
return strings.Contains(output, "ALIVE")
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Source | Description |
|
||||
|-------------|-------------|-----------------------------------------------|
|
||||
| warrant_id | hook_bead | Bead ID of the death warrant |
|
||||
| target | warrant | Session name to interrogate |
|
||||
| reason | warrant | Why warrant was issued |
|
||||
| requester | warrant | Who filed the warrant (e.g., deacon, witness) |
|
||||
|
||||
## Integration
|
||||
|
||||
Dogs are NOT Claude sessions. This molecule is:
|
||||
1. A specification document (defines the state machine)
|
||||
2. A reference for Go implementation in internal/shutdown/
|
||||
3. A template for creating warrant-tracking beads
|
||||
|
||||
The Go implementation follows this spec exactly."""
|
||||
formula = "mol-shutdown-dance"
|
||||
version = 1
|
||||
|
||||
[squash]
|
||||
trigger = "on_complete"
|
||||
template_type = "operational"
|
||||
include_metrics = true
|
||||
|
||||
# ============================================================================
|
||||
# STEP 1: WARRANT_RECEIVED
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "warrant-received"
|
||||
title = "Receive and validate death warrant"
|
||||
description = """
|
||||
Entry point when Dog is allocated from pool.
|
||||
|
||||
**1. Read warrant from allocation:**
|
||||
The Dog receives a Warrant struct containing:
|
||||
- ID: Bead ID of the warrant
|
||||
- Target: Session name (e.g., "gt-gastown-Toast")
|
||||
- Reason: Why termination requested
|
||||
- Requester: Who filed (deacon, witness, mayor)
|
||||
- FiledAt: Timestamp
|
||||
|
||||
**2. Validate target exists:**
|
||||
```bash
|
||||
tmux has-session -t {target} 2>/dev/null
|
||||
```
|
||||
|
||||
If target doesn't exist:
|
||||
- Warrant is stale (already dead)
|
||||
- Skip to EPITAPH with outcome=already_dead
|
||||
|
||||
**3. Initialize state file:**
|
||||
Write initial state to ~/gt/deacon/dogs/active/{dog-id}.json
|
||||
|
||||
**4. Set initial attempt counter:**
|
||||
attempt = 1
|
||||
|
||||
**Exit criteria:** Warrant validated, target confirmed alive, state initialized."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 2: INTERROGATION_1 (60s timeout)
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "interrogation-1"
|
||||
title = "First interrogation (60s timeout)"
|
||||
needs = ["warrant-received"]
|
||||
description = """
|
||||
First attempt to contact the session.
|
||||
|
||||
**1. Compose health check message:**
|
||||
```
|
||||
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within 60s or face termination.
|
||||
Warrant reason: {reason}
|
||||
Filed by: {requester}
|
||||
Attempt: 1/3
|
||||
```
|
||||
|
||||
**2. Send via tmux:**
|
||||
```bash
|
||||
tmux send-keys -t {target} "{message}" Enter
|
||||
```
|
||||
|
||||
**3. Open timeout gate:**
|
||||
Gate configuration:
|
||||
- Type: timer
|
||||
- Timeout: 60 seconds
|
||||
- Close conditions:
|
||||
a) Timer expires
|
||||
b) ALIVE keyword detected in output
|
||||
|
||||
**4. Wait for gate to close:**
|
||||
The Dog waits (select on timer channel or early close signal).
|
||||
|
||||
**5. Record interrogation timestamp:**
|
||||
Update state file with last_message_at.
|
||||
|
||||
**Exit criteria:** Message sent, waiting for gate to close."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 3: EVALUATE_1
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "evaluate-1"
|
||||
title = "Evaluate first interrogation response"
|
||||
needs = ["interrogation-1"]
|
||||
description = """
|
||||
Check if session responded to first interrogation.
|
||||
|
||||
**1. Capture tmux output:**
|
||||
```bash
|
||||
tmux capture-pane -t {target} -p | tail -50
|
||||
```
|
||||
|
||||
**2. Check for ALIVE keyword:**
|
||||
```go
|
||||
if strings.Contains(output, "ALIVE") {
|
||||
return PARDONED
|
||||
}
|
||||
```
|
||||
|
||||
**3. Decision:**
|
||||
- ALIVE found → Proceed to PARDON
|
||||
- No ALIVE → Proceed to INTERROGATION_2
|
||||
|
||||
**Exit criteria:** Response evaluated, next step determined."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 4: INTERROGATION_2 (120s timeout)
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "interrogation-2"
|
||||
title = "Second interrogation (120s timeout)"
|
||||
needs = ["evaluate-1"]
|
||||
gate = { type = "conditional", condition = "no_response_1" }
|
||||
description = """
|
||||
Second attempt with longer timeout.
|
||||
|
||||
Only executed if evaluate-1 found no response.
|
||||
|
||||
**1. Increment attempt:**
|
||||
attempt = 2
|
||||
|
||||
**2. Compose health check message:**
|
||||
```
|
||||
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within 120s or face termination.
|
||||
Warrant reason: {reason}
|
||||
Filed by: {requester}
|
||||
Attempt: 2/3
|
||||
```
|
||||
|
||||
**3. Send via tmux:**
|
||||
```bash
|
||||
tmux send-keys -t {target} "{message}" Enter
|
||||
```
|
||||
|
||||
**4. Open timeout gate:**
|
||||
- Type: timer
|
||||
- Timeout: 120 seconds
|
||||
|
||||
**5. Wait for gate to close.**
|
||||
|
||||
**Exit criteria:** Second message sent, waiting for gate."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 5: EVALUATE_2
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "evaluate-2"
|
||||
title = "Evaluate second interrogation response"
|
||||
needs = ["interrogation-2"]
|
||||
description = """
|
||||
Check if session responded to second interrogation.
|
||||
|
||||
**1. Capture tmux output:**
|
||||
```bash
|
||||
tmux capture-pane -t {target} -p | tail -50
|
||||
```
|
||||
|
||||
**2. Check for ALIVE keyword.**
|
||||
|
||||
**3. Decision:**
|
||||
- ALIVE found → Proceed to PARDON
|
||||
- No ALIVE → Proceed to INTERROGATION_3
|
||||
|
||||
**Exit criteria:** Response evaluated, next step determined."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 6: INTERROGATION_3 (240s timeout)
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "interrogation-3"
|
||||
title = "Final interrogation (240s timeout)"
|
||||
needs = ["evaluate-2"]
|
||||
gate = { type = "conditional", condition = "no_response_2" }
|
||||
description = """
|
||||
Final attempt before execution.
|
||||
|
||||
Only executed if evaluate-2 found no response.
|
||||
|
||||
**1. Increment attempt:**
|
||||
attempt = 3
|
||||
|
||||
**2. Compose health check message:**
|
||||
```
|
||||
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within 240s or face termination.
|
||||
Warrant reason: {reason}
|
||||
Filed by: {requester}
|
||||
Attempt: 3/3
|
||||
```
|
||||
|
||||
**3. Send via tmux:**
|
||||
```bash
|
||||
tmux send-keys -t {target} "{message}" Enter
|
||||
```
|
||||
|
||||
**4. Open timeout gate:**
|
||||
- Type: timer
|
||||
- Timeout: 240 seconds
|
||||
- This is the FINAL chance
|
||||
|
||||
**5. Wait for gate to close.**
|
||||
|
||||
**Exit criteria:** Final message sent, waiting for gate."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 7: EVALUATE_3
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "evaluate-3"
|
||||
title = "Evaluate final interrogation response"
|
||||
needs = ["interrogation-3"]
|
||||
description = """
|
||||
Final evaluation before execution.
|
||||
|
||||
**1. Capture tmux output:**
|
||||
```bash
|
||||
tmux capture-pane -t {target} -p | tail -50
|
||||
```
|
||||
|
||||
**2. Check for ALIVE keyword.**
|
||||
|
||||
**3. Decision:**
|
||||
- ALIVE found → Proceed to PARDON
|
||||
- No ALIVE → Proceed to EXECUTE
|
||||
|
||||
**Exit criteria:** Final decision made."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 8: PARDON (success path)
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "pardon"
|
||||
title = "Pardon session - cancel warrant"
|
||||
needs = ["evaluate-1", "evaluate-2", "evaluate-3"]
|
||||
gate = { type = "conditional", condition = "alive_detected" }
|
||||
description = """
|
||||
Session responded - cancel the death warrant.
|
||||
|
||||
**1. Update state:**
|
||||
state = PARDONED
|
||||
|
||||
**2. Record pardon details:**
|
||||
```json
|
||||
{
|
||||
"outcome": "pardoned",
|
||||
"attempt": {attempt},
|
||||
"response_time": "{time_since_last_interrogation}s",
|
||||
"pardoned_at": "{timestamp}"
|
||||
}
|
||||
```
|
||||
|
||||
**3. Cancel warrant bead:**
|
||||
```bash
|
||||
bd close {warrant_id} --reason "Session responded at attempt {attempt}"
|
||||
```
|
||||
|
||||
**4. Notify requester:**
|
||||
```bash
|
||||
gt mail send {requester}/ -s "PARDON: {target}" -m "Death warrant cancelled.
|
||||
Session responded after attempt {attempt}.
|
||||
Warrant: {warrant_id}
|
||||
Response detected: {timestamp}"
|
||||
```
|
||||
|
||||
**Exit criteria:** Warrant cancelled, requester notified."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 9: EXECUTE (termination path)
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "execute"
|
||||
title = "Execute warrant - kill session"
|
||||
needs = ["evaluate-3"]
|
||||
gate = { type = "conditional", condition = "no_response_final" }
|
||||
description = """
|
||||
Session unresponsive after 3 attempts - execute the warrant.
|
||||
|
||||
**1. Update state:**
|
||||
state = EXECUTING
|
||||
|
||||
**2. Kill the tmux session:**
|
||||
```bash
|
||||
tmux kill-session -t {target}
|
||||
```
|
||||
|
||||
**3. Verify session is dead:**
|
||||
```bash
|
||||
tmux has-session -t {target} 2>/dev/null
|
||||
# Should fail (session gone)
|
||||
```
|
||||
|
||||
**4. If session still exists (kill failed):**
|
||||
- Force kill with tmux kill-server if isolated
|
||||
- Or escalate to Boot for manual intervention
|
||||
|
||||
**5. Record execution details:**
|
||||
```json
|
||||
{
|
||||
"outcome": "executed",
|
||||
"attempts": 3,
|
||||
"total_wait": "420s",
|
||||
"executed_at": "{timestamp}"
|
||||
}
|
||||
```
|
||||
|
||||
**Exit criteria:** Session terminated."""
|
||||
|
||||
# ============================================================================
|
||||
# STEP 10: EPITAPH (completion)
|
||||
# ============================================================================
|
||||
[[steps]]
|
||||
id = "epitaph"
|
||||
title = "Log cause of death and close warrant"
|
||||
needs = ["pardon", "execute"]
|
||||
description = """
|
||||
Final step - create audit record and release Dog back to pool.
|
||||
|
||||
**1. Compose epitaph based on outcome:**
|
||||
|
||||
For PARDONED:
|
||||
```
|
||||
EPITAPH: {target}
|
||||
Verdict: PARDONED
|
||||
Warrant: {warrant_id}
|
||||
Reason: {reason}
|
||||
Filed by: {requester}
|
||||
Response: Attempt {attempt}, after {wait_time}s
|
||||
Pardoned at: {timestamp}
|
||||
```
|
||||
|
||||
For EXECUTED:
|
||||
```
|
||||
EPITAPH: {target}
|
||||
Verdict: EXECUTED
|
||||
Warrant: {warrant_id}
|
||||
Reason: {reason}
|
||||
Filed by: {requester}
|
||||
Attempts: 3 (60s + 120s + 240s = 420s total)
|
||||
Executed at: {timestamp}
|
||||
```
|
||||
|
||||
For ALREADY_DEAD (target gone before interrogation):
|
||||
```
|
||||
EPITAPH: {target}
|
||||
Verdict: ALREADY_DEAD
|
||||
Warrant: {warrant_id}
|
||||
Reason: {reason}
|
||||
Filed by: {requester}
|
||||
Note: Target session not found at warrant processing
|
||||
```
|
||||
|
||||
**2. Close warrant bead:**
|
||||
```bash
|
||||
bd close {warrant_id} --reason "{epitaph_summary}"
|
||||
```
|
||||
|
||||
**3. Move state file to completed:**
|
||||
```bash
|
||||
mv ~/gt/deacon/dogs/active/{dog-id}.json ~/gt/deacon/dogs/completed/
|
||||
```
|
||||
|
||||
**4. Report to Boot:**
|
||||
Write completion file: ~/gt/deacon/dogs/active/{dog-id}.done
|
||||
```json
|
||||
{
|
||||
"dog_id": "{dog-id}",
|
||||
"warrant_id": "{warrant_id}",
|
||||
"target": "{target}",
|
||||
"outcome": "{pardoned|executed|already_dead}",
|
||||
"duration": "{total_duration}s"
|
||||
}
|
||||
```
|
||||
|
||||
**5. Release Dog to pool:**
|
||||
Dog resets state and returns to idle channel.
|
||||
|
||||
**Exit criteria:** Warrant closed, Dog released, audit complete."""
|
||||
|
||||
# ============================================================================
|
||||
# VARIABLES
|
||||
# ============================================================================
|
||||
[vars]
|
||||
[vars.warrant_id]
|
||||
description = "Bead ID of the death warrant being processed"
|
||||
required = true
|
||||
|
||||
[vars.target]
|
||||
description = "Session name to interrogate (e.g., gt-gastown-Toast)"
|
||||
required = true
|
||||
|
||||
[vars.reason]
|
||||
description = "Why the warrant was issued"
|
||||
required = true
|
||||
|
||||
[vars.requester]
|
||||
description = "Who filed the warrant (deacon, witness, mayor)"
|
||||
required = true
|
||||
default = "deacon"
|
||||
@@ -56,7 +56,7 @@ needs = ['patrol-cleanup']
|
||||
title = 'Check own context limit'
|
||||
|
||||
[[steps]]
|
||||
description = "End of patrol cycle decision.\n\n**If context LOW**:\n- Sleep briefly to avoid tight loop (30-60 seconds)\n- Return to inbox-check step\n- Continue patrolling\n\n**If context HIGH**:\n- Write handoff mail to self with any notable observations:\n```bash\ngt handoff -s \"Witness patrol handoff\" -m \"<observations>\"\n```\n- Exit cleanly (daemon respawns fresh Witness)\n\nThe daemon ensures Witness is always running."
|
||||
description = "End of patrol cycle decision.\n\n**If context LOW** (can continue patrolling):\n1. Generate a brief summary of this patrol cycle\n2. Squash the current wisp:\n```bash\nbd mol squash <mol-id> --summary \"<patrol-summary>\"\n```\n3. Create a new patrol wisp:\n```bash\nbd mol wisp mol-witness-patrol\n```\n4. Continue executing from the inbox-check step of the new wisp\n\n**If context HIGH** (approaching limit):\n1. Write handoff mail with notable observations:\n```bash\ngt handoff -s \"Witness patrol handoff\" -m \"<observations>\"\n```\n2. Exit cleanly - the daemon will respawn a fresh Witness session\n\n**IMPORTANT**: You must either create a new wisp (context LOW) or exit (context HIGH).\nNever leave the session idle without work on your hook."
|
||||
id = 'loop-or-exit'
|
||||
needs = ['context-check']
|
||||
title = 'Loop or exit for respawn'
|
||||
|
||||
7922
.beads/issues.jsonl
7922
.beads/issues.jsonl
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
gt-gastown-polecat-warboy
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"database": "beads.db",
|
||||
"jsonl_export": "issues.jsonl"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-09eim",
|
||||
"branch": "polecat/toast-1767088545235",
|
||||
"target": "main",
|
||||
"source_issue": "toast-1767088545235",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: toast-1767088545235",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T02:01:08.537717-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-0a0vr",
|
||||
"branch": "polecat/furiosa-1767087671424",
|
||||
"target": "main",
|
||||
"source_issue": "furiosa-1767087671424",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: furiosa-1767087671424",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T01:53:07.730594-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-0h89l",
|
||||
"branch": "polecat/furiosa-1767084006859",
|
||||
"target": "main",
|
||||
"source_issue": "furiosa-1767084006859",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: furiosa-1767084006859",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:47:11.803227-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-215tk",
|
||||
"branch": "polecat/warboy-1767106060799",
|
||||
"target": "main",
|
||||
"source_issue": "warboy-1767106060799",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: warboy-1767106060799",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:40:55.503776-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-2c4o0",
|
||||
"branch": "polecat/dementus-1767087772272",
|
||||
"target": "main",
|
||||
"source_issue": "dementus-1767087772272",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: dementus-1767087772272",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T02:06:35.286507-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-2hirc",
|
||||
"branch": "polecat/capable-1767084028536",
|
||||
"target": "main",
|
||||
"source_issue": "capable-1767084028536",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: capable-1767084028536",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T01:03:19.471054-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-2puev",
|
||||
"branch": "polecat/dementus-1767081113622",
|
||||
"target": "main",
|
||||
"source_issue": "dementus-1767081113622",
|
||||
"worker": "dementus-1767081113622",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: dementus-1767081113622",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:05:15.468509-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-2tspu",
|
||||
"branch": "polecat/furiosa-dogs",
|
||||
"target": "main",
|
||||
"source_issue": "furiosa-dogs",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: furiosa-dogs",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:42:17.458391-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-3gepq",
|
||||
"branch": "polecat/toast-1767081120579",
|
||||
"target": "main",
|
||||
"source_issue": "toast-1767081120579",
|
||||
"worker": "toast-1767081120579",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: toast-1767081120579",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:05:15.468721-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-4a9y4",
|
||||
"branch": "polecat/slit-1767138831931",
|
||||
"target": "main",
|
||||
"source_issue": "slit-1767138831931",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: slit-1767138831931",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:15:39.347085-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-4nobz",
|
||||
"branch": "polecat/capable-1767140263101",
|
||||
"target": "main",
|
||||
"source_issue": "capable-1767140263101",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: capable-1767140263101",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:26:48.128098-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-4q7wh",
|
||||
"branch": "polecat/nux-1767141948667",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767141948667",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767141948667",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:51:43.00565-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-5ggcs",
|
||||
"branch": "polecat/slit-1767082302712",
|
||||
"target": "main",
|
||||
"source_issue": "slit-1767082302712",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: slit-1767082302712",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:18:54.19263-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-643ie",
|
||||
"branch": "polecat/slit-1767141951901",
|
||||
"target": "main",
|
||||
"source_issue": "slit-1767141951901",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: slit-1767141951901",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:56:13.685311-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-6l7h1",
|
||||
"branch": "polecat/morsov-dogs",
|
||||
"target": "main",
|
||||
"source_issue": "morsov-dogs",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: morsov-dogs",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:41:30.109352-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-804je",
|
||||
"branch": "polecat/dementus-1767146229184",
|
||||
"target": "main",
|
||||
"source_issue": "dementus-1767146229184",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: dementus-1767146229184",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T18:01:50.012819-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-860md",
|
||||
"branch": "polecat/capable-1767146233256",
|
||||
"target": "main",
|
||||
"source_issue": "capable-1767146233256",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: capable-1767146233256",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T18:03:37.998767-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-9g6md",
|
||||
"branch": "polecat/nux-1767082300311",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767082300311",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767082300311",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:18:32.959791-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-9hfky",
|
||||
"branch": "polecat/toast-1767140378007",
|
||||
"target": "main",
|
||||
"source_issue": "toast-1767140378007",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: toast-1767140378007",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:28:18.459411-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-aa1jz",
|
||||
"branch": "polecat/keeper-dogs",
|
||||
"target": "main",
|
||||
"source_issue": "keeper-dogs",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: keeper-dogs",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:36:28.247719-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-apft7",
|
||||
"branch": "polecat/capable-1767084028536",
|
||||
"target": "main",
|
||||
"source_issue": "capable-1767084028536",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: capable-1767084028536",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T01:04:07.334023-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-bnfus",
|
||||
"branch": "polecat/rictus-1767138835254",
|
||||
"target": "main",
|
||||
"source_issue": "rictus-1767138835254",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: rictus-1767138835254",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:27:17.228997-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-bx4ki",
|
||||
"branch": "polecat/keeper-dogs",
|
||||
"target": "main",
|
||||
"source_issue": "keeper-dogs",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: keeper-dogs",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:53:39.674941-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-c7qtp",
|
||||
"branch": "polecat/rictus-1767084016819",
|
||||
"target": "main",
|
||||
"source_issue": "rictus-1767084016819",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: rictus-1767084016819",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:49:18.337909-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-cfpd8",
|
||||
"branch": "polecat/capable-mq-events",
|
||||
"target": "main",
|
||||
"source_issue": "capable-mq",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: capable-mq",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T01:14:13.648371-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-cpxxv",
|
||||
"branch": "polecat/organic-1767106082951",
|
||||
"target": "main",
|
||||
"source_issue": "organic-1767106082951",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: organic-1767106082951",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:42:25.228746-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-djv74",
|
||||
"branch": "polecat/nux-1767081106779",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767081106779",
|
||||
"worker": "nux-1767081106779",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767081106779",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:05:15.468625-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-dufx1",
|
||||
"branch": "polecat/capable-1767140263101",
|
||||
"target": "main",
|
||||
"source_issue": "capable-1767140263101",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: capable-1767140263101",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:24:39.547495-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-e0p84",
|
||||
"branch": "polecat/toast-1767081120579",
|
||||
"target": "main",
|
||||
"source_issue": "toast-1767081120579",
|
||||
"worker": "toast-1767081120579",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: toast-1767081120579",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:05:15.468573-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-gdbcb",
|
||||
"branch": "polecat/rictus-1767141956287",
|
||||
"target": "main",
|
||||
"source_issue": "rictus-1767141956287",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: rictus-1767141956287",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:47:36.875216-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-gnuat",
|
||||
"branch": "polecat/dementus-1767081113622",
|
||||
"target": "main",
|
||||
"source_issue": "dementus-1767081113622",
|
||||
"worker": "dementus-1767081113622",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: dementus-1767081113622",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:05:15.468374-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-gres0",
|
||||
"branch": "polecat/nux-1767084010093",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767084010093",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767084010093",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:48:40.079116-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-hrhts",
|
||||
"branch": "polecat/cheedo-1767146245543",
|
||||
"target": "main",
|
||||
"source_issue": "cheedo-1767146245543",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: cheedo-1767146245543",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T18:00:27.283919-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-i6xqu",
|
||||
"branch": "polecat/toast-1767146237529",
|
||||
"target": "main",
|
||||
"source_issue": "toast-1767146237529",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: toast-1767146237529",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T18:03:32.883944-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-i7tmd",
|
||||
"branch": "polecat/rictus-1767084016819",
|
||||
"target": "main",
|
||||
"source_issue": "rictus-1767084016819",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: rictus-1767084016819",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:58:46.110174-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-i9y2a",
|
||||
"branch": "polecat/toast-1767146237529",
|
||||
"target": "main",
|
||||
"source_issue": "toast-1767146237529",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: toast-1767146237529",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T18:04:15.705404-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-iai8v",
|
||||
"branch": "polecat/nux-1767082300311",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767082300311",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767082300311",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:16:15.874394-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-jl4ze",
|
||||
"branch": "polecat/dementus-1767084022436",
|
||||
"target": "main",
|
||||
"source_issue": "dementus-1767084022436",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: dementus-1767084022436",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:49:44.391479-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-jsoiw",
|
||||
"branch": "polecat/dag-1767146241770",
|
||||
"target": "main",
|
||||
"source_issue": "dag-1767146241770",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: dag-1767146241770",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T18:03:10.025552-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-klu0r",
|
||||
"branch": "polecat/nux-1767083432904",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767083432904",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767083432904",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:35:43.911656-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-l2b6v",
|
||||
"branch": "polecat/slit-1767084013378",
|
||||
"target": "main",
|
||||
"source_issue": "slit-1767084013378",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: slit-1767084013378",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:49:46.335483-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-nduix",
|
||||
"branch": "polecat/nux-1767138828269",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767138828269",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767138828269",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:17:54.718789-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-npu0m",
|
||||
"branch": "polecat/imperator-1767106079026",
|
||||
"target": "main",
|
||||
"source_issue": "imperator-1767106079026",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: imperator-1767106079026",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:40:11.954481-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-nq5l9",
|
||||
"branch": "polecat/nux-1767087680976",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767087680976",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767087680976",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T13:43:41.691922-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-nu47q",
|
||||
"branch": "polecat/rictus-1767087768853",
|
||||
"target": "main",
|
||||
"source_issue": "rictus-1767087768853",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: rictus-1767087768853",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T01:54:12.913353-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-pulkh",
|
||||
"branch": "polecat/ace-dogs",
|
||||
"target": "main",
|
||||
"source_issue": "ace-dogs",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: ace-dogs",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:36:01.970507-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-qduud",
|
||||
"branch": "polecat/furiosa-1767084006859",
|
||||
"target": "main",
|
||||
"source_issue": "furiosa-1767084006859",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: furiosa-1767084006859",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:48:06.518381-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-r099o",
|
||||
"branch": "polecat/imperator-1767106079026",
|
||||
"target": "main",
|
||||
"source_issue": "imperator-1767106079026",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: imperator-1767106079026",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:46:40.452899-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-sp1tv",
|
||||
"branch": "polecat/rictus-1767081110235",
|
||||
"target": "main",
|
||||
"source_issue": "rictus-1767081110235",
|
||||
"worker": "rictus-1767081110235",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: rictus-1767081110235",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:05:15.468677-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-svmj8",
|
||||
"branch": "polecat/cheedo-1767088553821",
|
||||
"target": "main",
|
||||
"source_issue": "cheedo-1767088553821",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: cheedo-1767088553821",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:37:17.028645-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-t072g",
|
||||
"branch": "polecat/nux-1767084010093",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767084010093",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767084010093",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:50:06.177433-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-tjy9r",
|
||||
"branch": "polecat/capable-1767074974673",
|
||||
"target": "main",
|
||||
"source_issue": "capable-1767074974673",
|
||||
"worker": "capable-1767074974673",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: capable-1767074974673",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:05:15.468769-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-tpq7i",
|
||||
"branch": "polecat/keeper-1767074342207",
|
||||
"target": "main",
|
||||
"source_issue": "keeper-1767074342207",
|
||||
"worker": "keeper-1767074342207",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: keeper-1767074342207",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:05:15.468817-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-u65t8",
|
||||
"branch": "polecat/valkyrie-1767106008400",
|
||||
"target": "main",
|
||||
"source_issue": "valkyrie-1767106008400",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: valkyrie-1767106008400",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:43:03.505961-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-ug23r",
|
||||
"branch": "polecat/cheedo-1767088553821",
|
||||
"target": "main",
|
||||
"source_issue": "cheedo-1767088553821",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: cheedo-1767088553821",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T02:00:38.571996-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-w4v1o",
|
||||
"branch": "polecat/furiosa-dogs",
|
||||
"target": "main",
|
||||
"source_issue": "furiosa-dogs",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: furiosa-dogs",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T11:01:55.023855-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-x1xf4",
|
||||
"branch": "polecat/slit-1767087730371",
|
||||
"target": "main",
|
||||
"source_issue": "slit-1767087730371",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: slit-1767087730371",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T01:52:04.349503-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-xv6b6",
|
||||
"branch": "polecat/dementus-1767140140908",
|
||||
"target": "main",
|
||||
"source_issue": "dementus-1767140140908",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: dementus-1767140140908",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:23:04.504091-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-yh051",
|
||||
"branch": "polecat/rictus-1767084016819",
|
||||
"target": "main",
|
||||
"source_issue": "rictus-1767084016819",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: rictus-1767084016819",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T00:48:16.329248-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-yjrb7",
|
||||
"branch": "polecat/furiosa-dogs",
|
||||
"target": "main",
|
||||
"source_issue": "furiosa-dogs",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: furiosa-dogs",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T10:52:57.896189-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-yqxcq",
|
||||
"branch": "polecat/furiosa-1767141944421",
|
||||
"target": "main",
|
||||
"source_issue": "furiosa-1767141944421",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: furiosa-1767141944421",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:49:14.139123-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-zet9d",
|
||||
"branch": "polecat/nux-1767087680976",
|
||||
"target": "main",
|
||||
"source_issue": "nux-1767087680976",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: nux-1767087680976",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T01:50:53.298145-08:00"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "gt-zvfnu",
|
||||
"branch": "polecat/furiosa-1767138824776",
|
||||
"target": "main",
|
||||
"source_issue": "furiosa-1767138824776",
|
||||
"worker": "",
|
||||
"rig": "gastown",
|
||||
"title": "Merge: furiosa-1767138824776",
|
||||
"priority": 2,
|
||||
"created_at": "2025-12-30T16:09:55.272069-08:00"
|
||||
}
|
||||
33
.githooks/pre-push
Executable file
33
.githooks/pre-push
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Block PRs by preventing pushes to arbitrary feature branches.
|
||||
# Gas Town agents push to main (crew) or polecat/* branches (polecats).
|
||||
# PRs are for external contributors only.
|
||||
|
||||
# Allowed patterns:
|
||||
# main, beads-sync - Direct work branches
|
||||
# polecat/* - Polecat working branches (Refinery merges these)
|
||||
|
||||
while read local_ref local_sha remote_ref remote_sha; do
|
||||
branch="${remote_ref#refs/heads/}"
|
||||
|
||||
case "$branch" in
|
||||
main|beads-sync|polecat/*)
|
||||
# Allowed branches
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Invalid branch for Gas Town agents."
|
||||
echo ""
|
||||
echo "Blocked push to: $branch"
|
||||
echo ""
|
||||
echo "Allowed branches:"
|
||||
echo " main - Crew workers push here directly"
|
||||
echo " polecat/* - Polecat working branches"
|
||||
echo " beads-sync - Beads synchronization"
|
||||
echo ""
|
||||
echo "Do NOT create PRs. Push to main or let Refinery merge polecat work."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
exit 0
|
||||
51
.github/workflows/block-internal-prs.yml
vendored
Normal file
51
.github/workflows/block-internal-prs.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Block Internal PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
block-internal-prs:
|
||||
name: Block Internal PRs
|
||||
# Only run if PR is from the same repo (not a fork)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close PR and comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.issue.number;
|
||||
const branch = context.payload.pull_request.head.ref;
|
||||
|
||||
const body = [
|
||||
'**Internal PRs are not allowed.**',
|
||||
'',
|
||||
'Gas Town agents push directly to main. PRs are for external contributors only.',
|
||||
'',
|
||||
'To land your changes:',
|
||||
'```bash',
|
||||
'git checkout main',
|
||||
'git merge ' + branch,
|
||||
'git push origin main',
|
||||
'git push origin --delete ' + branch,
|
||||
'```',
|
||||
'',
|
||||
'See CLAUDE.md: "Crew workers push directly to main. No feature branches. NEVER create PRs."'
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: body
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
core.setFailed('Internal PR blocked. Push directly to main instead.');
|
||||
159
.github/workflows/ci.yml
vendored
159
.github/workflows/ci.yml
vendored
@@ -33,11 +33,43 @@ jobs:
|
||||
fi
|
||||
echo "No .beads/issues.jsonl changes detected"
|
||||
|
||||
# Verify committed formulas allow build without go:generate
|
||||
# This catches issues where go install @latest would fail
|
||||
check-embedded-formulas:
|
||||
name: Check embedded formulas
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Build without go:generate
|
||||
run: |
|
||||
# This must succeed with committed formulas only
|
||||
# If this fails, run: go generate ./... && git add -A && git commit
|
||||
go build -v ./cmd/gt
|
||||
|
||||
- name: Verify formulas are in sync
|
||||
run: |
|
||||
# Regenerate and check for differences
|
||||
go generate ./internal/formula/...
|
||||
if ! git diff --exit-code internal/formula/formulas/; then
|
||||
echo ""
|
||||
echo "ERROR: Committed formulas are out of sync with .beads/formulas/"
|
||||
echo "Run: go generate ./... && git add -A && git commit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
@@ -49,14 +81,125 @@ jobs:
|
||||
git config --global user.name "CI Bot"
|
||||
git config --global user.email "ci@gastown.test"
|
||||
|
||||
- name: Generate embedded files
|
||||
run: go generate ./internal/formula/...
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./cmd/gt
|
||||
|
||||
- name: Test
|
||||
run: go test -v -race -short ./...
|
||||
- name: Test with Coverage
|
||||
run: |
|
||||
go test -race -short -coverprofile=coverage.out ./... 2>&1 | tee test-output.txt
|
||||
|
||||
- name: Upload Coverage Data
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-data
|
||||
path: |
|
||||
coverage.out
|
||||
test-output.txt
|
||||
|
||||
# Separate job to process coverage after ALL tests complete
|
||||
coverage:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, integration]
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Download Coverage Data
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: coverage-data
|
||||
|
||||
- name: Generate Coverage Report
|
||||
run: |
|
||||
# Parse per-package coverage from test output
|
||||
echo "## Code Coverage Report" > coverage-report.md
|
||||
echo "" >> coverage-report.md
|
||||
|
||||
# Get overall coverage
|
||||
TOTAL=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
|
||||
echo "**Overall Coverage: ${TOTAL}**" >> coverage-report.md
|
||||
echo "" >> coverage-report.md
|
||||
|
||||
# Create per-package table
|
||||
echo "| Package | Coverage |" >> coverage-report.md
|
||||
echo "|---------|----------|" >> coverage-report.md
|
||||
|
||||
# Extract package coverage from all test output lines
|
||||
grep -E "github.com/steveyegge/gastown.*coverage:" test-output.txt | \
|
||||
sed 's/.*github.com\/steveyegge\/gastown\///' | \
|
||||
awk '{
|
||||
pkg = $1
|
||||
for (i=2; i<=NF; i++) {
|
||||
if ($i == "coverage:") {
|
||||
cov = $(i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
printf "| %s | %s |\n", pkg, cov
|
||||
}' | sort -u >> coverage-report.md
|
||||
|
||||
echo "" >> coverage-report.md
|
||||
echo "---" >> coverage-report.md
|
||||
echo "_Generated by CI_" >> coverage-report.md
|
||||
|
||||
# Show in logs
|
||||
cat coverage-report.md
|
||||
|
||||
- name: Upload Coverage Report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage-report.md
|
||||
retention-days: 30
|
||||
|
||||
- name: Comment Coverage on PR
|
||||
# Only for internal PRs - fork PRs can't write comments
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const report = fs.readFileSync('coverage-report.md', 'utf8');
|
||||
|
||||
// Find existing coverage comment
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('## Code Coverage Report')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: report
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: report
|
||||
});
|
||||
}
|
||||
|
||||
- name: Coverage Note for Fork PRs
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
run: |
|
||||
echo "::notice::Coverage report uploaded as artifact (fork PRs cannot post comments). Download from Actions tab."
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -69,9 +212,6 @@ jobs:
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Generate embedded files
|
||||
run: go generate ./internal/formula/...
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
@@ -97,9 +237,6 @@ jobs:
|
||||
- name: Install beads (bd)
|
||||
run: go install github.com/steveyegge/beads/cmd/bd@latest
|
||||
|
||||
- name: Generate embedded files
|
||||
run: go generate ./internal/formula/...
|
||||
|
||||
- name: Build gt
|
||||
run: go build -v -o gt ./cmd/gt
|
||||
|
||||
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -17,6 +17,9 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Claude Code local state
|
||||
.claude/
|
||||
|
||||
# Test
|
||||
coverage.out
|
||||
*.test
|
||||
@@ -29,10 +32,20 @@ gt
|
||||
# Runtime state
|
||||
state.json
|
||||
.runtime/
|
||||
|
||||
# Beads runtime state (not tracked)
|
||||
# Formulas ARE tracked for `go install @latest` - see bottom of file
|
||||
.beads/redirect
|
||||
.beads/issues.jsonl
|
||||
.beads/interactions.jsonl
|
||||
.beads/metadata.json
|
||||
.beads/mq/
|
||||
.beads/last-touched
|
||||
.beads/daemon-*.log.gz
|
||||
.beads-wisp/
|
||||
|
||||
# Clone-specific CLAUDE.md (regenerated locally per clone)
|
||||
CLAUDE.md
|
||||
|
||||
# Generated by go:generate from .beads/formulas/
|
||||
internal/formula/formulas/
|
||||
# Embedded formulas are committed so `go install @latest` works
|
||||
# Run `go generate ./...` after modifying .beads/formulas/
|
||||
|
||||
226
CHANGELOG.md
226
CHANGELOG.md
@@ -7,6 +7,232 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.3] - 2026-01-09
|
||||
|
||||
Worker safety release - prevents accidental termination of active agents.
|
||||
|
||||
> **Note**: The Deacon safety improvements are believed to be correct but have not
|
||||
> yet been extensively tested in production. We recommend running with
|
||||
> `gt deacon pause` initially and monitoring behavior before enabling full patrol.
|
||||
> Please report any issues. A 0.3.0 release will follow once these changes are
|
||||
> battle-tested.
|
||||
|
||||
### Critical Safety Improvements
|
||||
|
||||
- **Kill authority removed from Deacon** - Deacon patrol now only detects zombies via `--dry-run`, never kills directly. Death warrants are filed for Boot to handle interrogation/execution. This prevents destruction of worker context, mid-task progress, and unsaved state (#gt-vhaej)
|
||||
- **Bulletproof pause mechanism** - Multi-layer pause for Deacon with file-based state, `gt deacon pause/resume` commands, and guards in `gt prime` and heartbeat (#265)
|
||||
- **Doctor warns instead of killing** - `gt doctor` now warns about stale town-root settings rather than killing sessions (#243)
|
||||
- **Orphan process check informational** - Doctor's orphan process detection is now informational only, not actionable (#272)
|
||||
|
||||
### Added
|
||||
|
||||
- **`gt account switch` command** - Switch between Claude Code accounts with `gt account switch <handle>`. Manages `~/.claude` symlinks and updates default account
|
||||
- **`gt crew list --all`** - Show all crew members across all rigs (#276)
|
||||
- **Rig-level custom agent support** - Configure different agents per-rig (#12)
|
||||
- **Rig identity beads check** - Doctor validates rig identity beads exist
|
||||
- **GT_ROOT env var** - Set for all agent sessions for consistent environment
|
||||
- **New agent presets** - Added Cursor, Auggie (Augment Code), and Sourcegraph AMP as built-in agent presets (#247)
|
||||
- **Context Management docs** - Added to Witness template for better context handling (gt-jjama)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`gt prime --hook` recognized** - Doctor now recognizes `gt prime --hook` as valid session hook config (#14)
|
||||
- **Integration test reliability** - Improved test stability (#13)
|
||||
- **IsClaudeRunning detection** - Now detects 'claude' and version patterns correctly (#273)
|
||||
- **Deacon heartbeat restored** - `ensureDeaconRunning` restored to heartbeat using Manager pattern (#271)
|
||||
- **Deacon session names** - Correct session name references in formulas (#270)
|
||||
- **Hidden directory scanning** - Ignore `.claude` and other dot directories when enumerating polecats (#258, #279)
|
||||
- **SetupRedirect tracked beads** - Works correctly with tracked beads architecture where canonical location is `mayor/rig/.beads`
|
||||
- **Tmux shell ready** - Wait for shell ready before sending keys (#264)
|
||||
- **Gastown prefix derivation** - Correctly derive `gt-` prefix for gastown compound words (gt-m46bb)
|
||||
- **Custom beads types** - Register custom beads types during install (#250)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Refinery Manager pattern** - Replaced `ensureRefinerySession` with `refinery.Manager.Start()` for consistency
|
||||
|
||||
### Removed
|
||||
|
||||
- **Unused formula JSON** - Removed unused JSON formula file (cleanup)
|
||||
|
||||
### Contributors
|
||||
|
||||
Thanks to all contributors for this release:
|
||||
- @julianknutsen - Doctor fixes (#14, #271, #272, #273), formula fixes (#270), GT_ROOT env (#268)
|
||||
- @joshuavial - Hidden directory scanning (#258, #279), crew list --all (#276)
|
||||
|
||||
## [0.2.2] - 2026-01-07
|
||||
|
||||
Rig operational state management, unified agent startup, and extensive stability fixes.
|
||||
|
||||
### Added
|
||||
|
||||
#### Rig Operational State Management
|
||||
- **`gt rig park/unpark` commands** - Level 1 rig control: pause daemon auto-start while preserving sessions
|
||||
- **`gt rig dock/undock` commands** - Level 2 rig control: stop all sessions and prevent auto-start (gt-9gm9n)
|
||||
- **`gt rig config` commands** - Per-rig configuration management (gt-hhmkq)
|
||||
- **Rig identity beads** - Schema and creation for rig identity tracking (gt-zmznh)
|
||||
- **Property layer lookup** - Hierarchical configuration resolution (gt-emh1c)
|
||||
- **Operational state in status** - `gt rig status` shows park/dock state
|
||||
|
||||
#### Agent Configuration & Startup
|
||||
- **`--agent` overrides** - Override agent for start/attach/sling commands
|
||||
- **Unified agent startup** - Manager pattern for consistent agent initialization
|
||||
- **Claude settings installation** - Auto-install during rig and HQ creation
|
||||
- **Runtime-aware tmux checks** - Detect actual agent state from tmux sessions
|
||||
|
||||
#### Status & Monitoring
|
||||
- **`gt status --watch`** - Watch mode with auto-refresh (#231)
|
||||
- **Compact status output** - One-line-per-worker format as new default
|
||||
- **LED status indicators** - Visual indicators for rigs in Mayor tmux status line
|
||||
- **Parked/docked indicators** - Pause emoji (⏸) for inactive rigs in statusline
|
||||
|
||||
#### Beads & Workflow
|
||||
- **Minimum beads version check** - Validates beads CLI compatibility (gt-im3fl)
|
||||
- **ZFC convoy auto-close** - `bd close` triggers convoy completion (gt-3qw5s)
|
||||
- **Stale hooked bead cleanup** - Deacon clears orphaned hooks (gt-2yls3)
|
||||
- **Doctor prefix mismatch detection** - Detect misconfigured rig prefixes (gt-17wdl)
|
||||
- **Unified beads redirect** - Single redirect system for tracked and local beads (#222)
|
||||
- **Route from rig to town beads** - Cross-level bead routing
|
||||
|
||||
#### Infrastructure
|
||||
- **Windows-compatible file locking** - Daemon lock works on Windows
|
||||
- **`--purge` flag for crews** - Full crew obliteration option
|
||||
- **Debug logging for suppressed errors** - Better visibility into startup issues (gt-6d7eh)
|
||||
- **hq- prefix in tmux cycle bindings** - Navigate to Mayor/Deacon sessions
|
||||
- **Wisp config storage layer** - Transient/local settings for ephemeral workflows
|
||||
- **Sparse checkout** - Exclude Claude context files from source repos
|
||||
|
||||
### Changed
|
||||
|
||||
- **Daemon respects rig operational state** - Parked/docked rigs not auto-started
|
||||
- **Agent startup unified** - Manager pattern replaces ad-hoc initialization
|
||||
- **Mayor files moved** - Reorganized into `mayor/` subdirectory
|
||||
- **Refinery merges local branches** - No longer fetches from origin (gt-cio03)
|
||||
- **Polecats start from origin/default-branch** - Consistent recycled state
|
||||
- **Observable states removed** - Discover agent state from tmux, don't track (gt-zecmc)
|
||||
- **mol-town-shutdown v3** - Complete cleanup formula (gt-ux23f)
|
||||
- **Witness delays polecat cleanup** - Wait until MR merges (gt-12hwb)
|
||||
- **Nudge on divergence** - Daemon nudges agents instead of silent accept
|
||||
- **README rewritten** - Comprehensive guides and architecture docs (#226)
|
||||
- **`gt rigs` → `gt rig list`** - Command renamed in templates/docs (#217)
|
||||
|
||||
### Fixed
|
||||
|
||||
#### Doctor & Lifecycle
|
||||
- **`--restart-sessions` flag required** - Doctor won't cycle sessions without explicit flag (gt-j44ri)
|
||||
- **Only cycle patrol roles** - Doctor --fix doesn't restart crew/polecats (hq-qthgye)
|
||||
- **Session-ended events auto-closed** - Prevent accumulation (gt-8tc1v)
|
||||
- **GUPP propulsion nudge** - Added to daemon restartSession
|
||||
|
||||
#### Sling & Beads
|
||||
- **Sling uses bd native routing** - No BEADS_DIR override needed
|
||||
- **Sling parses wisp JSON correctly** - Handle `new_epic_id` field
|
||||
- **Sling resolves rig path** - Cross-rig bead hooking works
|
||||
- **Sling waits for Claude ready** - Don't nudge until session responsive (#146)
|
||||
- **Correct beads database for sling** - Rig-level beads used (gt-n5gga)
|
||||
- **Close hooked beads before clearing** - Proper cleanup order (gt-vwjz6)
|
||||
- **Removed dead sling flags** - `--molecule` and `--quality` cleaned up
|
||||
|
||||
#### Agent Sessions
|
||||
- **Witness kills tmux on Stop()** - Clean session termination
|
||||
- **Deacon uses session package** - Correct hq- session names (gt-r38pj)
|
||||
- **Honor rig agent for witness/refinery** - Respect per-rig settings
|
||||
- **Canonical hq role bead IDs** - Consistent naming
|
||||
- **hq- prefix in status display** - Global agents shown correctly (gt-vcvyd)
|
||||
- **Restart Claude when dead** - Recover sessions where tmux exists but Claude died
|
||||
- **Town session cycling** - Works from any directory
|
||||
|
||||
#### Polecat & Crew
|
||||
- **Nuke not blocked by stale hooks** - Closed beads don't prevent cleanup (gt-jc7bq)
|
||||
- **Crew stop dry-run support** - Preview cleanup before executing (gt-kjcx4)
|
||||
- **Crew defaults to --all** - `gt crew start <rig>` starts all crew (gt-s8mpt)
|
||||
- **Polecat cleanup handlers** - `gt witness process` invokes handlers (gt-h3gzj)
|
||||
|
||||
#### Daemon & Configuration
|
||||
- **Create mayor/daemon.json** - `gt start` and `gt doctor --fix` initialize daemon state (#225)
|
||||
- **Initialize git before beads** - Enable repo fingerprint (#180)
|
||||
- **Handoff preserves env vars** - Claude Code environment not lost (#216)
|
||||
- **Agent settings passed correctly** - Witness and daemon respawn use rigPath
|
||||
- **Log rig discovery errors** - Don't silently swallow (gt-rsnj9)
|
||||
|
||||
#### Refinery & Merge Queue
|
||||
- **Use rig's default_branch** - Not hardcoded 'main'
|
||||
- **MERGE_FAILED sent to Witness** - Proper failure notification
|
||||
- **Removed BranchPushedToRemote checks** - Local-only workflow support (gt-dymy5)
|
||||
|
||||
#### Misc Fixes
|
||||
- **BeadsSetupRedirect preserves tracked files** - Don't clobber existing files (gt-fj0ol)
|
||||
- **PATH export in hooks** - Ensure commands find binaries
|
||||
- **Replace panic with fallback** - ID generation gracefully degrades (#213)
|
||||
- **Removed duplicate WorktreeAddFromRef** - Code cleanup
|
||||
- **Town root beads for Deacon** - Use correct beads location (gt-sstg)
|
||||
|
||||
### Refactored
|
||||
|
||||
- **AgentStateManager pattern** - Shared state management extracted (gt-gaw8e)
|
||||
- **CleanupStatus type** - Replace raw strings (gt-77gq7)
|
||||
- **ExecWithOutput utility** - Common command execution (gt-vurfr)
|
||||
- **runBdCommand helper** - DRY mail package (gt-8i6bg)
|
||||
- **Config expansion helper** - Generic DRY config (gt-i85sg)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **Property layers guide** - Implementation documentation
|
||||
- **Worktree architecture** - Clarified beads routing
|
||||
- **Agent config** - Onboarding docs mention --agent overrides
|
||||
- **Polecat Operations section** - Added to Mayor docs (#140)
|
||||
|
||||
### Contributors
|
||||
|
||||
Thanks to all contributors for this release:
|
||||
- @julianknutsen - Claude settings inheritance (#239)
|
||||
- @joshuavial - Sling wisp JSON parse (#238)
|
||||
- @michaellady - Unified beads redirect (#222), daemon.json fix (#225)
|
||||
- @greghughespdx - PATH in hooks fix (#139)
|
||||
|
||||
## [0.2.1] - 2026-01-05
|
||||
|
||||
Bug fixes, security hardening, and new `gt config` command.
|
||||
|
||||
### Added
|
||||
|
||||
- **`gt config` command** - Manage agent settings (model, provider) per-rig or globally
|
||||
- **`hq-` prefix for patrol sessions** - Mayor and Deacon sessions use town-prefixed names
|
||||
- **Doctor hooks-path check** - Verify Git hooks path is configured correctly
|
||||
- **Block internal PRs** - Pre-push hook and GitHub Action prevent accidental internal PRs (#117)
|
||||
- **Dispatcher notifications** - Notify dispatcher when polecat work completes
|
||||
- **Unit tests** - Added tests for `formatTrackBeadID` helper, done redirect, hook slot E2E
|
||||
|
||||
### Fixed
|
||||
|
||||
#### Security
|
||||
- **Command injection prevention** - Validate beads prefix to prevent injection (gt-l1xsa)
|
||||
- **Path traversal prevention** - Validate crew names to prevent traversal (gt-wzxwm)
|
||||
- **ReDoS prevention** - Escape user input in mail search (gt-qysj9)
|
||||
- **Error handling** - Handle crypto/rand.Read errors in ID generation
|
||||
|
||||
#### Convoy & Sling
|
||||
- **Hook slot initialization** - Set hook slot when creating agent beads during sling (#124)
|
||||
- **Cross-rig bead formatting** - Format cross-rig beads as external refs in convoy tracking (#123)
|
||||
- **Reliable bd calls** - Add `--no-daemon` and `BEADS_DIR` for reliable beads operations
|
||||
|
||||
#### Rig Inference
|
||||
- **`gt rig status`** - Infer rig name from current working directory
|
||||
- **`gt crew start --all`** - Infer rig from cwd for batch crew starts
|
||||
- **`gt prime` in crew start** - Pass as initial prompt in crew start commands
|
||||
- **Town default_agent** - Honor default agent setting for Mayor and Deacon
|
||||
|
||||
#### Session & Lifecycle
|
||||
- **Hook persistence** - Hook persists across session interruption via `in_progress` lookup (gt-ttn3h)
|
||||
- **Polecat cleanup** - Clean up stale worktrees and git tracking
|
||||
- **`gt done` redirect** - Use ResolveBeadsDir for redirect file support
|
||||
|
||||
#### Build & CI
|
||||
- **Embedded formulas** - Sync and commit formulas for `go install @latest`
|
||||
- **CI lint fixes** - Resolve lint and build errors
|
||||
- **Flaky test fix** - Sync database before beads integration tests
|
||||
|
||||
## [0.2.0] - 2026-01-04
|
||||
|
||||
Major release featuring the Convoy Dashboard, two-level beads architecture, and significant multi-agent improvements.
|
||||
|
||||
2
Makefile
2
Makefile
@@ -23,7 +23,7 @@ ifeq ($(shell uname),Darwin)
|
||||
endif
|
||||
|
||||
install: build
|
||||
cp $(BUILD_DIR)/$(BINARY) ~/bin/$(BINARY)
|
||||
cp $(BUILD_DIR)/$(BINARY) ~/.local/bin/$(BINARY)
|
||||
|
||||
clean:
|
||||
rm -f $(BUILD_DIR)/$(BINARY)
|
||||
|
||||
629
README.md
629
README.md
@@ -1,372 +1,481 @@
|
||||
# Gas Town
|
||||
|
||||
Multi-agent orchestrator for Claude Code. Track work with convoys; sling to agents.
|
||||
**Multi-agent orchestration system for Claude Code with persistent work tracking**
|
||||
|
||||
## Why Gas Town?
|
||||
## Overview
|
||||
|
||||
| Without | With Gas Town |
|
||||
|---------|---------------|
|
||||
| Agents forget work after restart | Work persists on hooks - survives crashes, compaction, restarts |
|
||||
| Manual coordination | Agents have mailboxes, identities, and structured handoffs |
|
||||
| 4-10 agents is chaotic | Comfortably scale to 20-30 agents |
|
||||
| Work state in agent memory | Work state in Beads (git-backed ledger) |
|
||||
Gas Town is a workspace manager that lets you coordinate multiple Claude Code agents working on different tasks. Instead of losing context when agents restart, Gas Town persists work state in git-backed hooks, enabling reliable multi-agent workflows.
|
||||
|
||||
## Prerequisites
|
||||
### What Problem Does This Solve?
|
||||
|
||||
- **Go 1.23+** - [go.dev/dl](https://go.dev/dl/)
|
||||
- **Git 2.25+** - for worktree support
|
||||
- **beads (bd)** - [github.com/steveyegge/beads](https://github.com/steveyegge/beads) - required for issue tracking
|
||||
- **tmux 3.0+** - recommended for the full experience (the Mayor session is the primary interface)
|
||||
- **Claude Code CLI** - [claude.ai/code](https://claude.ai/code)
|
||||
| Challenge | Gas Town Solution |
|
||||
| ------------------------------- | -------------------------------------------- |
|
||||
| Agents lose context on restart | Work persists in git-backed hooks |
|
||||
| Manual agent coordination | Built-in mailboxes, identities, and handoffs |
|
||||
| 4-10 agents become chaotic | Scale comfortably to 20-30 agents |
|
||||
| Work state lost in agent memory | Work state stored in Beads ledger |
|
||||
|
||||
## Quick Start
|
||||
### Architecture
|
||||
|
||||
```bash
|
||||
# Install
|
||||
go install github.com/steveyegge/gastown/cmd/gt@latest
|
||||
```mermaid
|
||||
graph TB
|
||||
Mayor[The Mayor<br/>AI Coordinator]
|
||||
Town[Town Workspace<br/>~/gt/]
|
||||
|
||||
# Ensure Go binaries are in your PATH (add to ~/.zshrc or ~/.bashrc)
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
Town --> Mayor
|
||||
Town --> Rig1[Rig: Project A]
|
||||
Town --> Rig2[Rig: Project B]
|
||||
|
||||
# Create workspace (--git auto-initializes git repository)
|
||||
gt install ~/gt --git
|
||||
cd ~/gt
|
||||
Rig1 --> Crew1[Crew Member<br/>Your workspace]
|
||||
Rig1 --> Hooks1[Hooks<br/>Persistent storage]
|
||||
Rig1 --> Polecats1[Polecats<br/>Worker agents]
|
||||
|
||||
# Add a project
|
||||
gt rig add myproject https://github.com/you/repo.git
|
||||
Rig2 --> Crew2[Crew Member]
|
||||
Rig2 --> Hooks2[Hooks]
|
||||
Rig2 --> Polecats2[Polecats]
|
||||
|
||||
# Create your personal workspace
|
||||
gt crew add <yourname> --rig myproject
|
||||
Hooks1 -.git worktree.-> GitRepo1[Git Repository]
|
||||
Hooks2 -.git worktree.-> GitRepo2[Git Repository]
|
||||
|
||||
# Start working
|
||||
cd myproject/crew/<yourname>
|
||||
```
|
||||
|
||||
For advanced multi-agent coordination, use the Mayor session:
|
||||
|
||||
```bash
|
||||
gt mayor attach # Enter the Mayor's office
|
||||
```
|
||||
|
||||
Inside the Mayor session, you're talking to Claude with full town context:
|
||||
|
||||
> "Help me fix the authentication bug in myproject"
|
||||
|
||||
The Mayor will create convoys, dispatch workers, and coordinate everything. You can also run CLI commands directly:
|
||||
|
||||
```bash
|
||||
# Create a convoy and sling work (CLI workflow)
|
||||
gt convoy create "Feature X" issue-123 issue-456 --notify --human
|
||||
gt sling issue-123 myproject
|
||||
|
||||
# Track progress
|
||||
gt convoy list
|
||||
|
||||
# Switch between agent sessions
|
||||
gt agents
|
||||
style Mayor fill:#e1f5ff
|
||||
style Town fill:#f0f0f0
|
||||
style Rig1 fill:#fff4e1
|
||||
style Rig2 fill:#fff4e1
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
**The Mayor** is your AI coordinator. It's Claude Code with full context about your workspace, projects, and agents. The Mayor session (`gt prime`) is the primary way to interact with Gas Town - just tell it what you want to accomplish.
|
||||
### The Mayor 🎩
|
||||
|
||||
```
|
||||
Town (~/gt/) Your workspace
|
||||
├── Mayor Your AI coordinator (start here)
|
||||
├── Rig (project) Container for a git project + its agents
|
||||
│ ├── Polecats Workers (ephemeral, spawn → work → disappear)
|
||||
│ ├── Witness Monitors workers, handles lifecycle
|
||||
│ └── Refinery Merge queue processor
|
||||
```
|
||||
Your primary AI coordinator. The Mayor is a Claude Code instance with full context about your workspace, projects, and agents. **Start here** - just tell the Mayor what you want to accomplish.
|
||||
|
||||
**Hook**: Each agent has a hook where work hangs. On wake, run what's on your hook.
|
||||
### Town 🏘️
|
||||
|
||||
**Beads**: Git-backed issue tracker. All work state lives here. [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
|
||||
Your workspace directory (e.g., `~/gt/`). Contains all projects, agents, and configuration.
|
||||
|
||||
## Workflows
|
||||
### Rigs 🏗️
|
||||
|
||||
### Full Stack (Recommended)
|
||||
Project containers. Each rig wraps a git repository and manages its associated agents.
|
||||
|
||||
The primary Gas Town experience. Agents run in tmux sessions with the Mayor as your interface.
|
||||
### Crew Members 👤
|
||||
|
||||
Your personal workspace within a rig. Where you do hands-on work.
|
||||
|
||||
### Polecats 🦨
|
||||
|
||||
Ephemeral worker agents that spawn, complete a task, and disappear.
|
||||
|
||||
### Hooks 🪝
|
||||
|
||||
Git worktree-based persistent storage for agent work. Survives crashes and restarts.
|
||||
|
||||
### Convoys 🚚
|
||||
|
||||
Work tracking units. Bundle multiple issues/tasks that get assigned to agents.
|
||||
|
||||
### Beads Integration 📿
|
||||
|
||||
Git-backed issue tracking system that stores work state as structured data.
|
||||
|
||||
> **New to Gas Town?** See the [Glossary](docs/glossary.md) for a complete guide to terminology and concepts.
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Go 1.23+** - [go.dev/dl](https://go.dev/dl/)
|
||||
- **Git 2.25+** - for worktree support
|
||||
- **beads (bd) 0.44.0+** - [github.com/steveyegge/beads](https://github.com/steveyegge/beads) (required for custom type support)
|
||||
- **tmux 3.0+** - recommended for full experience
|
||||
- **Claude Code CLI** (default runtime) - [claude.ai/code](https://claude.ai/code)
|
||||
- **Codex CLI** (optional runtime) - [developers.openai.com/codex/cli](https://developers.openai.com/codex/cli)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
gt start # Start Gas Town (daemon + Mayor session)
|
||||
gt mayor attach # Enter Mayor session
|
||||
# Install Gas Town
|
||||
go install github.com/steveyegge/gastown/cmd/gt@latest
|
||||
|
||||
# Inside Mayor session, just ask:
|
||||
# "Create a convoy for issues 123 and 456 in myproject"
|
||||
# "What's the status of my work?"
|
||||
# "Show me what the witness is doing"
|
||||
# Add Go binaries to PATH (add to ~/.zshrc or ~/.bashrc)
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
|
||||
# Or use CLI commands:
|
||||
gt convoy create "Feature X" issue-123 issue-456
|
||||
gt sling issue-123 myproject # Spawns polecat automatically
|
||||
gt convoy list # Dashboard view
|
||||
gt agents # Navigate between sessions
|
||||
# Create workspace with git initialization
|
||||
gt install ~/gt --git
|
||||
cd ~/gt
|
||||
|
||||
# Add your first project
|
||||
gt rig add myproject https://github.com/you/repo.git
|
||||
|
||||
# Create your crew workspace
|
||||
gt crew add yourname --rig myproject
|
||||
cd myproject/crew/yourname
|
||||
|
||||
# Start the Mayor session (your main interface)
|
||||
gt mayor attach
|
||||
```
|
||||
|
||||
### Minimal (No Tmux)
|
||||
## Quick Start Guide
|
||||
|
||||
Run individual Claude Code instances manually. Gas Town just tracks state.
|
||||
### Basic Workflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant You
|
||||
participant Mayor
|
||||
participant Convoy
|
||||
participant Agent
|
||||
participant Hook
|
||||
|
||||
You->>Mayor: Tell Mayor what to build
|
||||
Mayor->>Convoy: Create convoy with issues
|
||||
Mayor->>Agent: Sling issue to agent
|
||||
Agent->>Hook: Store work state
|
||||
Agent->>Agent: Complete work
|
||||
Agent->>Convoy: Report completion
|
||||
Mayor->>You: Summary of progress
|
||||
```
|
||||
|
||||
### Example: Feature Development
|
||||
|
||||
```bash
|
||||
# 1. Start the Mayor
|
||||
gt mayor attach
|
||||
|
||||
# 2. In Mayor session, create a convoy
|
||||
gt convoy create "Feature X" issue-123 issue-456 --notify --human
|
||||
|
||||
# 3. Assign work to an agent
|
||||
gt sling issue-123 myproject
|
||||
|
||||
# 4. Track progress
|
||||
gt convoy list
|
||||
|
||||
# 5. Monitor agents
|
||||
gt agents
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Mayor Workflow (Recommended)
|
||||
|
||||
**Best for:** Coordinating complex, multi-issue work
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Start([Start Mayor]) --> Tell[Tell Mayor<br/>what to build]
|
||||
Tell --> Creates[Mayor creates<br/>convoy + agents]
|
||||
Creates --> Monitor[Monitor progress<br/>via convoy list]
|
||||
Monitor --> Done{All done?}
|
||||
Done -->|No| Monitor
|
||||
Done -->|Yes| Review[Review work]
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# Attach to Mayor
|
||||
gt mayor attach
|
||||
|
||||
# In Mayor, create convoy and let it orchestrate
|
||||
gt convoy create "Auth System" issue-101 issue-102 --notify
|
||||
|
||||
# Track progress
|
||||
gt convoy list
|
||||
```
|
||||
|
||||
### Minimal Mode (No Tmux)
|
||||
|
||||
Run individual runtime instances manually. Gas Town just tracks state.
|
||||
|
||||
```bash
|
||||
gt convoy create "Fix bugs" issue-123 # Create convoy (sling auto-creates if skipped)
|
||||
gt sling issue-123 myproject # Assign to worker
|
||||
claude --resume # Agent reads mail, runs work
|
||||
claude --resume # Agent reads mail, runs work (Claude)
|
||||
# or: codex # Start Codex in the workspace
|
||||
gt convoy list # Check progress
|
||||
```
|
||||
|
||||
### Pick Your Roles
|
||||
### Beads Formula Workflow
|
||||
|
||||
Gas Town is modular. Run what you need:
|
||||
**Best for:** Predefined, repeatable processes
|
||||
|
||||
- **Polecats only**: Manual spawning, no monitoring
|
||||
- **+ Witness**: Automatic worker lifecycle, stuck detection
|
||||
- **+ Refinery**: Merge queue, code review
|
||||
- **+ Mayor**: Cross-project coordination
|
||||
Formulas are TOML-defined workflows stored in `.beads/formulas/`.
|
||||
|
||||
## Cooking Formulas
|
||||
|
||||
Formulas define structured workflows. Cook them, sling them to agents.
|
||||
|
||||
### Basic Example
|
||||
**Example Formula** (`.beads/formulas/release.formula.toml`):
|
||||
|
||||
```toml
|
||||
# .beads/formulas/shiny.formula.toml
|
||||
formula = "shiny"
|
||||
description = "Design before code, review before ship"
|
||||
description = "Standard release process"
|
||||
formula = "release"
|
||||
version = 1
|
||||
|
||||
[[steps]]
|
||||
id = "design"
|
||||
description = "Think about architecture"
|
||||
|
||||
[[steps]]
|
||||
id = "implement"
|
||||
needs = ["design"]
|
||||
|
||||
[[steps]]
|
||||
id = "test"
|
||||
needs = ["implement"]
|
||||
|
||||
[[steps]]
|
||||
id = "submit"
|
||||
needs = ["test"]
|
||||
```
|
||||
|
||||
### Using Formulas
|
||||
|
||||
```bash
|
||||
bd formula list # See available formulas
|
||||
bd cook shiny # Cook into a protomolecule
|
||||
bd mol pour shiny --var feature=auth # Create runnable molecule
|
||||
gt convoy create "Auth feature" gt-xyz # Track with convoy
|
||||
gt sling gt-xyz myproject # Assign to worker
|
||||
gt convoy list # Monitor progress
|
||||
```
|
||||
|
||||
### What Happens
|
||||
|
||||
1. **Cook** expands the formula into a protomolecule (frozen template)
|
||||
2. **Pour** creates a molecule (live workflow) with steps as beads
|
||||
3. **Worker executes** each step, closing beads as it goes
|
||||
4. **Crash recovery**: Worker restarts, reads molecule, continues from last step
|
||||
|
||||
### Example: Beads Release Molecule
|
||||
|
||||
A real workflow for releasing a new beads version:
|
||||
|
||||
```toml
|
||||
formula = "beads-release"
|
||||
description = "Version bump and release workflow"
|
||||
[vars.version]
|
||||
description = "The semantic version to release (e.g., 1.2.0)"
|
||||
required = true
|
||||
|
||||
[[steps]]
|
||||
id = "bump-version"
|
||||
description = "Update version in version.go and CHANGELOG"
|
||||
|
||||
[[steps]]
|
||||
id = "update-deps"
|
||||
needs = ["bump-version"]
|
||||
description = "Run go mod tidy, update go.sum"
|
||||
title = "Bump version"
|
||||
description = "Run ./scripts/bump-version.sh {{version}}"
|
||||
|
||||
[[steps]]
|
||||
id = "run-tests"
|
||||
needs = ["update-deps"]
|
||||
description = "Full test suite, check for regressions"
|
||||
title = "Run tests"
|
||||
description = "Run make test"
|
||||
needs = ["bump-version"]
|
||||
|
||||
[[steps]]
|
||||
id = "build-binaries"
|
||||
id = "build"
|
||||
title = "Build"
|
||||
description = "Run make build"
|
||||
needs = ["run-tests"]
|
||||
description = "Cross-compile for all platforms"
|
||||
|
||||
[[steps]]
|
||||
id = "create-tag"
|
||||
needs = ["build-binaries"]
|
||||
description = "Git tag with version, push to origin"
|
||||
title = "Create release tag"
|
||||
description = "Run git tag -a v{{version}} -m 'Release v{{version}}'"
|
||||
needs = ["build"]
|
||||
|
||||
[[steps]]
|
||||
id = "publish-release"
|
||||
id = "publish"
|
||||
title = "Publish"
|
||||
description = "Run ./scripts/publish.sh"
|
||||
needs = ["create-tag"]
|
||||
description = "Create GitHub release with binaries"
|
||||
```
|
||||
|
||||
Cook it, pour it, sling it. The polecat runs through each step, and if it crashes
|
||||
after `run-tests`, a new polecat picks up at `build-binaries`.
|
||||
**Execute:**
|
||||
|
||||
### Formula Composition
|
||||
```bash
|
||||
# List available formulas
|
||||
bd formula list
|
||||
|
||||
```toml
|
||||
# Extend an existing formula
|
||||
formula = "shiny-enterprise"
|
||||
extends = ["shiny"]
|
||||
# Run a formula with variables
|
||||
bd cook release --var version=1.2.0
|
||||
|
||||
[compose]
|
||||
aspects = ["security-audit"] # Add cross-cutting concerns
|
||||
# Create formula instance for tracking
|
||||
bd mol pour release --var version=1.2.0
|
||||
```
|
||||
|
||||
### Manual Convoy Workflow
|
||||
|
||||
**Best for:** Direct control over work distribution
|
||||
|
||||
```bash
|
||||
# Create convoy manually
|
||||
gt convoy create "Bug Fixes" --human
|
||||
|
||||
# Add issues
|
||||
gt convoy add-issue bug-101 bug-102
|
||||
|
||||
# Assign to specific agents
|
||||
gt sling bug-101 myproject/my-agent
|
||||
|
||||
# Check status
|
||||
gt convoy show
|
||||
```
|
||||
|
||||
## Runtime Configuration
|
||||
|
||||
Gas Town supports multiple AI coding runtimes. Per-rig runtime settings are in `settings/config.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": {
|
||||
"provider": "codex",
|
||||
"command": "codex",
|
||||
"args": [],
|
||||
"prompt_mode": "none"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
- Claude uses hooks in `.claude/settings.json` for mail injection and startup.
|
||||
- For Codex, set `project_doc_fallback_filenames = ["CLAUDE.md"]` in
|
||||
`~/.codex/config.toml` so role instructions are picked up.
|
||||
- For runtimes without hooks (e.g., Codex), Gas Town sends a startup fallback
|
||||
after the session is ready: `gt prime`, optional `gt mail check --inject`
|
||||
for autonomous roles, and `gt nudge deacon session-started`.
|
||||
|
||||
## Key Commands
|
||||
|
||||
### For Humans (Overseer)
|
||||
### Workspace Management
|
||||
|
||||
```bash
|
||||
gt start # Start Gas Town (daemon + agents)
|
||||
gt shutdown # Graceful shutdown
|
||||
gt status # Town overview
|
||||
gt <role> attach # Jump into any agent session
|
||||
# e.g., gt mayor attach, gt witness attach
|
||||
gt install <path> # Initialize workspace
|
||||
gt rig add <name> <repo> # Add project
|
||||
gt rig list # List projects
|
||||
gt crew add <name> --rig <rig> # Create crew workspace
|
||||
```
|
||||
|
||||
Most other work happens through agents - just ask them.
|
||||
|
||||
### For Agents
|
||||
### Agent Operations
|
||||
|
||||
```bash
|
||||
# Convoy (primary dashboard)
|
||||
gt convoy list # Active work across all rigs
|
||||
gt convoy status <id> # Detailed convoy progress
|
||||
gt convoy create "name" <issues> # Create new convoy
|
||||
|
||||
# Work assignment
|
||||
gt sling <bead> <rig> # Assign work to polecat
|
||||
bd ready # Show available work
|
||||
bd list --status=in_progress # Active work
|
||||
|
||||
# Communication
|
||||
gt mail inbox # Check messages
|
||||
gt mail send <addr> -s "..." -m "..."
|
||||
|
||||
# Lifecycle
|
||||
gt handoff # Request session cycle
|
||||
gt peek <agent> # Check agent health
|
||||
|
||||
# Diagnostics
|
||||
gt doctor # Health check
|
||||
gt doctor --fix # Auto-repair
|
||||
gt agents # List active agents
|
||||
gt sling <issue> <rig> # Assign work to agent
|
||||
gt sling <issue> <rig> --agent cursor # Override runtime for this sling/spawn
|
||||
gt mayor attach # Start Mayor session
|
||||
gt mayor start --agent auggie # Run Mayor with a specific agent alias
|
||||
gt prime # Alternative to mayor attach
|
||||
```
|
||||
|
||||
**Built-in agent presets**: `claude`, `gemini`, `codex`, `cursor`, `auggie`, `amp`
|
||||
|
||||
### Convoy (Work Tracking)
|
||||
|
||||
```bash
|
||||
gt convoy create <name> [issues...] # Create convoy
|
||||
gt convoy list # List all convoys
|
||||
gt convoy show [id] # Show convoy details
|
||||
gt convoy add-issue <issue> # Add issue to convoy
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# Set custom agent command
|
||||
gt config agent set claude-glm "claude-glm --model glm-4"
|
||||
gt config agent set codex-low "codex --thinking low"
|
||||
|
||||
# Set default agent
|
||||
gt config default-agent claude-glm
|
||||
|
||||
# View config
|
||||
gt config show
|
||||
```
|
||||
|
||||
### Beads Integration
|
||||
|
||||
```bash
|
||||
bd formula list # List formulas
|
||||
bd cook <formula> # Execute formula
|
||||
bd mol pour <formula> # Create trackable instance
|
||||
bd mol list # List active instances
|
||||
```
|
||||
|
||||
## Cooking Formulas
|
||||
|
||||
Gas Town includes built-in formulas for common workflows. See `.beads/formulas/` for available recipes.
|
||||
|
||||
## Dashboard
|
||||
|
||||
Web-based dashboard for monitoring Gas Town activity.
|
||||
Gas Town includes a web dashboard for monitoring:
|
||||
|
||||
```bash
|
||||
# Start the dashboard
|
||||
# Start dashboard
|
||||
gt dashboard --port 8080
|
||||
|
||||
# Open in browser
|
||||
open http://localhost:8080
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- **Convoy tracking** - View all active convoys with progress bars and work status
|
||||
- **Polecat workers** - See active worker sessions and their activity status
|
||||
- **Refinery status** - Monitor merge queue and PR processing
|
||||
- **Auto-refresh** - Updates every 10 seconds via htmx
|
||||
Features:
|
||||
|
||||
Work status indicators:
|
||||
| Status | Color | Meaning |
|
||||
|--------|-------|---------|
|
||||
| `complete` | Green | All tracked items done |
|
||||
| `active` | Green | Recent activity (< 1 min) |
|
||||
| `stale` | Yellow | Activity 1-5 min ago |
|
||||
| `stuck` | Red | Activity > 5 min ago |
|
||||
| `waiting` | Gray | No assignee/activity |
|
||||
- Real-time agent status
|
||||
- Convoy progress tracking
|
||||
- Hook state visualization
|
||||
- Configuration management
|
||||
|
||||
## Advanced Concepts
|
||||
|
||||
### The Propulsion Principle
|
||||
|
||||
Gas Town uses git hooks as a propulsion mechanism. Each hook is a git worktree with:
|
||||
|
||||
1. **Persistent state** - Work survives agent restarts
|
||||
2. **Version control** - All changes tracked in git
|
||||
3. **Rollback capability** - Revert to any previous state
|
||||
4. **Multi-agent coordination** - Shared through git
|
||||
|
||||
### Hook Lifecycle
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Created: Agent spawned
|
||||
Created --> Active: Work assigned
|
||||
Active --> Suspended: Agent paused
|
||||
Suspended --> Active: Agent resumed
|
||||
Active --> Completed: Work done
|
||||
Completed --> Archived: Hook archived
|
||||
Archived --> [*]
|
||||
```
|
||||
|
||||
### MEOW (Mayor-Enhanced Orchestration Workflow)
|
||||
|
||||
MEOW is the recommended pattern:
|
||||
|
||||
1. **Tell the Mayor** - Describe what you want
|
||||
2. **Mayor analyzes** - Breaks down into tasks
|
||||
3. **Convoy creation** - Mayor creates convoy with issues
|
||||
4. **Agent spawning** - Mayor spawns appropriate agents
|
||||
5. **Work distribution** - Issues slung to agents via hooks
|
||||
6. **Progress monitoring** - Track through convoy status
|
||||
7. **Completion** - Mayor summarizes results
|
||||
|
||||
## Shell Completions
|
||||
|
||||
Enable tab completion for `gt` commands:
|
||||
|
||||
### Bash
|
||||
|
||||
```bash
|
||||
# Add to ~/.bashrc
|
||||
source <(gt completion bash)
|
||||
# Bash
|
||||
gt completion bash > /etc/bash_completion.d/gt
|
||||
|
||||
# Or install permanently
|
||||
gt completion bash > /usr/local/etc/bash_completion.d/gt
|
||||
```
|
||||
|
||||
### Zsh
|
||||
|
||||
```bash
|
||||
# Add to ~/.zshrc (before compinit)
|
||||
source <(gt completion zsh)
|
||||
|
||||
# Or install to fpath
|
||||
# Zsh
|
||||
gt completion zsh > "${fpath[1]}/_gt"
|
||||
```
|
||||
|
||||
### Fish
|
||||
|
||||
```bash
|
||||
# Fish
|
||||
gt completion fish > ~/.config/fish/completions/gt.fish
|
||||
```
|
||||
|
||||
## Roles
|
||||
## Project Roles
|
||||
|
||||
| Role | Scope | Job |
|
||||
|------|-------|-----|
|
||||
| **Overseer** | Human | Sets strategy, reviews output, handles escalations |
|
||||
| **Mayor** | Town-wide | Cross-rig coordination, work dispatch |
|
||||
| **Deacon** | Town-wide | Daemon process, agent lifecycle, plugin execution |
|
||||
| **Witness** | Per-rig | Monitor polecats, nudge stuck workers |
|
||||
| **Refinery** | Per-rig | Merge queue, PR review, integration |
|
||||
| **Polecat** | Per-task | Execute work, file discovered issues, request shutdown |
|
||||
| Role | Description | Primary Interface |
|
||||
| --------------- | ------------------ | -------------------- |
|
||||
| **Mayor** | AI coordinator | `gt mayor attach` |
|
||||
| **Human (You)** | Crew member | Your crew directory |
|
||||
| **Polecat** | Worker agent | Spawned by Mayor |
|
||||
| **Hook** | Persistent storage | Git worktree |
|
||||
| **Convoy** | Work tracker | `gt convoy` commands |
|
||||
|
||||
## The Propulsion Principle
|
||||
## Tips
|
||||
|
||||
> If your hook has work, RUN IT.
|
||||
- **Always start with the Mayor** - It's designed to be your primary interface
|
||||
- **Use convoys for coordination** - They provide visibility across agents
|
||||
- **Leverage hooks for persistence** - Your work won't disappear
|
||||
- **Create formulas for repeated tasks** - Save time with Beads recipes
|
||||
- **Monitor the dashboard** - Get real-time visibility
|
||||
- **Let the Mayor orchestrate** - It knows how to manage agents
|
||||
|
||||
Agents wake up, check their hook, execute the molecule. No waiting for commands.
|
||||
Molecules survive crashes - any agent can continue where another left off.
|
||||
## Troubleshooting
|
||||
|
||||
---
|
||||
### Agents lose connection
|
||||
|
||||
## Optional: MEOW Deep Dive
|
||||
Check hooks are properly initialized:
|
||||
|
||||
**M**olecular **E**xpression **O**f **W**ork - the full algebra.
|
||||
```bash
|
||||
gt hooks list
|
||||
gt hooks repair
|
||||
```
|
||||
|
||||
### States of Matter
|
||||
### Convoy stuck
|
||||
|
||||
| Phase | Name | Storage | Behavior |
|
||||
|-------|------|---------|----------|
|
||||
| Ice-9 | Formula | `.beads/formulas/` | Source template, composable |
|
||||
| Solid | Protomolecule | `.beads/` | Frozen template, reusable |
|
||||
| Liquid | Mol | `.beads/` | Flowing work, persistent |
|
||||
| Vapor | Wisp | `.beads/` (ephemeral flag) | Transient, for patrols |
|
||||
Force refresh:
|
||||
|
||||
*(Protomolecules are an homage to The Expanse. Ice-9 is a nod to Vonnegut.)*
|
||||
```bash
|
||||
gt convoy refresh <convoy-id>
|
||||
```
|
||||
|
||||
### Operators
|
||||
### Mayor not responding
|
||||
|
||||
| Operator | From → To | Effect |
|
||||
|----------|-----------|--------|
|
||||
| `cook` | Formula → Protomolecule | Expand macros, flatten |
|
||||
| `pour` | Proto → Mol | Instantiate as persistent |
|
||||
| `wisp` | Proto → Wisp | Instantiate as ephemeral |
|
||||
| `squash` | Mol/Wisp → Digest | Condense to permanent record |
|
||||
| `burn` | Wisp → ∅ | Discard without record |
|
||||
Restart Mayor session:
|
||||
|
||||
---
|
||||
```bash
|
||||
gt mayor detach
|
||||
gt mayor attach
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT License - see LICENSE file for details
|
||||
|
||||
---
|
||||
|
||||
**Getting Started:** Run `gt install ~/gt --git && cd ~/gt && gt config agent list && gt mayor attach` (or `gt mayor attach --agent codex`) and tell the Mayor what you want to build!
|
||||
|
||||
@@ -17,7 +17,9 @@ Complete setup guide for Gas Town multi-agent orchestrator.
|
||||
| Tool | Version | Check | Install |
|
||||
|------|---------|-------|---------|
|
||||
| **tmux** | 3.0+ | `tmux -V` | See below |
|
||||
| **Claude Code** | latest | `claude --version` | See [claude.ai/claude-code](https://claude.ai/claude-code) |
|
||||
| **Claude Code** (default) | latest | `claude --version` | See [claude.ai/claude-code](https://claude.ai/claude-code) |
|
||||
| **Codex CLI** (optional) | latest | `codex --version` | See [developers.openai.com/codex/cli](https://developers.openai.com/codex/cli) |
|
||||
| **OpenCode CLI** (optional) | latest | `opencode --version` | See [opencode.ai](https://opencode.ai) |
|
||||
|
||||
## Installing Prerequisites
|
||||
|
||||
@@ -130,22 +132,46 @@ gt doctor # Run health checks
|
||||
gt status # Show workspace status
|
||||
```
|
||||
|
||||
### Step 5: Configure Agents (Optional)
|
||||
|
||||
Gas Town supports built-in runtimes (`claude`, `gemini`, `codex`) plus custom agent aliases.
|
||||
|
||||
```bash
|
||||
# List available agents
|
||||
gt config agent list
|
||||
|
||||
# Create an alias (aliases can encode model/thinking flags)
|
||||
gt config agent set codex-low "codex --thinking low"
|
||||
gt config agent set claude-haiku "claude --model haiku --dangerously-skip-permissions"
|
||||
|
||||
# Set the town default agent (used when a rig doesn't specify one)
|
||||
gt config default-agent codex-low
|
||||
```
|
||||
|
||||
You can also override the agent per command without changing defaults:
|
||||
|
||||
```bash
|
||||
gt start --agent codex-low
|
||||
gt sling issue-123 myproject --agent claude-haiku
|
||||
```
|
||||
|
||||
## Minimal Mode vs Full Stack Mode
|
||||
|
||||
Gas Town supports two operational modes:
|
||||
|
||||
### Minimal Mode (No Daemon)
|
||||
|
||||
Run individual Claude Code instances manually. Gas Town only tracks state.
|
||||
Run individual runtime instances manually. Gas Town only tracks state.
|
||||
|
||||
```bash
|
||||
# Create and assign work
|
||||
gt convoy create "Fix bugs" issue-123
|
||||
gt sling issue-123 myproject
|
||||
|
||||
# Run Claude manually
|
||||
# Run runtime manually
|
||||
cd ~/gt/myproject/polecats/<worker>
|
||||
claude --resume
|
||||
claude --resume # Claude Code
|
||||
# or: codex # Codex CLI
|
||||
|
||||
# Check progress
|
||||
gt convoy list
|
||||
@@ -184,7 +210,7 @@ Gas Town is modular. Enable only what you need:
|
||||
|--------------|-------|----------|
|
||||
| **Polecats only** | Workers | Manual spawning, no monitoring |
|
||||
| **+ Witness** | + Monitor | Automatic lifecycle, stuck detection |
|
||||
| **+ Refinery** | + Merge queue | PR review, code integration |
|
||||
| **+ Refinery** | + Merge queue | MR review, code integration |
|
||||
| **+ Mayor** | + Coordinator | Cross-project coordination |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -84,29 +84,43 @@ Each agent bead references its role bead via the `role_bead` field.
|
||||
│ └── town.json Town configuration
|
||||
└── <rig>/ Project container (NOT a git clone)
|
||||
├── config.json Rig identity and beads prefix
|
||||
├── .beads/ → mayor/rig/.beads Symlink to canonical beads
|
||||
├── .repo.git/ Bare repo (shared by worktrees)
|
||||
├── mayor/rig/ Mayor's clone (canonical beads)
|
||||
├── refinery/rig/ Worktree on main
|
||||
├── mayor/rig/ Canonical clone (beads live here)
|
||||
│ └── .beads/ Rig-level beads database
|
||||
├── refinery/rig/ Worktree from mayor/rig
|
||||
├── witness/ No clone (monitors only)
|
||||
├── crew/<name>/ Human workspaces
|
||||
└── polecats/<name>/ Worker worktrees
|
||||
├── crew/<name>/ Human workspaces (full clones)
|
||||
└── polecats/<name>/ Worker worktrees from mayor/rig
|
||||
```
|
||||
|
||||
### Worktree Architecture
|
||||
|
||||
Polecats and refinery are git worktrees, not full clones. This enables fast spawning
|
||||
and shared object storage. The worktree base is `mayor/rig`:
|
||||
|
||||
```go
|
||||
// From polecat/manager.go - worktrees are based on mayor/rig
|
||||
git worktree add -b polecat/<name>-<timestamp> polecats/<name>
|
||||
```
|
||||
|
||||
Crew workspaces (`crew/<name>/`) are full git clones for human developers who need
|
||||
independent repos. Polecats are ephemeral and benefit from worktree efficiency.
|
||||
|
||||
## Beads Routing
|
||||
|
||||
The `routes.jsonl` file maps issue ID prefixes to their storage locations:
|
||||
The `routes.jsonl` file maps issue ID prefixes to rig locations (relative to town root):
|
||||
|
||||
```jsonl
|
||||
{"prefix":"hq","path":"/Users/stevey/gt/.beads"}
|
||||
{"prefix":"gt","path":"/Users/stevey/gt/gastown/mayor/rig/.beads"}
|
||||
{"prefix":"hq-","path":"."}
|
||||
{"prefix":"gt-","path":"gastown/mayor/rig"}
|
||||
{"prefix":"bd-","path":"beads/mayor/rig"}
|
||||
```
|
||||
|
||||
Routes point to `mayor/rig` because that's where the canonical `.beads/` lives.
|
||||
This enables transparent cross-rig beads operations:
|
||||
|
||||
```bash
|
||||
bd show hq-mayor # Routes to town beads
|
||||
bd show gt-xyz # Routes to gastown rig beads
|
||||
bd show hq-mayor # Routes to town beads (~/.gt/.beads)
|
||||
bd show gt-xyz # Routes to gastown/mayor/rig/.beads
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
94
docs/glossary.md
Normal file
94
docs/glossary.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Gas Town Glossary
|
||||
|
||||
Gas Town is an agentic development environment for managing multiple Claude Code instances simultaneously using the `gt` and `bd` (Beads) binaries, coordinated with tmux in git-managed directories.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### MEOW (Molecular Expression of Work)
|
||||
Breaking large goals into detailed instructions for agents. Supported by Beads, Epics, Formulas, and Molecules. MEOW ensures work is decomposed into trackable, atomic units that agents can execute autonomously.
|
||||
|
||||
### GUPP (Gas Town Universal Propulsion Principle)
|
||||
"If there is work on your Hook, YOU MUST RUN IT." This principle ensures agents autonomously proceed with available work without waiting for external input. GUPP is the heartbeat of autonomous operation.
|
||||
|
||||
### NDI (Nondeterministic Idempotence)
|
||||
The overarching goal ensuring useful outcomes through orchestration of potentially unreliable processes. Persistent Beads and oversight agents (Witness, Deacon) guarantee eventual workflow completion even when individual operations may fail or produce varying results.
|
||||
|
||||
## Environments
|
||||
|
||||
### Town
|
||||
The management headquarters (e.g., `~/gt/`). The Town coordinates all workers across multiple Rigs and houses town-level agents like Mayor and Deacon.
|
||||
|
||||
### Rig
|
||||
A project-specific Git repository under Gas Town management. Each Rig has its own Polecats, Refinery, Witness, and Crew members. Rigs are where actual development work happens.
|
||||
|
||||
## Town-Level Roles
|
||||
|
||||
### Mayor
|
||||
Chief-of-staff agent responsible for initiating Convoys, coordinating work distribution, and notifying users of important events. The Mayor operates from the town level and has visibility across all Rigs.
|
||||
|
||||
### Deacon
|
||||
Daemon beacon running continuous Patrol cycles. The Deacon ensures worker activity, monitors system health, and triggers recovery when agents become unresponsive. Think of the Deacon as the system's watchdog.
|
||||
|
||||
### Dogs
|
||||
The Deacon's crew of maintenance agents handling background tasks like cleanup, health checks, and system maintenance.
|
||||
|
||||
### Boot (the Dog)
|
||||
A special Dog that checks the Deacon every 5 minutes, ensuring the watchdog itself is still watching. This creates a chain of accountability.
|
||||
|
||||
## Rig-Level Roles
|
||||
|
||||
### Polecat
|
||||
Ephemeral worker agents that produce Merge Requests. Polecats are spawned for specific tasks, complete their work, and are then cleaned up. They work in isolated git worktrees to avoid conflicts.
|
||||
|
||||
### Refinery
|
||||
Manages the Merge Queue for a Rig. The Refinery intelligently merges changes from Polecats, handling conflicts and ensuring code quality before changes reach the main branch.
|
||||
|
||||
### Witness
|
||||
Patrol agent that oversees Polecats and the Refinery within a Rig. The Witness monitors progress, detects stuck agents, and can trigger recovery actions.
|
||||
|
||||
### Crew
|
||||
Long-lived, named agents for persistent collaboration. Unlike ephemeral Polecats, Crew members maintain context across sessions and are ideal for ongoing work relationships.
|
||||
|
||||
## Work Units
|
||||
|
||||
### Bead
|
||||
Git-backed atomic work unit stored in JSONL format. Beads are the fundamental unit of work tracking in Gas Town. They can represent issues, tasks, epics, or any trackable work item.
|
||||
|
||||
### Formula
|
||||
TOML-based workflow source template. Formulas define reusable patterns for common operations like patrol cycles, code review, or deployment.
|
||||
|
||||
### Protomolecule
|
||||
A template class for instantiating Molecules. Protomolecules define the structure and steps of a workflow without being tied to specific work items.
|
||||
|
||||
### Molecule
|
||||
Durable chained Bead workflows. Molecules represent multi-step processes where each step is tracked as a Bead. They survive agent restarts and ensure complex workflows complete.
|
||||
|
||||
### Wisp
|
||||
Ephemeral Beads destroyed after runs. Wisps are lightweight work items used for transient operations that don't need permanent tracking.
|
||||
|
||||
### Hook
|
||||
A special pinned Bead for each agent. The Hook is an agent's primary work queue - when work appears on your Hook, GUPP dictates you must run it.
|
||||
|
||||
## Workflow Commands
|
||||
|
||||
### Convoy
|
||||
Primary work-order wrapping related Beads. Convoys group related tasks together and can be assigned to multiple workers. Created with `gt convoy create`.
|
||||
|
||||
### Slinging
|
||||
Assigning work to agents via `gt sling`. When you sling work to a Polecat or Crew member, you're putting it on their Hook for execution.
|
||||
|
||||
### Nudging
|
||||
Real-time messaging between agents with `gt nudge`. Nudges allow immediate communication without going through the mail system.
|
||||
|
||||
### Handoff
|
||||
Agent session refresh via `/handoff`. When context gets full or an agent needs a fresh start, handoff transfers work state to a new session.
|
||||
|
||||
### Seance
|
||||
Communicating with previous sessions via `gt seance`. Allows agents to query their predecessors for context and decisions from earlier work.
|
||||
|
||||
### Patrol
|
||||
Ephemeral loop maintaining system heartbeat. Patrol agents (Deacon, Witness) continuously cycle through health checks and trigger actions as needed.
|
||||
|
||||
---
|
||||
|
||||
*This glossary was contributed by [Clay Shirky](https://github.com/cshirky) in [Issue #80](https://github.com/steveyegge/gastown/issues/80).*
|
||||
@@ -12,7 +12,7 @@ They execute molecule steps sequentially, closing each step as they complete it.
|
||||
| Type | Storage | Use Case |
|
||||
|------|---------|----------|
|
||||
| **Regular Molecule** | `.beads/` (synced) | Discrete deliverables, audit trail |
|
||||
| **Wisp** | `.beads-wisp/` (ephemeral) | Patrol cycles, operational loops |
|
||||
| **Wisp** | `.beads/` (ephemeral, type=wisp) | Patrol cycles, operational loops |
|
||||
|
||||
Polecats typically use **regular molecules** because each assignment has audit value.
|
||||
Patrol agents (Witness, Refinery, Deacon) use **wisps** to prevent accumulation.
|
||||
|
||||
300
docs/property-layers.md
Normal file
300
docs/property-layers.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Property Layers: Multi-Level Configuration
|
||||
|
||||
> Implementation guide for Gas Town's configuration system.
|
||||
> Created: 2025-01-06
|
||||
|
||||
## Overview
|
||||
|
||||
Gas Town uses a layered property system for configuration. Properties are
|
||||
looked up through multiple layers, with earlier layers overriding later ones.
|
||||
This enables both local control and global coordination.
|
||||
|
||||
## The Four Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. WISP LAYER (transient, town-local) │
|
||||
│ Location: <rig>/.beads-wisp/config/ │
|
||||
│ Synced: Never │
|
||||
│ Use: Temporary local overrides │
|
||||
└─────────────────────────────┬───────────────────────────────┘
|
||||
│ if missing
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2. RIG BEAD LAYER (persistent, synced globally) │
|
||||
│ Location: <rig>/.beads/ (rig identity bead labels) │
|
||||
│ Synced: Via git (all clones see it) │
|
||||
│ Use: Project-wide operational state │
|
||||
└─────────────────────────────┬───────────────────────────────┘
|
||||
│ if missing
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3. TOWN DEFAULTS │
|
||||
│ Location: ~/gt/config.json or ~/gt/.beads/ │
|
||||
│ Synced: N/A (per-town) │
|
||||
│ Use: Town-wide policies │
|
||||
└─────────────────────────────┬───────────────────────────────┘
|
||||
│ if missing
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 4. SYSTEM DEFAULTS (compiled in) │
|
||||
│ Use: Fallback when nothing else specified │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Lookup Behavior
|
||||
|
||||
### Override Semantics (Default)
|
||||
|
||||
For most properties, the first non-nil value wins:
|
||||
|
||||
```go
|
||||
func GetConfig(key string) interface{} {
|
||||
if val := wisp.Get(key); val != nil {
|
||||
if val == Blocked { return nil }
|
||||
return val
|
||||
}
|
||||
if val := rigBead.GetLabel(key); val != nil {
|
||||
return val
|
||||
}
|
||||
if val := townDefaults.Get(key); val != nil {
|
||||
return val
|
||||
}
|
||||
return systemDefaults[key]
|
||||
}
|
||||
```
|
||||
|
||||
### Stacking Semantics (Integers)
|
||||
|
||||
For integer properties, values from wisp and bead layers **add** to the base:
|
||||
|
||||
```go
|
||||
func GetIntConfig(key string) int {
|
||||
base := getBaseDefault(key) // Town or system default
|
||||
beadAdj := rigBead.GetInt(key) // 0 if missing
|
||||
wispAdj := wisp.GetInt(key) // 0 if missing
|
||||
return base + beadAdj + wispAdj
|
||||
}
|
||||
```
|
||||
|
||||
This enables temporary adjustments without changing the base value.
|
||||
|
||||
### Blocking Inheritance
|
||||
|
||||
You can explicitly block a property from being inherited:
|
||||
|
||||
```bash
|
||||
gt rig config set gastown auto_restart --block
|
||||
```
|
||||
|
||||
This creates a "blocked" marker in the wisp layer. Even if the rig bead
|
||||
or defaults say `auto_restart: true`, the lookup returns nil.
|
||||
|
||||
## Rig Identity Beads
|
||||
|
||||
Each rig has an identity bead for operational state:
|
||||
|
||||
```yaml
|
||||
id: gt-rig-gastown
|
||||
type: rig
|
||||
name: gastown
|
||||
repo: git@github.com:steveyegge/gastown.git
|
||||
prefix: gt
|
||||
|
||||
labels:
|
||||
- status:operational
|
||||
- priority:normal
|
||||
```
|
||||
|
||||
These beads sync via git, so all clones of the rig see the same state.
|
||||
|
||||
## Two-Level Rig Control
|
||||
|
||||
### Level 1: Park (Local, Ephemeral)
|
||||
|
||||
```bash
|
||||
gt rig park gastown # Stop services, daemon won't restart
|
||||
gt rig unpark gastown # Allow services to run
|
||||
```
|
||||
|
||||
- Stored in wisp layer (`.beads-wisp/config/`)
|
||||
- Only affects this town
|
||||
- Disappears on cleanup
|
||||
- Use: Local maintenance, debugging
|
||||
|
||||
### Level 2: Dock (Global, Persistent)
|
||||
|
||||
```bash
|
||||
gt rig dock gastown # Set status:docked label on rig bead
|
||||
gt rig undock gastown # Remove label
|
||||
```
|
||||
|
||||
- Stored on rig identity bead
|
||||
- Syncs to all clones via git
|
||||
- Permanent until explicitly changed
|
||||
- Use: Project-wide maintenance, coordinated downtime
|
||||
|
||||
### Daemon Behavior
|
||||
|
||||
The daemon checks both levels before auto-restarting:
|
||||
|
||||
```go
|
||||
func shouldAutoRestart(rig *Rig) bool {
|
||||
status := rig.GetConfig("status")
|
||||
if status == "parked" || status == "docked" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Keys
|
||||
|
||||
| Key | Type | Behavior | Description |
|
||||
|-----|------|----------|-------------|
|
||||
| `status` | string | Override | operational/parked/docked |
|
||||
| `auto_restart` | bool | Override | Daemon auto-restart behavior |
|
||||
| `max_polecats` | int | Override | Maximum concurrent polecats |
|
||||
| `priority_adjustment` | int | **Stack** | Scheduling priority modifier |
|
||||
| `maintenance_window` | string | Override | When maintenance allowed |
|
||||
| `dnd` | bool | Override | Do not disturb mode |
|
||||
|
||||
## Commands
|
||||
|
||||
### View Configuration
|
||||
|
||||
```bash
|
||||
gt rig config show gastown # Show effective config (all layers)
|
||||
gt rig config show gastown --layer # Show which layer each value comes from
|
||||
```
|
||||
|
||||
### Set Configuration
|
||||
|
||||
```bash
|
||||
# Set in wisp layer (local, ephemeral)
|
||||
gt rig config set gastown key value
|
||||
|
||||
# Set in bead layer (global, permanent)
|
||||
gt rig config set gastown key value --global
|
||||
|
||||
# Block inheritance
|
||||
gt rig config set gastown key --block
|
||||
|
||||
# Clear from wisp layer
|
||||
gt rig config unset gastown key
|
||||
```
|
||||
|
||||
### Rig Lifecycle
|
||||
|
||||
```bash
|
||||
gt rig park gastown # Local: stop + prevent restart
|
||||
gt rig unpark gastown # Local: allow restart
|
||||
|
||||
gt rig dock gastown # Global: mark as offline
|
||||
gt rig undock gastown # Global: mark as operational
|
||||
|
||||
gt rig status gastown # Show current state
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Temporary Priority Boost
|
||||
|
||||
```bash
|
||||
# Base priority: 0 (from defaults)
|
||||
# Give this rig temporary priority boost for urgent work
|
||||
|
||||
gt rig config set gastown priority_adjustment 10
|
||||
|
||||
# Effective priority: 0 + 10 = 10
|
||||
# When done, clear it:
|
||||
|
||||
gt rig config unset gastown priority_adjustment
|
||||
```
|
||||
|
||||
### Local Maintenance
|
||||
|
||||
```bash
|
||||
# I'm upgrading the local clone, don't restart services
|
||||
gt rig park gastown
|
||||
|
||||
# ... do maintenance ...
|
||||
|
||||
gt rig unpark gastown
|
||||
```
|
||||
|
||||
### Project-Wide Maintenance
|
||||
|
||||
```bash
|
||||
# Major refactor in progress, all clones should pause
|
||||
gt rig dock gastown
|
||||
|
||||
# Syncs via git - other towns see the rig as docked
|
||||
bd sync
|
||||
|
||||
# When done:
|
||||
gt rig undock gastown
|
||||
bd sync
|
||||
```
|
||||
|
||||
### Block Auto-Restart Locally
|
||||
|
||||
```bash
|
||||
# Rig bead says auto_restart: true
|
||||
# But I'm debugging and don't want that here
|
||||
|
||||
gt rig config set gastown auto_restart --block
|
||||
|
||||
# Now auto_restart returns nil for this town only
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Wisp Storage
|
||||
|
||||
Wisp config stored in `.beads-wisp/config/<rig>.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"rig": "gastown",
|
||||
"values": {
|
||||
"status": "parked",
|
||||
"priority_adjustment": 10
|
||||
},
|
||||
"blocked": ["auto_restart"]
|
||||
}
|
||||
```
|
||||
|
||||
### Rig Bead Labels
|
||||
|
||||
Rig operational state stored as labels on the rig identity bead:
|
||||
|
||||
```bash
|
||||
bd label add gt-rig-gastown status:docked
|
||||
bd label remove gt-rig-gastown status:docked
|
||||
```
|
||||
|
||||
### Daemon Integration
|
||||
|
||||
The daemon's lifecycle manager checks config before starting services:
|
||||
|
||||
```go
|
||||
func (d *Daemon) maybeStartRigServices(rig string) {
|
||||
r := d.getRig(rig)
|
||||
|
||||
status := r.GetConfig("status")
|
||||
if status == "parked" || status == "docked" {
|
||||
log.Info("Rig %s is offline, skipping auto-start", rig)
|
||||
return
|
||||
}
|
||||
|
||||
d.ensureWitness(rig)
|
||||
d.ensureRefinery(rig)
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documents
|
||||
|
||||
- `~/gt/docs/hop/PROPERTY-LAYERS.md` - Strategic architecture
|
||||
- `wisp-architecture.md` - Wisp system design
|
||||
- `agent-as-bead.md` - Agent identity beads (similar pattern)
|
||||
@@ -7,24 +7,38 @@ Technical reference for Gas Town internals. Read the README first.
|
||||
```
|
||||
~/gt/ Town root
|
||||
├── .beads/ Town-level beads (hq-* prefix)
|
||||
├── mayor/ Mayor config
|
||||
│ └── town.json
|
||||
├── mayor/ Mayor agent home (town coordinator)
|
||||
│ ├── town.json Town configuration
|
||||
│ ├── CLAUDE.md Mayor context (on disk)
|
||||
│ └── .claude/settings.json Mayor Claude settings
|
||||
├── deacon/ Deacon agent home (background supervisor)
|
||||
│ └── .claude/settings.json Deacon settings (context via gt prime)
|
||||
└── <rig>/ Project container (NOT a git clone)
|
||||
├── config.json Rig identity
|
||||
├── .beads/ → mayor/rig/.beads
|
||||
├── .repo.git/ Bare repo (shared by worktrees)
|
||||
├── mayor/rig/ Mayor's clone (canonical beads)
|
||||
├── refinery/rig/ Worktree on main
|
||||
├── witness/ No clone (monitors only)
|
||||
├── crew/<name>/ Human workspaces
|
||||
└── polecats/<name>/ Worker worktrees
|
||||
│ └── CLAUDE.md Per-rig mayor context (on disk)
|
||||
├── witness/ Witness agent home (monitors only)
|
||||
│ └── .claude/settings.json (context via gt prime)
|
||||
├── refinery/ Refinery settings parent
|
||||
│ ├── .claude/settings.json
|
||||
│ └── rig/ Worktree on main
|
||||
│ └── CLAUDE.md Refinery context (on disk)
|
||||
├── crew/ Crew settings parent (shared)
|
||||
│ ├── .claude/settings.json (context via gt prime)
|
||||
│ └── <name>/rig/ Human workspaces
|
||||
└── polecats/ Polecat settings parent (shared)
|
||||
├── .claude/settings.json (context via gt prime)
|
||||
└── <name>/rig/ Worker worktrees
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- Rig root is a container, not a clone
|
||||
- `.repo.git/` is bare - refinery and polecats are worktrees
|
||||
- Mayor clone holds canonical `.beads/`, others inherit via redirect
|
||||
- Per-rig `mayor/rig/` holds canonical `.beads/`, others inherit via redirect
|
||||
- Settings placed in parent dirs (not git clones) for upward traversal
|
||||
|
||||
## Beads Routing
|
||||
|
||||
@@ -204,6 +218,123 @@ gt mol step done <step> # Complete a molecule step
|
||||
| `GT_RIG` | Rig name for rig-level agents |
|
||||
| `GT_POLECAT` | Polecat name (for polecats only) |
|
||||
|
||||
## Agent Working Directories and Settings
|
||||
|
||||
Each agent runs in a specific working directory and has its own Claude settings.
|
||||
Understanding this hierarchy is essential for proper configuration.
|
||||
|
||||
### Working Directories by Role
|
||||
|
||||
| Role | Working Directory | Notes |
|
||||
|------|-------------------|-------|
|
||||
| **Mayor** | `~/gt/mayor/` | Town-level coordinator, isolated from rigs |
|
||||
| **Deacon** | `~/gt/deacon/` | Background supervisor daemon |
|
||||
| **Witness** | `~/gt/<rig>/witness/` | No git clone, monitors polecats only |
|
||||
| **Refinery** | `~/gt/<rig>/refinery/rig/` | Worktree on main branch |
|
||||
| **Crew** | `~/gt/<rig>/crew/<name>/rig/` | Persistent human workspace clone |
|
||||
| **Polecat** | `~/gt/<rig>/polecats/<name>/rig/` | Ephemeral worker worktree |
|
||||
|
||||
Note: The per-rig `<rig>/mayor/rig/` directory is NOT a working directory—it's
|
||||
a git clone that holds the canonical `.beads/` database for that rig.
|
||||
|
||||
### Settings File Locations
|
||||
|
||||
Claude Code searches for `.claude/settings.json` starting from the working
|
||||
directory and traversing upward. Settings are placed in **parent directories**
|
||||
(not inside git clones) so they're found via directory traversal without
|
||||
polluting source repositories:
|
||||
|
||||
```
|
||||
~/gt/
|
||||
├── mayor/.claude/settings.json # Mayor settings
|
||||
├── deacon/.claude/settings.json # Deacon settings
|
||||
└── <rig>/
|
||||
├── witness/.claude/settings.json # Witness settings (no rig/ subdir)
|
||||
├── refinery/.claude/settings.json # Found by refinery/rig/ via traversal
|
||||
├── crew/.claude/settings.json # Shared by all crew/<name>/rig/
|
||||
└── polecats/.claude/settings.json # Shared by all polecats/<name>/rig/
|
||||
```
|
||||
|
||||
**Why parent directories?** Agents working in git clones (like `refinery/rig/`)
|
||||
would pollute the source repo if settings were placed there. By putting settings
|
||||
one level up, Claude finds them via upward traversal, and all workers of the
|
||||
same type share the same settings.
|
||||
|
||||
### CLAUDE.md Locations
|
||||
|
||||
Role context is delivered via CLAUDE.md files or ephemeral injection:
|
||||
|
||||
| Role | CLAUDE.md Location | Method |
|
||||
|------|-------------------|--------|
|
||||
| **Mayor** | `~/gt/mayor/CLAUDE.md` | On disk |
|
||||
| **Deacon** | (none) | Injected via `gt prime` at SessionStart |
|
||||
| **Witness** | (none) | Injected via `gt prime` at SessionStart |
|
||||
| **Refinery** | `<rig>/refinery/rig/CLAUDE.md` | On disk (inside worktree) |
|
||||
| **Crew** | (none) | Injected via `gt prime` at SessionStart |
|
||||
| **Polecat** | (none) | Injected via `gt prime` at SessionStart |
|
||||
|
||||
Additionally, each rig has `<rig>/mayor/rig/CLAUDE.md` for the per-rig mayor clone
|
||||
(used for beads operations, not a running agent).
|
||||
|
||||
**Why ephemeral injection?** Writing CLAUDE.md into git clones would:
|
||||
1. Pollute source repos when agents commit/push
|
||||
2. Leak Gas Town internals into project history
|
||||
3. Conflict with project-specific CLAUDE.md files
|
||||
|
||||
The `gt prime` command runs at SessionStart hook and injects context without
|
||||
persisting it to disk.
|
||||
|
||||
### Sparse Checkout (Source Repo Isolation)
|
||||
|
||||
When agents work on source repositories that have their own Claude Code configuration,
|
||||
Gas Town uses git sparse checkout to exclude all context files:
|
||||
|
||||
```bash
|
||||
# Automatically configured for worktrees - excludes:
|
||||
# - .claude/ : settings, rules, agents, commands
|
||||
# - CLAUDE.md : primary context file
|
||||
# - CLAUDE.local.md: personal context file
|
||||
# - .mcp.json : MCP server configuration
|
||||
git sparse-checkout set --no-cone '/*' '!/.claude/' '!/CLAUDE.md' '!/CLAUDE.local.md' '!/.mcp.json'
|
||||
```
|
||||
|
||||
This ensures agents use Gas Town's context, not the source repo's instructions.
|
||||
|
||||
**Doctor check**: `gt doctor` verifies sparse checkout is configured correctly.
|
||||
Run `gt doctor --fix` to update legacy configurations missing the newer patterns.
|
||||
|
||||
### Settings Inheritance
|
||||
|
||||
Claude Code's settings search order (first match wins):
|
||||
|
||||
1. `.claude/settings.json` in current working directory
|
||||
2. `.claude/settings.json` in parent directories (traversing up)
|
||||
3. `~/.claude/settings.json` (user global settings)
|
||||
|
||||
Gas Town places settings at each agent's working directory root, so agents
|
||||
find their role-specific settings before reaching any parent or global config.
|
||||
|
||||
### Settings Templates
|
||||
|
||||
Gas Town uses two settings templates based on role type:
|
||||
|
||||
| Type | Roles | Key Difference |
|
||||
|------|-------|----------------|
|
||||
| **Interactive** | Mayor, Crew | Mail injected on `UserPromptSubmit` hook |
|
||||
| **Autonomous** | Polecat, Witness, Refinery, Deacon | Mail injected on `SessionStart` hook |
|
||||
|
||||
Autonomous agents may start without user input, so they need mail checked
|
||||
at session start. Interactive agents wait for user prompts.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Agent using wrong settings | Check `gt doctor`, verify sparse checkout |
|
||||
| Settings not found | Ensure `.claude/settings.json` exists at role home |
|
||||
| Source repo settings leaking | Run `gt doctor --fix` to configure sparse checkout |
|
||||
| Mayor settings affecting polecats | Mayor should run in `mayor/`, not town root |
|
||||
|
||||
## CLI Reference
|
||||
|
||||
### Town Management
|
||||
@@ -215,6 +346,69 @@ gt doctor # Health check
|
||||
gt doctor --fix # Auto-repair
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# Agent management
|
||||
gt config agent list [--json] # List all agents (built-in + custom)
|
||||
gt config agent get <name> # Show agent configuration
|
||||
gt config agent set <name> <cmd> # Create or update custom agent
|
||||
gt config agent remove <name> # Remove custom agent (built-ins protected)
|
||||
|
||||
# Default agent
|
||||
gt config default-agent [name] # Get or set town default agent
|
||||
```
|
||||
|
||||
**Built-in agents**: `claude`, `gemini`, `codex`, `cursor`, `auggie`, `amp`
|
||||
|
||||
**Custom agents**: Define per-town via CLI or JSON:
|
||||
```bash
|
||||
gt config agent set claude-glm "claude-glm --model glm-4"
|
||||
gt config agent set claude "claude-opus" # Override built-in
|
||||
gt config default-agent claude-glm # Set default
|
||||
```
|
||||
|
||||
**Advanced agent config** (`settings/agents.json`):
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"agents": {
|
||||
"opencode": {
|
||||
"command": "opencode",
|
||||
"args": [],
|
||||
"resume_flag": "--session",
|
||||
"resume_style": "flag",
|
||||
"non_interactive": {
|
||||
"subcommand": "run",
|
||||
"output_flag": "--format json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rig-level agents** (`<rig>/settings/config.json`):
|
||||
```json
|
||||
{
|
||||
"type": "rig-settings",
|
||||
"version": 1,
|
||||
"agent": "opencode",
|
||||
"agents": {
|
||||
"opencode": {
|
||||
"command": "opencode",
|
||||
"args": ["--session"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Agent resolution order**: rig-level → town-level → built-in presets.
|
||||
|
||||
For OpenCode autonomous mode, set env var in your shell profile:
|
||||
```bash
|
||||
export OPENCODE_PERMISSION='{"*":"allow"}'
|
||||
```
|
||||
|
||||
### Rig Management
|
||||
|
||||
```bash
|
||||
@@ -242,12 +436,19 @@ Note: "Swarm" is ephemeral (workers on a convoy's issues). See [Convoys](convoy.
|
||||
# Standard workflow: convoy first, then sling
|
||||
gt convoy create "Feature X" gt-abc gt-def
|
||||
gt sling gt-abc <rig> # Assign to polecat
|
||||
gt sling gt-def <rig> --molecule=<proto> # With workflow template
|
||||
gt sling gt-abc <rig> --agent codex # Override runtime for this sling/spawn
|
||||
gt sling <proto> --on gt-def <rig> # With workflow template
|
||||
|
||||
# Quick sling (auto-creates convoy)
|
||||
gt sling <bead> <rig> # Auto-convoy for dashboard visibility
|
||||
```
|
||||
|
||||
Agent overrides:
|
||||
|
||||
- `gt start --agent <alias>` overrides the Mayor/Deacon runtime for this launch.
|
||||
- `gt mayor start|attach|restart --agent <alias>` and `gt deacon start|attach|restart --agent <alias>` do the same.
|
||||
- `gt start crew <name> --agent <alias>` and `gt crew at <name> --agent <alias>` override the crew worker runtime.
|
||||
|
||||
### Communication
|
||||
|
||||
```bash
|
||||
@@ -323,7 +524,7 @@ Deacon, Witness, and Refinery run continuous patrol loops using wisps:
|
||||
|-------|-----------------|----------------|
|
||||
| **Deacon** | `mol-deacon-patrol` | Agent lifecycle, plugin execution, health checks |
|
||||
| **Witness** | `mol-witness-patrol` | Monitor polecats, nudge stuck workers |
|
||||
| **Refinery** | `mol-refinery-patrol` | Process merge queue, review PRs |
|
||||
| **Refinery** | `mol-refinery-patrol` | Process merge queue, review MRs |
|
||||
|
||||
```
|
||||
1. bd mol wisp mol-<role>-patrol
|
||||
|
||||
220
docs/reviews/infrastructure-review.md
Normal file
220
docs/reviews/infrastructure-review.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Infrastructure & Utilities Code Review
|
||||
|
||||
**Review ID**: gt-a02fj.8
|
||||
**Date**: 2026-01-04
|
||||
**Reviewer**: gastown/polecats/interceptor (polecat gus)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Reviewed 14 infrastructure packages for dead code, missing abstractions, performance concerns, and error handling consistency. Found significant cleanup opportunities totaling ~44% dead code in constants package and an entire unused package (keepalive).
|
||||
|
||||
---
|
||||
|
||||
## 1. Dead Code Inventory
|
||||
|
||||
### Critical: Entire Package Unused
|
||||
|
||||
| Package | Status | Recommendation |
|
||||
|---------|--------|----------------|
|
||||
| `internal/keepalive/` | 100% unused | **DELETE ENTIRE PACKAGE** |
|
||||
|
||||
The keepalive package (5 functions) was removed from the codebase on Dec 30, 2025 as part of the shift to feed-based activation. No imports exist anywhere.
|
||||
|
||||
### High Priority: Functions to Remove
|
||||
|
||||
| Package | Function | Location | Notes |
|
||||
|---------|----------|----------|-------|
|
||||
| `config` | `NewExampleAgentRegistry()` | agents.go:361-381 | Zero usage in codebase |
|
||||
| `constants` | `DirMayor`, `DirPolecats`, `DirCrew`, etc. | constants.go:32-59 | 9 unused directory constants |
|
||||
| `constants` | `FileRigsJSON`, `FileTownJSON`, etc. | constants.go:62-74 | 4 unused file constants |
|
||||
| `constants` | `BranchMain`, `BranchBeadsSync`, etc. | constants.go:77-89 | 4 unused branch constants |
|
||||
| `constants` | `RigBeadsPath()`, `RigPolecatsPath()`, etc. | constants.go | 5 unused path helper functions |
|
||||
| `doctor` | `itoa()` | daemon_check.go:93-111 | Duplicate of `strconv.Itoa()` |
|
||||
| `lock` | `DetectCollisions()` | lock.go:367-402 | Superseded by doctor checks |
|
||||
| `events` | `BootPayload()` | events.go:186-191 | Never called |
|
||||
| `events` | `TypePatrolStarted`, `TypeSessionEnd` | events.go:50,54 | Never emitted |
|
||||
| `events` | `VisibilityBoth` | events.go:32 | Never set |
|
||||
| `boot` | `DeaconDir()` | boot.go:235-237 | Exported but never called |
|
||||
| `dog` | `IdleCount()`, `WorkingCount()` | manager.go:532-562 | Inlined in callers |
|
||||
|
||||
### Medium Priority: Duplicate Definitions
|
||||
|
||||
| Package | Item | Duplicate Location | Action |
|
||||
|---------|------|-------------------|--------|
|
||||
| `constants` | `RigSettingsPath()` | Also in config/loader.go:673 | Remove from constants |
|
||||
| `util` | Atomic write pattern | Also in mrqueue/, wisp/ | Consolidate to util |
|
||||
| `doctor` | `findRigs()` | 3 identical implementations | Extract shared helper |
|
||||
|
||||
---
|
||||
|
||||
## 2. Utility Consolidation Plan
|
||||
|
||||
### Pattern: Atomic Write (Priority: HIGH)
|
||||
|
||||
**Current state**: Duplicated in 3+ locations
|
||||
- `util/atomic.go` (canonical)
|
||||
- `mrqueue/mrqueue.go` (duplicate)
|
||||
- `wisp/io.go` (duplicate)
|
||||
- `polecat/pending.go` (NON-ATOMIC - bug!)
|
||||
|
||||
**Action**:
|
||||
1. Fix `polecat/pending.go:SavePending()` to use `util.AtomicWriteJSON`
|
||||
2. Replace inline atomic writes in mrqueue and wisp with util calls
|
||||
|
||||
### Pattern: Rig Discovery (Priority: HIGH)
|
||||
|
||||
**Current state**: 7+ implementations scattered across doctor package
|
||||
- `BranchCheck.findPersistentRoleDirs()`
|
||||
- `OrphanSessionCheck.getValidRigs()`
|
||||
- `PatrolMoleculesExistCheck.discoverRigs()`
|
||||
- `config_check.go.findAllRigs()`
|
||||
- Multiple `findCrewDirs()` implementations
|
||||
|
||||
**Action**: Create `internal/workspace/discovery.go`:
|
||||
```go
|
||||
type RigDiscovery struct { ... }
|
||||
func (d *RigDiscovery) FindAllRigs() []string
|
||||
func (d *RigDiscovery) FindCrewDirs(rig string) []string
|
||||
func (d *RigDiscovery) FindPolecatDirs(rig string) []string
|
||||
```
|
||||
|
||||
### Pattern: Clone Validation (Priority: MEDIUM)
|
||||
|
||||
**Current state**: Duplicate logic in doctor checks
|
||||
- `rig_check.go`: Validates .git, runs git status
|
||||
- `branch_check.go`: Similar traversal logic
|
||||
|
||||
**Action**: Create `internal/workspace/clone.go`:
|
||||
```go
|
||||
type CloneValidator struct { ... }
|
||||
func (v *CloneValidator) ValidateClone(path string) error
|
||||
func (v *CloneValidator) GetCloneInfo(path string) (*CloneInfo, error)
|
||||
```
|
||||
|
||||
### Pattern: Tmux Session Handling (Priority: MEDIUM)
|
||||
|
||||
**Current state**: Fragmented across lock, doctor, daemon
|
||||
- `lock/lock.go`: `getActiveTmuxSessions()`
|
||||
- `doctor/identity_check.go`: Similar logic
|
||||
- `cmd/agents.go`: Uses `tmux.NewTmux()`
|
||||
|
||||
**Action**: Consolidate into `internal/tmux/sessions.go`
|
||||
|
||||
### Pattern: Load/Validate Config Files (Priority: LOW)
|
||||
|
||||
**Current state**: 8 near-identical Load* functions in config/loader.go
|
||||
- `LoadTownConfig`, `LoadRigsConfig`, `LoadRigConfig`, etc.
|
||||
|
||||
**Action**: Create generic loader using Go generics:
|
||||
```go
|
||||
func loadConfigFile[T Validator](path string) (*T, error)
|
||||
```
|
||||
|
||||
### Pattern: Math Utilities (Priority: LOW)
|
||||
|
||||
**Current state**: `min()`, `max()`, `min3()`, `abs()` in suggest/suggest.go
|
||||
|
||||
**Action**: If needed elsewhere, move to `internal/util/math.go`
|
||||
|
||||
---
|
||||
|
||||
## 3. Performance Concerns
|
||||
|
||||
### Critical: File I/O Per-Event
|
||||
|
||||
| Package | Issue | Impact | Recommendation |
|
||||
|---------|-------|--------|----------------|
|
||||
| `events` | Opens/closes file for every event | High on busy systems | Batch writes or buffered logger |
|
||||
| `townlog` | Opens/closes file per log entry | Medium | Same as events |
|
||||
| `events` | `workspace.FindFromCwd()` on every Log() | Low-medium | Cache town root |
|
||||
|
||||
### Critical: Process Tree Walking
|
||||
|
||||
| Package | Issue | Impact | Recommendation |
|
||||
|---------|-------|--------|----------------|
|
||||
| `doctor/orphan_check` | `hasCrewAncestor()` calls `ps` in loop | O(n) subprocess calls | Batch gather process info |
|
||||
|
||||
### High: Directory Traversal Inefficiencies
|
||||
|
||||
| Package | Issue | Impact | Recommendation |
|
||||
|---------|-------|--------|----------------|
|
||||
| `doctor/hook_check` | Uses `exec.Command("find")` | Subprocess overhead | Use `filepath.Walk` |
|
||||
| `lock` | `FindAllLocks()` - unbounded Walk | Scales poorly | Add depth limits |
|
||||
| `townlog` | `TailEvents()` reads entire file | Memory for large logs | Implement true tail |
|
||||
|
||||
### Medium: Redundant Operations
|
||||
|
||||
| Package | Issue | Recommendation |
|
||||
|---------|-------|----------------|
|
||||
| `dog` | `List()` + iterate = double work | Provide `CountByState()` |
|
||||
| `dog` | Creates new git.Git per worktree | Cache or batch |
|
||||
| `doctor/rig_check` | Runs git status twice per polecat | Combine operations |
|
||||
| `checkpoint/Capture` | 3 separate git commands | Use combined flags |
|
||||
|
||||
### Low: JSON Formatting Overhead
|
||||
|
||||
| Package | Issue | Recommendation |
|
||||
|---------|-------|----------------|
|
||||
| `lock` | `MarshalIndent()` for lock files | Use `Marshal()` (no indentation needed) |
|
||||
| `townlog` | No compression for old logs | Consider gzip rotation |
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Handling Issues
|
||||
|
||||
### Pattern: Silent Failures
|
||||
|
||||
| Package | Location | Issue | Fix |
|
||||
|---------|----------|-------|-----|
|
||||
| `events` | All callers | 19 instances of `_ = events.LogFeed()` | Standardize: always ignore or always check |
|
||||
| `townlog` | `ParseLogLines()` | Silently skips malformed lines | Log warnings |
|
||||
| `lock` | Lines 91, 180, 194-195 | Silent `_ =` without comments | Document intent |
|
||||
| `checkpoint` | `Capture()` | Returns nil error but git commands fail | Return actual errors |
|
||||
| `deps` | `BeadsUnknown` case | Silently passes | Log warning or fail |
|
||||
|
||||
### Pattern: Inconsistent State Handling
|
||||
|
||||
| Package | Issue | Recommendation |
|
||||
|---------|-------|----------------|
|
||||
| `dog/Get()` | Returns minimal Dog if state missing | Document or error |
|
||||
| `config/GetAccount()` | Returns pointer to loop variable (bug!) | Return by value |
|
||||
| `boot` | `LoadStatus()` returns empty struct if missing | Document behavior |
|
||||
|
||||
### Bug: Missing Role Mapping
|
||||
|
||||
| Package | Issue | Impact |
|
||||
|---------|-------|--------|
|
||||
| `claude` | `RoleTypeFor()` missing `deacon`, `crew` | Wrong settings applied |
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing Gaps
|
||||
|
||||
| Package | Gap | Priority |
|
||||
|---------|-----|----------|
|
||||
| `checkpoint` | No unit tests | HIGH (crash recovery) |
|
||||
| `dog` | 4 tests, major paths untested | HIGH |
|
||||
| `deps` | Minimal failure path testing | MEDIUM |
|
||||
| `claude` | No tests | LOW |
|
||||
|
||||
---
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
| Category | Count | Packages Affected |
|
||||
|----------|-------|-------------------|
|
||||
| **Dead Code Items** | 25+ | config, constants, doctor, lock, events, boot, dog, keepalive |
|
||||
| **Duplicate Patterns** | 6 | util, doctor, config, lock |
|
||||
| **Performance Issues** | 12 | events, townlog, doctor, dog, lock, checkpoint |
|
||||
| **Error Handling Issues** | 15 | events, townlog, lock, checkpoint, deps, claude |
|
||||
| **Testing Gaps** | 4 packages | checkpoint, dog, deps, claude |
|
||||
|
||||
## Recommended Priority
|
||||
|
||||
1. **Delete keepalive package** (entire package unused)
|
||||
2. **Fix claude/RoleTypeFor()** (incorrect behavior)
|
||||
3. **Fix config/GetAccount()** (pointer to stack bug)
|
||||
4. **Fix polecat/pending.go** (non-atomic writes)
|
||||
5. **Delete 21 unused constants** (maintenance burden)
|
||||
6. **Consolidate atomic write pattern** (DRY)
|
||||
7. **Add checkpoint tests** (crash recovery critical)
|
||||
154
docs/test-coverage-review.md
Normal file
154
docs/test-coverage-review.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Test Coverage and Quality Review
|
||||
|
||||
**Reviewed by**: polecat/gus
|
||||
**Date**: 2026-01-04
|
||||
**Issue**: gt-a02fj.9
|
||||
|
||||
## Executive Summary
|
||||
|
||||
- **80 test files** covering **32 out of 42 packages** (76% package coverage)
|
||||
- **631 test functions** with 192 subtests (30% use table-driven pattern)
|
||||
- **10 packages** with **0 test coverage** (2,452 lines)
|
||||
- **1 confirmed flaky test** candidate
|
||||
- Test quality is generally good with moderate mocking
|
||||
|
||||
---
|
||||
|
||||
## Coverage Gap Inventory
|
||||
|
||||
### Packages Without Tests (Priority Order)
|
||||
|
||||
| Priority | Package | Lines | Risk | Notes |
|
||||
|----------|---------|-------|------|-------|
|
||||
| **P0** | `internal/lock` | 402 | **CRITICAL** | Multi-agent lock management. Bugs cause worker collisions. Already has `execCommand` mockable for testing. |
|
||||
| **P1** | `internal/events` | 295 | HIGH | Event bus for audit trail. Mutex-protected writes. Core observability. |
|
||||
| **P1** | `internal/boot` | 242 | HIGH | Boot watchdog lifecycle. Spawns tmux sessions. |
|
||||
| **P1** | `internal/checkpoint` | 216 | HIGH | Session crash recovery. Critical for polecat continuity. |
|
||||
| **P2** | `internal/tui/convoy` | 601 | MEDIUM | TUI component. Harder to test but user-facing. |
|
||||
| **P2** | `internal/constants` | 221 | LOW | Mostly configuration constants. Low behavioral risk. |
|
||||
| **P3** | `internal/style` | 331 | LOW | Output formatting. Visual only. |
|
||||
| **P3** | `internal/claude` | 80 | LOW | Claude settings parsing. |
|
||||
| **P3** | `internal/wisp` | 52 | LOW | Ephemeral molecule I/O. Small surface. |
|
||||
| **P4** | `cmd/gt` | 12 | TRIVIAL | Main entry point. Minimal code. |
|
||||
|
||||
**Total untested lines**: 2,452
|
||||
|
||||
---
|
||||
|
||||
## Flaky Test Candidates
|
||||
|
||||
### Confirmed: `internal/feed/curator_test.go`
|
||||
|
||||
**Issue**: Uses `time.Sleep()` for synchronization (lines 59, 71, 119, 138)
|
||||
|
||||
```go
|
||||
// Give curator time to start
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
...
|
||||
// Wait for processing
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
```
|
||||
|
||||
**Risk**: Flaky under load, CI delays, or slow machines.
|
||||
|
||||
**Fix**: Replace with channel-based synchronization or polling with timeout:
|
||||
```go
|
||||
// Wait for condition with timeout
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if conditionMet() {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Quality Analysis
|
||||
|
||||
### Strengths
|
||||
|
||||
1. **Table-driven tests**: 30% of tests use `t.Run()` (192/631)
|
||||
2. **Good isolation**: Only 2 package-level test variables
|
||||
3. **Dedicated integration tests**: 15 files with explicit integration/e2e naming
|
||||
4. **Error handling**: 316 uses of `if err != nil` in tests
|
||||
5. **No random data**: No `rand.` usage in tests (deterministic)
|
||||
6. **Environment safety**: Uses `t.Setenv()` for clean env var handling
|
||||
|
||||
### Areas for Improvement
|
||||
|
||||
1. **`testing.Short()`**: Only 1 usage. Long-running tests should check this.
|
||||
2. **External dependencies**: 26 tests skip when `bd` or `tmux` unavailable - consider mocking more.
|
||||
3. **time.Sleep usage**: Found in `curator_test.go` - should be eliminated.
|
||||
|
||||
---
|
||||
|
||||
## Test Smells (Minor)
|
||||
|
||||
| Smell | Location | Severity | Notes |
|
||||
|-------|----------|----------|-------|
|
||||
| Sleep-based sync | `feed/curator_test.go` | HIGH | See flaky section |
|
||||
| External dep skips | Multiple files | LOW | Reasonable for integration tests |
|
||||
| Skip-heavy file | `tmux/tmux_test.go` | LOW | Acceptable - tmux not always available |
|
||||
|
||||
---
|
||||
|
||||
## Priority List for New Tests
|
||||
|
||||
### Immediate (P0)
|
||||
|
||||
1. **`internal/lock`** - Critical path
|
||||
- Test `Acquire()` with stale lock cleanup
|
||||
- Test `Check()` with live/dead PIDs
|
||||
- Test `CleanStaleLocks()` with mock tmux sessions
|
||||
- Test `DetectCollisions()`
|
||||
- Test concurrent lock acquisition (race detection)
|
||||
|
||||
### High Priority (P1)
|
||||
|
||||
2. **`internal/events`**
|
||||
- Test `Log()` file creation and append
|
||||
- Test `write()` mutex behavior
|
||||
- Test payload helpers
|
||||
- Test graceful handling when not in workspace
|
||||
|
||||
3. **`internal/boot`**
|
||||
- Test `IsRunning()` with stale markers
|
||||
- Test `AcquireLock()` / `ReleaseLock()` cycle
|
||||
- Test `SaveStatus()` / `LoadStatus()` round-trip
|
||||
- Test degraded mode path
|
||||
|
||||
4. **`internal/checkpoint`**
|
||||
- Test `Read()` / `Write()` round-trip
|
||||
- Test `Capture()` git state extraction
|
||||
- Test `IsStale()` with various durations
|
||||
- Test `Summary()` output
|
||||
|
||||
### Medium Priority (P2)
|
||||
|
||||
5. **`internal/tui/convoy`** - Consider golden file tests for view output
|
||||
6. **`internal/constants`** - Test any validation logic
|
||||
|
||||
---
|
||||
|
||||
## Missing Test Types
|
||||
|
||||
| Type | Current State | Recommendation |
|
||||
|------|--------------|----------------|
|
||||
| Unit tests | Good coverage where present | Add for P0-P1 packages |
|
||||
| Integration tests | 15 dedicated files | Adequate |
|
||||
| E2E tests | `browser_e2e_test.go` | Consider more CLI E2E |
|
||||
| Fuzz tests | None | Consider for parsers (`formula/parser.go`) |
|
||||
| Benchmark tests | None visible | Add for hot paths (`lock`, `events`) |
|
||||
|
||||
---
|
||||
|
||||
## Actionable Next Steps
|
||||
|
||||
1. **Fix flaky test**: Refactor `feed/curator_test.go` to use channels/polling
|
||||
2. **Add lock tests**: Highest priority - bugs here break multi-agent
|
||||
3. **Add events tests**: Core observability must be tested
|
||||
4. **Add checkpoint tests**: Session recovery is critical path
|
||||
5. **Run with race detector**: `go test -race ./...` to catch data races
|
||||
6. **Consider `-short` flag**: Add `testing.Short()` checks to slow tests
|
||||
@@ -26,7 +26,7 @@ These roles manage the Gas Town system itself:
|
||||
|
||||
| Role | Description | Lifecycle |
|
||||
|------|-------------|-----------|
|
||||
| **Mayor** | Global coordinator at town root | Singleton, persistent |
|
||||
| **Mayor** | Global coordinator at mayor/ | Singleton, persistent |
|
||||
| **Deacon** | Background supervisor daemon ([watchdog chain](watchdog-chain.md)) | Singleton, persistent |
|
||||
| **Witness** | Per-rig polecat lifecycle manager | One per rig, persistent |
|
||||
| **Refinery** | Per-rig merge queue processor | One per rig, persistent |
|
||||
|
||||
@@ -66,10 +66,10 @@ The daemon could directly monitor agents without AI, but:
|
||||
| Agent | Session Name | Location | Lifecycle |
|
||||
|-------|--------------|----------|-----------|
|
||||
| Daemon | (Go process) | `~/gt/daemon/` | Persistent, auto-restart |
|
||||
| Boot | `gt-deacon-boot` | `~/gt/deacon/dogs/boot/` | Ephemeral, fresh each tick |
|
||||
| Deacon | `gt-deacon` | `~/gt/deacon/` | Long-running, handoff loop |
|
||||
| Boot | `gt-boot` | `~/gt/deacon/dogs/boot/` | Ephemeral, fresh each tick |
|
||||
| Deacon | `hq-deacon` | `~/gt/deacon/` | Long-running, handoff loop |
|
||||
|
||||
**Critical**: Boot runs in `gt-deacon-boot`, NOT `gt-deacon`. This prevents Boot
|
||||
**Critical**: Boot runs in `gt-boot`, NOT `hq-deacon`. This prevents Boot
|
||||
from conflicting with a running Deacon session.
|
||||
|
||||
## Heartbeat Mechanics
|
||||
@@ -82,11 +82,11 @@ The daemon runs a heartbeat tick every 3 minutes:
|
||||
func (d *Daemon) heartbeatTick() {
|
||||
d.ensureBootRunning() // 1. Spawn Boot for triage
|
||||
d.checkDeaconHeartbeat() // 2. Belt-and-suspenders fallback
|
||||
d.ensureWitnessesRunning() // 3. Witness health
|
||||
d.triggerPendingSpawns() // 4. Bootstrap polecats
|
||||
d.processLifecycleRequests() // 5. Cycle/restart requests
|
||||
d.checkStaleAgents() // 6. Timeout detection
|
||||
// ... more checks
|
||||
d.ensureWitnessesRunning() // 3. Witness health (checks tmux directly)
|
||||
d.ensureRefineriesRunning() // 4. Refinery health (checks tmux directly)
|
||||
d.triggerPendingSpawns() // 5. Bootstrap polecats
|
||||
d.processLifecycleRequests() // 6. Cycle/restart requests
|
||||
// Agent state derived from tmux, not recorded in beads (gt-zecmc)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -190,7 +190,7 @@ Multiple layers ensure recovery:
|
||||
|
||||
1. **Boot triage** - Intelligent observation, first line
|
||||
2. **Daemon checkDeaconHeartbeat()** - Belt-and-suspenders if Boot fails
|
||||
3. **Daemon checkStaleAgents()** - Timeout-based detection
|
||||
3. **Tmux-based discovery** - Daemon checks tmux sessions directly (no bead state)
|
||||
4. **Human escalation** - Mail to overseer for unrecoverable states
|
||||
|
||||
## State Files
|
||||
@@ -227,21 +227,23 @@ gt deacon health-check
|
||||
|
||||
### Boot Spawns in Wrong Session
|
||||
|
||||
**Symptom**: Boot runs in `gt-deacon` instead of `gt-deacon-boot`
|
||||
**Symptom**: Boot runs in `hq-deacon` instead of `gt-boot`
|
||||
**Cause**: Session name confusion in spawn code
|
||||
**Fix**: Ensure `gt boot triage` specifies `--session=gt-deacon-boot`
|
||||
**Fix**: Ensure `gt boot triage` specifies `--session=gt-boot`
|
||||
|
||||
### Zombie Sessions Block Restart
|
||||
|
||||
**Symptom**: tmux session exists but Claude is dead
|
||||
**Cause**: Daemon checks session existence, not process health
|
||||
**Fix**: Kill zombie sessions before recreating: `gt session kill gt-deacon`
|
||||
**Fix**: Kill zombie sessions before recreating: `gt session kill hq-deacon`
|
||||
|
||||
### Status Shows Wrong State
|
||||
|
||||
**Symptom**: `gt status` shows "stopped" for running agents
|
||||
**Cause**: Bead state and tmux state diverged
|
||||
**Fix**: Reconcile with `gt sync-status` or restart agent
|
||||
**Symptom**: `gt status` shows wrong state for agents
|
||||
**Cause**: Previously bead state and tmux state could diverge
|
||||
**Fix**: As of gt-zecmc, status derives state from tmux directly (no bead state for
|
||||
observable conditions like running/stopped). Non-observable states (stuck, awaiting-gate)
|
||||
are still stored in beads.
|
||||
|
||||
## Design Decision: Keep Separation
|
||||
|
||||
@@ -250,15 +252,15 @@ The issue [gt-1847v] considered three options:
|
||||
### Option A: Keep Boot/Deacon Separation (CHOSEN)
|
||||
|
||||
- Boot is ephemeral, spawns fresh each heartbeat
|
||||
- Boot runs in `gt-deacon-boot`, exits after triage
|
||||
- Deacon runs in `gt-deacon`, continuous patrol
|
||||
- Boot runs in `gt-boot`, exits after triage
|
||||
- Deacon runs in `hq-deacon`, continuous patrol
|
||||
- Clear session boundaries, clear lifecycle
|
||||
|
||||
**Verdict**: This is the correct design. The implementation needs fixing, not the architecture.
|
||||
|
||||
### Option B: Merge Boot into Deacon (Rejected)
|
||||
|
||||
- Single `gt-deacon` session handles everything
|
||||
- Single `hq-deacon` session handles everything
|
||||
- Deacon checks "should I be awake?" internally
|
||||
|
||||
**Why rejected**:
|
||||
@@ -284,7 +286,7 @@ The separation is correct; these bugs need fixing:
|
||||
|
||||
1. **Session confusion** (gt-sgzsb): Boot spawns in wrong session
|
||||
2. **Zombie blocking** (gt-j1i0r): Daemon can't kill zombie sessions
|
||||
3. **Status mismatch** (gt-doih4): Bead vs tmux state divergence
|
||||
3. ~~**Status mismatch** (gt-doih4): Bead vs tmux state divergence~~ → FIXED in gt-zecmc
|
||||
4. **Ensure semantics** (gt-ekc5u): Start should kill zombies first
|
||||
|
||||
## Summary
|
||||
|
||||
495
dog-pool-architecture.md
Normal file
495
dog-pool-architecture.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# Dog Pool Architecture for Concurrent Shutdown Dances
|
||||
|
||||
> Design document for gt-fsld8
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Boot needs to run multiple shutdown-dance molecules concurrently when multiple death
|
||||
warrants are issued. The current hook design only allows one molecule per agent.
|
||||
|
||||
Example scenario:
|
||||
- Warrant 1: Kill stuck polecat Toast (60s into interrogation)
|
||||
- Warrant 2: Kill stuck polecat Shadow (just started)
|
||||
- Warrant 3: Kill stuck witness (120s into interrogation)
|
||||
|
||||
All three need concurrent tracking, independent timeouts, and separate outcomes.
|
||||
|
||||
## Design Decision: Lightweight State Machines
|
||||
|
||||
After analyzing the options, the shutdown-dance does NOT need Claude sessions.
|
||||
The dance is a deterministic state machine:
|
||||
|
||||
```
|
||||
WARRANT -> INTERROGATE -> EVALUATE -> PARDON|EXECUTE
|
||||
```
|
||||
|
||||
Each step is mechanical:
|
||||
1. Send a tmux message (no LLM needed)
|
||||
2. Wait for timeout or response (timer)
|
||||
3. Check tmux output for ALIVE keyword (string match)
|
||||
4. Repeat or terminate
|
||||
|
||||
**Decision**: Dogs are lightweight Go routines, not Claude sessions.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ BOOT │
|
||||
│ (Claude session in tmux) │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Dog Manager │ │
|
||||
│ │ │ │
|
||||
│ │ Pool: [Dog1, Dog2, Dog3, ...] (goroutines + state files) │ │
|
||||
│ │ │ │
|
||||
│ │ allocate() → Dog │ │
|
||||
│ │ release(Dog) │ │
|
||||
│ │ status() → []DogStatus │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Boot's job: │
|
||||
│ - Watch for warrants (file or event) │
|
||||
│ - Allocate dog from pool │
|
||||
│ - Monitor dog progress │
|
||||
│ - Handle dog completion/failure │
|
||||
│ - Report results │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Dog Structure
|
||||
|
||||
```go
|
||||
// Dog represents a shutdown-dance executor
|
||||
type Dog struct {
|
||||
ID string // Unique ID (e.g., "dog-1704567890123")
|
||||
Warrant *Warrant // The death warrant being processed
|
||||
State ShutdownDanceState
|
||||
Attempt int // Current interrogation attempt (1-3)
|
||||
StartedAt time.Time
|
||||
StateFile string // Persistent state: ~/gt/deacon/dogs/active/<id>.json
|
||||
}
|
||||
|
||||
type ShutdownDanceState string
|
||||
|
||||
const (
|
||||
StateIdle ShutdownDanceState = "idle"
|
||||
StateInterrogating ShutdownDanceState = "interrogating" // Sent message, waiting
|
||||
StateEvaluating ShutdownDanceState = "evaluating" // Checking response
|
||||
StatePardoned ShutdownDanceState = "pardoned" // Session responded
|
||||
StateExecuting ShutdownDanceState = "executing" // Killing session
|
||||
StateComplete ShutdownDanceState = "complete" // Done, ready for cleanup
|
||||
StateFailed ShutdownDanceState = "failed" // Dog crashed/errored
|
||||
)
|
||||
|
||||
type Warrant struct {
|
||||
ID string // Bead ID for the warrant
|
||||
Target string // Session to interrogate (e.g., "gt-gastown-Toast")
|
||||
Reason string // Why warrant was issued
|
||||
Requester string // Who filed the warrant
|
||||
FiledAt time.Time
|
||||
}
|
||||
```
|
||||
|
||||
## Pool Design
|
||||
|
||||
### Fixed Pool Size
|
||||
|
||||
**Decision**: Fixed pool of 5 dogs, configurable via environment.
|
||||
|
||||
Rationale:
|
||||
- Dynamic sizing adds complexity without clear benefit
|
||||
- 5 concurrent shutdown dances handles worst-case scenarios
|
||||
- If pool exhausted, warrants queue (better than infinite dog spawning)
|
||||
- Memory footprint is negligible (goroutines + small state files)
|
||||
|
||||
```go
|
||||
const (
|
||||
DefaultPoolSize = 5
|
||||
MaxPoolSize = 20
|
||||
)
|
||||
|
||||
type DogPool struct {
|
||||
mu sync.Mutex
|
||||
dogs []*Dog // All dogs in pool
|
||||
idle chan *Dog // Channel of available dogs
|
||||
active map[string]*Dog // ID -> Dog for active dogs
|
||||
stateDir string // ~/gt/deacon/dogs/active/
|
||||
}
|
||||
|
||||
func (p *DogPool) Allocate(warrant *Warrant) (*Dog, error) {
|
||||
select {
|
||||
case dog := <-p.idle:
|
||||
dog.Warrant = warrant
|
||||
dog.State = StateInterrogating
|
||||
dog.Attempt = 1
|
||||
dog.StartedAt = time.Now()
|
||||
p.active[dog.ID] = dog
|
||||
return dog, nil
|
||||
default:
|
||||
return nil, ErrPoolExhausted
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DogPool) Release(dog *Dog) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
delete(p.active, dog.ID)
|
||||
dog.Reset()
|
||||
p.idle <- dog
|
||||
}
|
||||
```
|
||||
|
||||
### Why Not Dynamic Pool?
|
||||
|
||||
Considered but rejected:
|
||||
- Adding dogs on demand increases complexity
|
||||
- No clear benefit - warrants rarely exceed 5 concurrent
|
||||
- If needed, raise DefaultPoolSize
|
||||
- Simpler to reason about fixed resources
|
||||
|
||||
## Communication: State Files + Events
|
||||
|
||||
### State Persistence
|
||||
|
||||
Each active dog writes state to `~/gt/deacon/dogs/active/<id>.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "dog-1704567890123",
|
||||
"warrant": {
|
||||
"id": "gt-abc123",
|
||||
"target": "gt-gastown-Toast",
|
||||
"reason": "no_response_health_check",
|
||||
"requester": "deacon",
|
||||
"filed_at": "2026-01-07T20:15:00Z"
|
||||
},
|
||||
"state": "interrogating",
|
||||
"attempt": 2,
|
||||
"started_at": "2026-01-07T20:15:00Z",
|
||||
"last_message_at": "2026-01-07T20:16:00Z",
|
||||
"next_timeout": "2026-01-07T20:18:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Boot Monitoring
|
||||
|
||||
Boot monitors dogs via:
|
||||
1. **Polling**: `gt dog status --active` every tick
|
||||
2. **Completion files**: Dogs write `<id>.done` when complete
|
||||
|
||||
```go
|
||||
type DogResult struct {
|
||||
DogID string
|
||||
Warrant *Warrant
|
||||
Outcome DogOutcome // pardoned | executed | failed
|
||||
Duration time.Duration
|
||||
Details string
|
||||
}
|
||||
|
||||
type DogOutcome string
|
||||
|
||||
const (
|
||||
OutcomePardoned DogOutcome = "pardoned" // Session responded
|
||||
OutcomeExecuted DogOutcome = "executed" // Session killed
|
||||
OutcomeFailed DogOutcome = "failed" // Dog crashed
|
||||
)
|
||||
```
|
||||
|
||||
### Why Not Mail?
|
||||
|
||||
Considered but rejected for dog<->boot communication:
|
||||
- Mail is async, poll-based - adds latency
|
||||
- State files are simpler for local coordination
|
||||
- Dogs don't need complex inter-agent communication
|
||||
- Keep mail for external coordination (Witness, Mayor)
|
||||
|
||||
## Shutdown Dance State Machine
|
||||
|
||||
Each dog executes this state machine:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────────────────────────┐ │
|
||||
│ INTERROGATING │ │
|
||||
│ │ │
|
||||
│ 1. Send health check │ │
|
||||
│ 2. Start timeout timer │ │
|
||||
└───────────┬───────────────┘ │
|
||||
│ │
|
||||
│ timeout or response │
|
||||
▼ │
|
||||
┌───────────────────────────┐ │
|
||||
│ EVALUATING │ │
|
||||
│ │ │
|
||||
│ Check tmux output for │ │
|
||||
│ ALIVE keyword │ │
|
||||
└───────────┬───────────────┘ │
|
||||
│ │
|
||||
┌───────┴───────┐ │
|
||||
│ │ │
|
||||
▼ ▼ │
|
||||
[ALIVE found] [No ALIVE] │
|
||||
│ │ │
|
||||
│ │ attempt < 3? │
|
||||
│ ├──────────────────────────────────→─┘
|
||||
│ │ yes: attempt++, longer timeout
|
||||
│ │
|
||||
│ │ no: attempt == 3
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────────┐
|
||||
│ PARDONED│ │ EXECUTING │
|
||||
│ │ │ │
|
||||
│ Cancel │ │ Kill tmux │
|
||||
│ warrant │ │ session │
|
||||
└────┬────┘ └──────┬──────┘
|
||||
│ │
|
||||
└────────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ COMPLETE │
|
||||
│ │
|
||||
│ Write result │
|
||||
│ Release dog │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
### Timeout Gates
|
||||
|
||||
| Attempt | Timeout | Cumulative Wait |
|
||||
|---------|---------|-----------------|
|
||||
| 1 | 60s | 60s |
|
||||
| 2 | 120s | 180s (3 min) |
|
||||
| 3 | 240s | 420s (7 min) |
|
||||
|
||||
### Health Check Message
|
||||
|
||||
```
|
||||
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within {timeout}s or face termination.
|
||||
Warrant reason: {reason}
|
||||
Filed by: {requester}
|
||||
Attempt: {attempt}/3
|
||||
```
|
||||
|
||||
### Response Detection
|
||||
|
||||
```go
|
||||
func (d *Dog) CheckForResponse() bool {
|
||||
tm := tmux.NewTmux()
|
||||
output, err := tm.CapturePane(d.Warrant.Target, 50) // Last 50 lines
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Any output after our health check counts as alive
|
||||
// Specifically look for ALIVE keyword for explicit response
|
||||
return strings.Contains(output, "ALIVE")
|
||||
}
|
||||
```
|
||||
|
||||
## Dog Implementation
|
||||
|
||||
### Not Reusing Polecat Infrastructure
|
||||
|
||||
**Decision**: Dogs do NOT reuse polecat infrastructure.
|
||||
|
||||
Rationale:
|
||||
- Polecats are Claude sessions with molecules, hooks, sandboxes
|
||||
- Dogs are simple state machine executors
|
||||
- Polecats have 3-layer lifecycle (session/sandbox/slot)
|
||||
- Dogs have single-layer lifecycle (just state)
|
||||
- Different resource profiles, different management
|
||||
|
||||
What dogs DO share:
|
||||
- tmux utilities for message sending/capture
|
||||
- State file patterns
|
||||
- Pool allocation pattern
|
||||
|
||||
### Dog Execution Loop
|
||||
|
||||
```go
|
||||
func (d *Dog) Run(ctx context.Context) DogResult {
|
||||
d.State = StateInterrogating
|
||||
d.saveState()
|
||||
|
||||
for d.Attempt <= 3 {
|
||||
// Send interrogation message
|
||||
if err := d.sendHealthCheck(); err != nil {
|
||||
return d.fail(err)
|
||||
}
|
||||
|
||||
// Wait for timeout or context cancellation
|
||||
timeout := d.timeoutForAttempt(d.Attempt)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return d.fail(ctx.Err())
|
||||
case <-time.After(timeout):
|
||||
// Timeout reached
|
||||
}
|
||||
|
||||
// Evaluate response
|
||||
d.State = StateEvaluating
|
||||
d.saveState()
|
||||
|
||||
if d.CheckForResponse() {
|
||||
// Session is alive
|
||||
return d.pardon()
|
||||
}
|
||||
|
||||
// No response - try again or execute
|
||||
d.Attempt++
|
||||
if d.Attempt <= 3 {
|
||||
d.State = StateInterrogating
|
||||
d.saveState()
|
||||
}
|
||||
}
|
||||
|
||||
// All attempts exhausted - execute warrant
|
||||
return d.execute()
|
||||
}
|
||||
```
|
||||
|
||||
## Failure Handling
|
||||
|
||||
### Dog Crashes Mid-Dance
|
||||
|
||||
If a dog crashes (Boot process restarts, system crash):
|
||||
|
||||
1. State files persist in `~/gt/deacon/dogs/active/`
|
||||
2. On Boot restart, scan for orphaned state files
|
||||
3. Resume or restart based on state:
|
||||
|
||||
| State | Recovery Action |
|
||||
|------------------|------------------------------------|
|
||||
| interrogating | Restart from current attempt |
|
||||
| evaluating | Check response, continue |
|
||||
| executing | Verify kill, mark complete |
|
||||
| pardoned/complete| Already done, clean up |
|
||||
|
||||
```go
|
||||
func (p *DogPool) RecoverOrphans() error {
|
||||
files, _ := filepath.Glob(p.stateDir + "/*.json")
|
||||
for _, f := range files {
|
||||
state := loadDogState(f)
|
||||
if state.State != StateComplete && state.State != StatePardoned {
|
||||
dog := p.allocateForRecovery(state)
|
||||
go dog.Resume()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Pool Exhaustion
|
||||
|
||||
If all dogs are busy when new warrant arrives:
|
||||
|
||||
```go
|
||||
func (b *Boot) HandleWarrant(warrant *Warrant) error {
|
||||
dog, err := b.pool.Allocate(warrant)
|
||||
if err == ErrPoolExhausted {
|
||||
// Queue the warrant for later processing
|
||||
b.warrantQueue.Push(warrant)
|
||||
b.log("Warrant %s queued (pool exhausted)", warrant.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
result := dog.Run(b.ctx)
|
||||
b.handleResult(result)
|
||||
b.pool.Release(dog)
|
||||
|
||||
// Check queue for pending warrants
|
||||
if next := b.warrantQueue.Pop(); next != nil {
|
||||
b.HandleWarrant(next)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
~/gt/deacon/dogs/
|
||||
├── boot/ # Boot's working directory
|
||||
│ ├── CLAUDE.md # Boot context
|
||||
│ └── .boot-status.json # Boot execution status
|
||||
├── active/ # Active dog state files
|
||||
│ ├── dog-123.json # Dog 1 state
|
||||
│ ├── dog-456.json # Dog 2 state
|
||||
│ └── ...
|
||||
├── completed/ # Completed dance records (for audit)
|
||||
│ ├── dog-789.json # Historical record
|
||||
│ └── ...
|
||||
└── warrants/ # Pending warrant queue
|
||||
├── warrant-abc.json
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Command Interface
|
||||
|
||||
```bash
|
||||
# Pool status
|
||||
gt dog pool status
|
||||
# Output:
|
||||
# Dog Pool: 3/5 active
|
||||
# dog-123: interrogating Toast (attempt 2, 45s remaining)
|
||||
# dog-456: executing Shadow
|
||||
# dog-789: idle
|
||||
|
||||
# Manual dog operations (for debugging)
|
||||
gt dog pool allocate <warrant-id>
|
||||
gt dog pool release <dog-id>
|
||||
|
||||
# View active dances
|
||||
gt dog dances
|
||||
# Output:
|
||||
# Active Shutdown Dances:
|
||||
# dog-123 → Toast: Interrogating (2/3), timeout in 45s
|
||||
# dog-456 → Shadow: Executing warrant
|
||||
|
||||
# View warrant queue
|
||||
gt dog warrants
|
||||
# Output:
|
||||
# Pending Warrants: 2
|
||||
# 1. gt-abc: witness-gastown (stuck_no_progress)
|
||||
# 2. gt-def: polecat-Copper (crash_loop)
|
||||
```
|
||||
|
||||
## Integration with Existing Dogs
|
||||
|
||||
The existing `dog` package (`internal/dog/`) manages Deacon's multi-rig helper dogs.
|
||||
Those are different from shutdown-dance dogs:
|
||||
|
||||
| Aspect | Helper Dogs (existing) | Dance Dogs (new) |
|
||||
|-----------------|-----------------------------|-----------------------------|
|
||||
| Purpose | Cross-rig infrastructure | Shutdown dance execution |
|
||||
| Sessions | Claude sessions | Goroutines (no Claude) |
|
||||
| Worktrees | One per rig | None |
|
||||
| Lifecycle | Long-lived, reusable | Ephemeral per warrant |
|
||||
| State | idle/working | Dance state machine |
|
||||
|
||||
**Recommendation**: Use different package to avoid confusion:
|
||||
- `internal/dog/` - existing helper dogs
|
||||
- `internal/shutdown/` - shutdown dance pool
|
||||
|
||||
## Summary: Answers to Design Questions
|
||||
|
||||
| Question | Answer |
|
||||
|----------|--------|
|
||||
| How many Dogs in pool? | Fixed: 5 (configurable via GT_DOG_POOL_SIZE) |
|
||||
| How do Dogs communicate with Boot? | State files + completion markers |
|
||||
| Are Dogs tmux sessions? | No - goroutines with state machine |
|
||||
| Reuse polecat infrastructure? | No - too heavyweight, different model |
|
||||
| What if Dog dies mid-dance? | State file recovery on Boot restart |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Architecture document for Dog pool
|
||||
- [x] Clear allocation/deallocation protocol
|
||||
- [x] Failure handling for Dog crashes
|
||||
7
go.mod
7
go.mod
@@ -7,6 +7,8 @@ require (
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/go-rod/rod v0.116.2
|
||||
github.com/gofrs/flock v0.13.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
golang.org/x/term v0.38.0
|
||||
@@ -34,5 +36,10 @@ require (
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/got v0.40.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
)
|
||||
|
||||
26
go.sum
26
go.sum
@@ -27,8 +27,14 @@ github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEX
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -47,6 +53,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -54,8 +62,24 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
|
||||
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
|
||||
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
|
||||
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
|
||||
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
|
||||
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
|
||||
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
|
||||
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
||||
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
@@ -68,3 +92,5 @@ golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
76
internal/agent/state.go
Normal file
76
internal/agent/state.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Package agent provides shared types and utilities for Gas Town agents
|
||||
// (witness, refinery, deacon, etc.).
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/util"
|
||||
)
|
||||
|
||||
// State represents an agent's running state.
|
||||
type State string
|
||||
|
||||
const (
|
||||
// StateStopped means the agent is not running.
|
||||
StateStopped State = "stopped"
|
||||
|
||||
// StateRunning means the agent is actively operating.
|
||||
StateRunning State = "running"
|
||||
|
||||
// StatePaused means the agent is paused (not operating but not stopped).
|
||||
StatePaused State = "paused"
|
||||
)
|
||||
|
||||
// StateManager handles loading and saving agent state to disk.
|
||||
// It uses generics to work with any state type.
|
||||
type StateManager[T any] struct {
|
||||
stateFilePath string
|
||||
defaultFactory func() *T
|
||||
}
|
||||
|
||||
// NewStateManager creates a new StateManager for the given state file path.
|
||||
// The defaultFactory function is called when the state file doesn't exist
|
||||
// to create a new state with default values.
|
||||
func NewStateManager[T any](rigPath, stateFileName string, defaultFactory func() *T) *StateManager[T] {
|
||||
return &StateManager[T]{
|
||||
stateFilePath: filepath.Join(rigPath, ".runtime", stateFileName),
|
||||
defaultFactory: defaultFactory,
|
||||
}
|
||||
}
|
||||
|
||||
// StateFile returns the path to the state file.
|
||||
func (m *StateManager[T]) StateFile() string {
|
||||
return m.stateFilePath
|
||||
}
|
||||
|
||||
// Load loads agent state from disk.
|
||||
// If the file doesn't exist, returns a new state created by the default factory.
|
||||
func (m *StateManager[T]) Load() (*T, error) {
|
||||
data, err := os.ReadFile(m.stateFilePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return m.defaultFactory(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var state T
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// Save persists agent state to disk using atomic write.
|
||||
func (m *StateManager[T]) Save(state *T) error {
|
||||
dir := filepath.Dir(m.stateFilePath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.AtomicWriteJSON(m.stateFilePath, state)
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
@@ -71,15 +73,186 @@ func ResolveBeadsDir(workDir string) string {
|
||||
return beadsDir
|
||||
}
|
||||
|
||||
// Detect redirect chains: check if resolved path also has a redirect
|
||||
resolvedRedirect := filepath.Join(resolved, "redirect")
|
||||
if _, err := os.Stat(resolvedRedirect); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: redirect chain detected: %s -> %s (which also has a redirect)\n", beadsDir, resolved)
|
||||
// Don't follow chains - just return the first resolved path
|
||||
// The target's redirect is likely errant and should be removed
|
||||
// Follow redirect chains (e.g., crew/.beads -> rig/.beads -> mayor/rig/.beads)
|
||||
// This is intentional for the rig-level redirect architecture.
|
||||
// Limit depth to prevent infinite loops from misconfigured redirects.
|
||||
return resolveBeadsDirWithDepth(resolved, 3)
|
||||
}
|
||||
|
||||
// resolveBeadsDirWithDepth follows redirect chains with a depth limit.
|
||||
func resolveBeadsDirWithDepth(beadsDir string, maxDepth int) string {
|
||||
if maxDepth <= 0 {
|
||||
fmt.Fprintf(os.Stderr, "Warning: redirect chain too deep at %s, stopping\n", beadsDir)
|
||||
return beadsDir
|
||||
}
|
||||
|
||||
return resolved
|
||||
redirectPath := filepath.Join(beadsDir, "redirect")
|
||||
data, err := os.ReadFile(redirectPath) //nolint:gosec // G304: path is constructed internally
|
||||
if err != nil {
|
||||
// No redirect, this is the final destination
|
||||
return beadsDir
|
||||
}
|
||||
|
||||
redirectTarget := strings.TrimSpace(string(data))
|
||||
if redirectTarget == "" {
|
||||
return beadsDir
|
||||
}
|
||||
|
||||
// Resolve relative to parent of beadsDir (the workDir)
|
||||
workDir := filepath.Dir(beadsDir)
|
||||
resolved := filepath.Clean(filepath.Join(workDir, redirectTarget))
|
||||
|
||||
// Detect circular redirect
|
||||
if resolved == beadsDir {
|
||||
fmt.Fprintf(os.Stderr, "Warning: circular redirect detected in %s, stopping\n", redirectPath)
|
||||
return beadsDir
|
||||
}
|
||||
|
||||
// Recursively follow
|
||||
return resolveBeadsDirWithDepth(resolved, maxDepth-1)
|
||||
}
|
||||
|
||||
// cleanBeadsRuntimeFiles removes gitignored runtime files from a .beads directory
|
||||
// while preserving tracked files (formulas/, README.md, config.yaml, .gitignore).
|
||||
// This is safe to call even if the directory doesn't exist.
|
||||
func cleanBeadsRuntimeFiles(beadsDir string) error {
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
return nil // Nothing to clean
|
||||
}
|
||||
|
||||
// Runtime files/patterns that are gitignored and safe to remove
|
||||
runtimePatterns := []string{
|
||||
// SQLite databases
|
||||
"*.db", "*.db-*", "*.db?*",
|
||||
// Daemon runtime
|
||||
"daemon.lock", "daemon.log", "daemon.pid", "bd.sock",
|
||||
// Sync state
|
||||
"sync-state.json", "last-touched", "metadata.json",
|
||||
// Version tracking
|
||||
".local_version",
|
||||
// Redirect file (we're about to recreate it)
|
||||
"redirect",
|
||||
// Merge artifacts
|
||||
"beads.base.*", "beads.left.*", "beads.right.*",
|
||||
// JSONL files (tracked but will be redirected, safe to remove in worktrees)
|
||||
"issues.jsonl", "interactions.jsonl",
|
||||
// Runtime directories
|
||||
"mq",
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for _, pattern := range runtimePatterns {
|
||||
matches, err := filepath.Glob(filepath.Join(beadsDir, pattern))
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, match := range matches {
|
||||
if err := os.RemoveAll(match); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// SetupRedirect creates a .beads/redirect file for a worktree to point to the rig's shared beads.
|
||||
// This is used by crew, polecats, and refinery worktrees to share the rig's beads database.
|
||||
//
|
||||
// Parameters:
|
||||
// - townRoot: the town root directory (e.g., ~/gt)
|
||||
// - worktreePath: the worktree directory (e.g., <rig>/crew/<name> or <rig>/refinery/rig)
|
||||
//
|
||||
// The function:
|
||||
// 1. Computes the relative path from worktree to rig-level .beads
|
||||
// 2. Cleans up runtime files (preserving tracked files like formulas/)
|
||||
// 3. Creates the redirect file
|
||||
//
|
||||
// Safety: This function refuses to create redirects in the canonical beads location
|
||||
// (mayor/rig) to prevent circular redirect chains.
|
||||
func SetupRedirect(townRoot, worktreePath string) error {
|
||||
// Get rig root from worktree path
|
||||
// worktreePath = <town>/<rig>/crew/<name> or <town>/<rig>/refinery/rig etc.
|
||||
relPath, err := filepath.Rel(townRoot, worktreePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("computing relative path: %w", err)
|
||||
}
|
||||
parts := strings.Split(filepath.ToSlash(relPath), "/")
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("invalid worktree path: must be at least 2 levels deep from town root")
|
||||
}
|
||||
|
||||
// Safety check: prevent creating redirect in canonical beads location (mayor/rig)
|
||||
// This would create a circular redirect chain since rig/.beads redirects to mayor/rig/.beads
|
||||
if len(parts) >= 2 && parts[1] == "mayor" {
|
||||
return fmt.Errorf("cannot create redirect in canonical beads location (mayor/rig)")
|
||||
}
|
||||
|
||||
rigRoot := filepath.Join(townRoot, parts[0])
|
||||
rigBeadsPath := filepath.Join(rigRoot, ".beads")
|
||||
mayorBeadsPath := filepath.Join(rigRoot, "mayor", "rig", ".beads")
|
||||
|
||||
// Check rig-level .beads first, fall back to mayor/rig/.beads (tracked beads architecture)
|
||||
usesMayorFallback := false
|
||||
if _, err := os.Stat(rigBeadsPath); os.IsNotExist(err) {
|
||||
// No rig/.beads - check for mayor/rig/.beads (tracked beads architecture)
|
||||
if _, err := os.Stat(mayorBeadsPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("no beads found at %s or %s", rigBeadsPath, mayorBeadsPath)
|
||||
}
|
||||
// Using mayor fallback - warn user to run bd doctor
|
||||
fmt.Fprintf(os.Stderr, "Warning: rig .beads not found at %s, using %s\n", rigBeadsPath, mayorBeadsPath)
|
||||
fmt.Fprintf(os.Stderr, " Run 'bd doctor' to fix rig beads configuration\n")
|
||||
usesMayorFallback = true
|
||||
}
|
||||
|
||||
// Clean up runtime files in .beads/ but preserve tracked files (formulas/, README.md, etc.)
|
||||
worktreeBeadsDir := filepath.Join(worktreePath, ".beads")
|
||||
if err := cleanBeadsRuntimeFiles(worktreeBeadsDir); err != nil {
|
||||
return fmt.Errorf("cleaning runtime files: %w", err)
|
||||
}
|
||||
|
||||
// Create .beads directory if it doesn't exist
|
||||
if err := os.MkdirAll(worktreeBeadsDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating .beads dir: %w", err)
|
||||
}
|
||||
|
||||
// Compute relative path from worktree to rig root
|
||||
// e.g., crew/<name> (depth 2) -> ../../.beads
|
||||
// refinery/rig (depth 2) -> ../../.beads
|
||||
depth := len(parts) - 1 // subtract 1 for rig name itself
|
||||
upPath := strings.Repeat("../", depth)
|
||||
|
||||
var redirectPath string
|
||||
if usesMayorFallback {
|
||||
// Direct redirect to mayor/rig/.beads since rig/.beads doesn't exist
|
||||
redirectPath = upPath + "mayor/rig/.beads"
|
||||
} else {
|
||||
redirectPath = upPath + ".beads"
|
||||
|
||||
// Check if rig-level beads has a redirect (tracked beads case).
|
||||
// If so, redirect directly to the final destination to avoid chains.
|
||||
// The bd CLI doesn't support redirect chains, so we must skip intermediate hops.
|
||||
rigRedirectPath := filepath.Join(rigBeadsPath, "redirect")
|
||||
if data, err := os.ReadFile(rigRedirectPath); err == nil {
|
||||
rigRedirectTarget := strings.TrimSpace(string(data))
|
||||
if rigRedirectTarget != "" {
|
||||
// Rig has redirect (e.g., "mayor/rig/.beads" for tracked beads).
|
||||
// Redirect worktree directly to the final destination.
|
||||
redirectPath = upPath + rigRedirectTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create redirect file
|
||||
redirectFile := filepath.Join(worktreeBeadsDir, "redirect")
|
||||
if err := os.WriteFile(redirectFile, []byte(redirectPath+"\n"), 0644); err != nil {
|
||||
return fmt.Errorf("creating redirect file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Issue represents a beads issue.
|
||||
@@ -249,6 +422,13 @@ func (b *Beads) run(args ...string) ([]byte, error) {
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
// Run executes a bd command and returns stdout.
|
||||
// This is a public wrapper around the internal run method for cases where
|
||||
// callers need to run arbitrary bd commands.
|
||||
func (b *Beads) Run(args ...string) ([]byte, error) {
|
||||
return b.run(args...)
|
||||
}
|
||||
|
||||
// wrapError wraps bd errors with context.
|
||||
func (b *Beads) wrapError(err error, stderr string, args []string) error {
|
||||
stderr = strings.TrimSpace(stderr)
|
||||
@@ -593,7 +773,7 @@ func (b *Beads) Update(id string, opts UpdateOptions) error {
|
||||
}
|
||||
|
||||
// Close closes one or more issues.
|
||||
// If CLAUDE_SESSION_ID is set in the environment, it is passed to bd close
|
||||
// If a runtime session ID is set in the environment, it is passed to bd close
|
||||
// for work attribution tracking (see decision 009-session-events-architecture.md).
|
||||
func (b *Beads) Close(ids ...string) error {
|
||||
if len(ids) == 0 {
|
||||
@@ -603,7 +783,7 @@ func (b *Beads) Close(ids ...string) error {
|
||||
args := append([]string{"close"}, ids...)
|
||||
|
||||
// Pass session ID for work attribution if available
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
args = append(args, "--session="+sessionID)
|
||||
}
|
||||
|
||||
@@ -612,7 +792,7 @@ func (b *Beads) Close(ids ...string) error {
|
||||
}
|
||||
|
||||
// CloseWithReason closes one or more issues with a reason.
|
||||
// If CLAUDE_SESSION_ID is set in the environment, it is passed to bd close
|
||||
// If a runtime session ID is set in the environment, it is passed to bd close
|
||||
// for work attribution tracking (see decision 009-session-events-architecture.md).
|
||||
func (b *Beads) CloseWithReason(reason string, ids ...string) error {
|
||||
if len(ids) == 0 {
|
||||
@@ -623,7 +803,7 @@ func (b *Beads) CloseWithReason(reason string, ids ...string) error {
|
||||
args = append(args, "--reason="+reason)
|
||||
|
||||
// Pass session ID for work attribution if available
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
args = append(args, "--session="+sessionID)
|
||||
}
|
||||
|
||||
@@ -975,6 +1155,16 @@ func (b *Beads) CreateAgentBead(id, title string, fields *AgentFields) (*Issue,
|
||||
}
|
||||
}
|
||||
|
||||
// Set the hook slot if specified (this is the authoritative storage)
|
||||
// This fixes the slot inconsistency bug where bead status is 'hooked' but
|
||||
// agent's hook slot is empty. See mi-619.
|
||||
if fields != nil && fields.HookBead != "" {
|
||||
if _, err := b.run("slot", "set", id, "hook", fields.HookBead); err != nil {
|
||||
// Non-fatal: warn but continue - description text has the backup
|
||||
fmt.Printf("Warning: could not set hook slot: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &issue, nil
|
||||
}
|
||||
|
||||
@@ -1026,6 +1216,38 @@ func (b *Beads) UpdateAgentState(id string, state string, hookBead *string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetHookBead sets the hook_bead slot on an agent bead.
|
||||
// This is a convenience wrapper that only sets the hook without changing agent_state.
|
||||
// Per gt-zecmc: agent_state ("running", "dead", "idle") is observable from tmux
|
||||
// and should not be recorded in beads ("discover, don't track" principle).
|
||||
func (b *Beads) SetHookBead(agentBeadID, hookBeadID string) error {
|
||||
// Set the hook using bd slot set
|
||||
// This updates the hook_bead column directly in SQLite
|
||||
_, err := b.run("slot", "set", agentBeadID, "hook", hookBeadID)
|
||||
if err != nil {
|
||||
// If slot is already occupied, clear it first then retry
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "already occupied") {
|
||||
_, _ = b.run("slot", "clear", agentBeadID, "hook")
|
||||
_, err = b.run("slot", "set", agentBeadID, "hook", hookBeadID)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting hook: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearHookBead clears the hook_bead slot on an agent bead.
|
||||
// Used when work is complete or unslung.
|
||||
func (b *Beads) ClearHookBead(agentBeadID string) error {
|
||||
_, err := b.run("slot", "clear", agentBeadID, "hook")
|
||||
if err != nil {
|
||||
return fmt.Errorf("clearing hook: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAgentCleanupStatus updates the cleanup_status field in an agent bead.
|
||||
// This is called by the polecat to self-report its git state (ZFC compliance).
|
||||
// Valid statuses: clean, has_uncommitted, has_stash, has_unpushed
|
||||
@@ -1623,3 +1845,113 @@ func (b *Beads) MergeSlotEnsureExists() (string, error) {
|
||||
|
||||
return status.ID, nil
|
||||
}
|
||||
|
||||
// ===== Rig Identity Beads =====
|
||||
|
||||
// RigFields contains the fields specific to rig identity beads.
|
||||
type RigFields struct {
|
||||
Repo string // Git URL for the rig's repository
|
||||
Prefix string // Beads prefix for this rig (e.g., "gt", "bd")
|
||||
State string // Operational state: active, archived, maintenance
|
||||
}
|
||||
|
||||
// FormatRigDescription formats the description field for a rig identity bead.
|
||||
func FormatRigDescription(name string, fields *RigFields) string {
|
||||
if fields == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, fmt.Sprintf("Rig identity bead for %s.", name))
|
||||
lines = append(lines, "")
|
||||
|
||||
if fields.Repo != "" {
|
||||
lines = append(lines, fmt.Sprintf("repo: %s", fields.Repo))
|
||||
}
|
||||
if fields.Prefix != "" {
|
||||
lines = append(lines, fmt.Sprintf("prefix: %s", fields.Prefix))
|
||||
}
|
||||
if fields.State != "" {
|
||||
lines = append(lines, fmt.Sprintf("state: %s", fields.State))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// ParseRigFields extracts rig fields from an issue's description.
|
||||
func ParseRigFields(description string) *RigFields {
|
||||
fields := &RigFields{}
|
||||
|
||||
for _, line := range strings.Split(description, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(line[:colonIdx])
|
||||
value := strings.TrimSpace(line[colonIdx+1:])
|
||||
if value == "null" || value == "" {
|
||||
value = ""
|
||||
}
|
||||
|
||||
switch strings.ToLower(key) {
|
||||
case "repo":
|
||||
fields.Repo = value
|
||||
case "prefix":
|
||||
fields.Prefix = value
|
||||
case "state":
|
||||
fields.State = value
|
||||
}
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// CreateRigBead creates a rig identity bead for tracking rig metadata.
|
||||
// The ID format is: <prefix>-rig-<name> (e.g., gt-rig-gastown)
|
||||
// Use RigBeadID() helper to generate correct IDs.
|
||||
// The created_by field is populated from BD_ACTOR env var for provenance tracking.
|
||||
func (b *Beads) CreateRigBead(id, title string, fields *RigFields) (*Issue, error) {
|
||||
description := FormatRigDescription(title, fields)
|
||||
|
||||
args := []string{"create", "--json",
|
||||
"--id=" + id,
|
||||
"--type=rig",
|
||||
"--title=" + title,
|
||||
"--description=" + description,
|
||||
}
|
||||
|
||||
// Default actor from BD_ACTOR env var for provenance tracking
|
||||
if actor := os.Getenv("BD_ACTOR"); actor != "" {
|
||||
args = append(args, "--actor="+actor)
|
||||
}
|
||||
|
||||
out, err := b.run(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var issue Issue
|
||||
if err := json.Unmarshal(out, &issue); err != nil {
|
||||
return nil, fmt.Errorf("parsing bd create output: %w", err)
|
||||
}
|
||||
|
||||
return &issue, nil
|
||||
}
|
||||
|
||||
// RigBeadIDWithPrefix generates a rig identity bead ID using the specified prefix.
|
||||
// Format: <prefix>-rig-<name> (e.g., gt-rig-gastown)
|
||||
func RigBeadIDWithPrefix(prefix, name string) string {
|
||||
return fmt.Sprintf("%s-rig-%s", prefix, name)
|
||||
}
|
||||
|
||||
// RigBeadID generates a rig identity bead ID using "gt" prefix.
|
||||
// For non-gastown rigs, use RigBeadIDWithPrefix with the rig's configured prefix.
|
||||
func RigBeadID(name string) string {
|
||||
return RigBeadIDWithPrefix("gt", name)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package beads
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -87,9 +88,9 @@ func TestWrapError(t *testing.T) {
|
||||
b := New("/test")
|
||||
|
||||
tests := []struct {
|
||||
stderr string
|
||||
wantErr error
|
||||
wantNil bool
|
||||
stderr string
|
||||
wantErr error
|
||||
wantNil bool
|
||||
}{
|
||||
{"not a beads repository", ErrNotARepo, false},
|
||||
{"No .beads directory found", ErrNotARepo, false},
|
||||
@@ -126,7 +127,6 @@ func TestIntegration(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Walk up to find .beads
|
||||
dir := cwd
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, ".beads")); err == nil {
|
||||
@@ -139,8 +139,24 @@ func TestIntegration(t *testing.T) {
|
||||
dir = parent
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dir, ".beads", "beads.db")
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
t.Skip("no beads.db found (JSONL-only repo)")
|
||||
}
|
||||
|
||||
b := New(dir)
|
||||
|
||||
// Sync database with JSONL before testing to avoid "Database out of sync" errors.
|
||||
// This can happen when JSONL is updated (e.g., by git pull) but the SQLite database
|
||||
// hasn't been imported yet. Running sync --import-only ensures we test against
|
||||
// consistent data and prevents flaky test failures.
|
||||
syncCmd := exec.Command("bd", "--no-daemon", "sync", "--import-only")
|
||||
syncCmd.Dir = dir
|
||||
if err := syncCmd.Run(); err != nil {
|
||||
// If sync fails (e.g., no database exists), just log and continue
|
||||
t.Logf("bd sync --import-only failed (may not have db): %v", err)
|
||||
}
|
||||
|
||||
// Test List
|
||||
t.Run("List", func(t *testing.T) {
|
||||
issues, err := b.List(ListOptions{Status: "open"})
|
||||
@@ -189,10 +205,10 @@ func TestIntegration(t *testing.T) {
|
||||
// TestParseMRFields tests parsing MR fields from issue descriptions.
|
||||
func TestParseMRFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *Issue
|
||||
wantNil bool
|
||||
wantFields *MRFields
|
||||
name string
|
||||
issue *Issue
|
||||
wantNil bool
|
||||
wantFields *MRFields
|
||||
}{
|
||||
{
|
||||
name: "nil issue",
|
||||
@@ -509,8 +525,8 @@ author: someone
|
||||
target: main`,
|
||||
},
|
||||
fields: &MRFields{
|
||||
Branch: "polecat/Capable/gt-ghi",
|
||||
Target: "integration/epic",
|
||||
Branch: "polecat/Capable/gt-ghi",
|
||||
Target: "integration/epic",
|
||||
CloseReason: "merged",
|
||||
},
|
||||
want: `branch: polecat/Capable/gt-ghi
|
||||
@@ -1020,10 +1036,10 @@ func TestParseAgentBeadID(t *testing.T) {
|
||||
// Parseable but not valid agent roles (IsAgentSessionBead will reject)
|
||||
{"gt-abc123", "", "abc123", "", true}, // Parses as town-level but not valid role
|
||||
// Other prefixes (bd-, hq-)
|
||||
{"bd-mayor", "", "mayor", "", true}, // bd prefix town-level
|
||||
{"bd-beads-witness", "beads", "witness", "", true}, // bd prefix rig-level singleton
|
||||
{"bd-beads-polecat-pearl", "beads", "polecat", "pearl", true}, // bd prefix rig-level named
|
||||
{"hq-mayor", "", "mayor", "", true}, // hq prefix town-level
|
||||
{"bd-mayor", "", "mayor", "", true}, // bd prefix town-level
|
||||
{"bd-beads-witness", "beads", "witness", "", true}, // bd prefix rig-level singleton
|
||||
{"bd-beads-polecat-pearl", "beads", "polecat", "pearl", true}, // bd prefix rig-level named
|
||||
{"hq-mayor", "", "mayor", "", true}, // hq prefix town-level
|
||||
// Truly invalid patterns
|
||||
{"x-mayor", "", "", "", false}, // Prefix too short (1 char)
|
||||
{"abcd-mayor", "", "", "", false}, // Prefix too long (4 chars)
|
||||
@@ -1490,3 +1506,293 @@ func TestDelegationTerms(t *testing.T) {
|
||||
t.Errorf("parsed.CreditShare = %d, want %d", parsed.CreditShare, terms.CreditShare)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetupRedirect tests the beads redirect setup for worktrees.
|
||||
func TestSetupRedirect(t *testing.T) {
|
||||
t.Run("crew worktree with local beads", func(t *testing.T) {
|
||||
// Setup: town/rig/.beads (local, no redirect)
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
rigBeads := filepath.Join(rigRoot, ".beads")
|
||||
crewPath := filepath.Join(rigRoot, "crew", "max")
|
||||
|
||||
// Create rig structure
|
||||
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir rig beads: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(crewPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir crew: %v", err)
|
||||
}
|
||||
|
||||
// Run SetupRedirect
|
||||
if err := SetupRedirect(townRoot, crewPath); err != nil {
|
||||
t.Fatalf("SetupRedirect failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify redirect was created
|
||||
redirectPath := filepath.Join(crewPath, ".beads", "redirect")
|
||||
content, err := os.ReadFile(redirectPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read redirect: %v", err)
|
||||
}
|
||||
|
||||
want := "../../.beads\n"
|
||||
if string(content) != want {
|
||||
t.Errorf("redirect content = %q, want %q", string(content), want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("crew worktree with tracked beads", func(t *testing.T) {
|
||||
// Setup: town/rig/.beads/redirect -> mayor/rig/.beads (tracked)
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
rigBeads := filepath.Join(rigRoot, ".beads")
|
||||
mayorRigBeads := filepath.Join(rigRoot, "mayor", "rig", ".beads")
|
||||
crewPath := filepath.Join(rigRoot, "crew", "max")
|
||||
|
||||
// Create rig structure with tracked beads
|
||||
if err := os.MkdirAll(mayorRigBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir mayor/rig beads: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir rig beads: %v", err)
|
||||
}
|
||||
// Create rig-level redirect to mayor/rig/.beads
|
||||
if err := os.WriteFile(filepath.Join(rigBeads, "redirect"), []byte("mayor/rig/.beads\n"), 0644); err != nil {
|
||||
t.Fatalf("write rig redirect: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(crewPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir crew: %v", err)
|
||||
}
|
||||
|
||||
// Run SetupRedirect
|
||||
if err := SetupRedirect(townRoot, crewPath); err != nil {
|
||||
t.Fatalf("SetupRedirect failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify redirect goes directly to mayor/rig/.beads (no chain - bd CLI doesn't support chains)
|
||||
redirectPath := filepath.Join(crewPath, ".beads", "redirect")
|
||||
content, err := os.ReadFile(redirectPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read redirect: %v", err)
|
||||
}
|
||||
|
||||
want := "../../mayor/rig/.beads\n"
|
||||
if string(content) != want {
|
||||
t.Errorf("redirect content = %q, want %q", string(content), want)
|
||||
}
|
||||
|
||||
// Verify redirect resolves correctly
|
||||
resolved := ResolveBeadsDir(crewPath)
|
||||
// crew/max -> ../../mayor/rig/.beads (direct, no chain)
|
||||
if resolved != mayorRigBeads {
|
||||
t.Errorf("resolved = %q, want %q", resolved, mayorRigBeads)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("polecat worktree", func(t *testing.T) {
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
rigBeads := filepath.Join(rigRoot, ".beads")
|
||||
polecatPath := filepath.Join(rigRoot, "polecats", "worker1")
|
||||
|
||||
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir rig beads: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(polecatPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir polecat: %v", err)
|
||||
}
|
||||
|
||||
if err := SetupRedirect(townRoot, polecatPath); err != nil {
|
||||
t.Fatalf("SetupRedirect failed: %v", err)
|
||||
}
|
||||
|
||||
redirectPath := filepath.Join(polecatPath, ".beads", "redirect")
|
||||
content, err := os.ReadFile(redirectPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read redirect: %v", err)
|
||||
}
|
||||
|
||||
want := "../../.beads\n"
|
||||
if string(content) != want {
|
||||
t.Errorf("redirect content = %q, want %q", string(content), want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("refinery worktree", func(t *testing.T) {
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
rigBeads := filepath.Join(rigRoot, ".beads")
|
||||
refineryPath := filepath.Join(rigRoot, "refinery", "rig")
|
||||
|
||||
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir rig beads: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(refineryPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir refinery: %v", err)
|
||||
}
|
||||
|
||||
if err := SetupRedirect(townRoot, refineryPath); err != nil {
|
||||
t.Fatalf("SetupRedirect failed: %v", err)
|
||||
}
|
||||
|
||||
redirectPath := filepath.Join(refineryPath, ".beads", "redirect")
|
||||
content, err := os.ReadFile(redirectPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read redirect: %v", err)
|
||||
}
|
||||
|
||||
want := "../../.beads\n"
|
||||
if string(content) != want {
|
||||
t.Errorf("redirect content = %q, want %q", string(content), want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cleans runtime files but preserves tracked files", func(t *testing.T) {
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
rigBeads := filepath.Join(rigRoot, ".beads")
|
||||
crewPath := filepath.Join(rigRoot, "crew", "max")
|
||||
crewBeads := filepath.Join(crewPath, ".beads")
|
||||
|
||||
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir rig beads: %v", err)
|
||||
}
|
||||
// Simulate worktree with both runtime and tracked files
|
||||
if err := os.MkdirAll(crewBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir crew beads: %v", err)
|
||||
}
|
||||
// Runtime files (should be removed)
|
||||
if err := os.WriteFile(filepath.Join(crewBeads, "beads.db"), []byte("fake db"), 0644); err != nil {
|
||||
t.Fatalf("write fake db: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(crewBeads, "issues.jsonl"), []byte("{}"), 0644); err != nil {
|
||||
t.Fatalf("write issues.jsonl: %v", err)
|
||||
}
|
||||
// Tracked files (should be preserved)
|
||||
if err := os.WriteFile(filepath.Join(crewBeads, "config.yaml"), []byte("prefix: test"), 0644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(crewBeads, "README.md"), []byte("# Beads"), 0644); err != nil {
|
||||
t.Fatalf("write README: %v", err)
|
||||
}
|
||||
|
||||
if err := SetupRedirect(townRoot, crewPath); err != nil {
|
||||
t.Fatalf("SetupRedirect failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify runtime files were cleaned up
|
||||
if _, err := os.Stat(filepath.Join(crewBeads, "beads.db")); !os.IsNotExist(err) {
|
||||
t.Error("beads.db should have been removed")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(crewBeads, "issues.jsonl")); !os.IsNotExist(err) {
|
||||
t.Error("issues.jsonl should have been removed")
|
||||
}
|
||||
|
||||
// Verify tracked files were preserved
|
||||
if _, err := os.Stat(filepath.Join(crewBeads, "config.yaml")); err != nil {
|
||||
t.Errorf("config.yaml should have been preserved: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(crewBeads, "README.md")); err != nil {
|
||||
t.Errorf("README.md should have been preserved: %v", err)
|
||||
}
|
||||
|
||||
// Verify redirect was created
|
||||
redirectPath := filepath.Join(crewBeads, "redirect")
|
||||
if _, err := os.Stat(redirectPath); err != nil {
|
||||
t.Errorf("redirect file should exist: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects mayor/rig canonical location", func(t *testing.T) {
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
rigBeads := filepath.Join(rigRoot, ".beads")
|
||||
mayorRigPath := filepath.Join(rigRoot, "mayor", "rig")
|
||||
|
||||
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir rig beads: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(mayorRigPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir mayor/rig: %v", err)
|
||||
}
|
||||
|
||||
err := SetupRedirect(townRoot, mayorRigPath)
|
||||
if err == nil {
|
||||
t.Error("SetupRedirect should reject mayor/rig location")
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), "canonical") {
|
||||
t.Errorf("error should mention canonical location, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects path too shallow", func(t *testing.T) {
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
|
||||
if err := os.MkdirAll(rigRoot, 0755); err != nil {
|
||||
t.Fatalf("mkdir rig: %v", err)
|
||||
}
|
||||
|
||||
err := SetupRedirect(townRoot, rigRoot)
|
||||
if err == nil {
|
||||
t.Error("SetupRedirect should reject rig root (too shallow)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails if rig beads missing", func(t *testing.T) {
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
crewPath := filepath.Join(rigRoot, "crew", "max")
|
||||
|
||||
// No rig/.beads or mayor/rig/.beads created
|
||||
if err := os.MkdirAll(crewPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir crew: %v", err)
|
||||
}
|
||||
|
||||
err := SetupRedirect(townRoot, crewPath)
|
||||
if err == nil {
|
||||
t.Error("SetupRedirect should fail if rig .beads missing")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("crew worktree with mayor/rig beads only", func(t *testing.T) {
|
||||
// Setup: no rig/.beads, only mayor/rig/.beads exists
|
||||
// This is the tracked beads architecture where rig root has no .beads directory
|
||||
townRoot := t.TempDir()
|
||||
rigRoot := filepath.Join(townRoot, "testrig")
|
||||
mayorRigBeads := filepath.Join(rigRoot, "mayor", "rig", ".beads")
|
||||
crewPath := filepath.Join(rigRoot, "crew", "max")
|
||||
|
||||
// Create only mayor/rig/.beads (no rig/.beads)
|
||||
if err := os.MkdirAll(mayorRigBeads, 0755); err != nil {
|
||||
t.Fatalf("mkdir mayor/rig beads: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(crewPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir crew: %v", err)
|
||||
}
|
||||
|
||||
// Run SetupRedirect - should succeed and point to mayor/rig/.beads
|
||||
if err := SetupRedirect(townRoot, crewPath); err != nil {
|
||||
t.Fatalf("SetupRedirect failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify redirect points to mayor/rig/.beads
|
||||
redirectPath := filepath.Join(crewPath, ".beads", "redirect")
|
||||
content, err := os.ReadFile(redirectPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read redirect: %v", err)
|
||||
}
|
||||
|
||||
want := "../../mayor/rig/.beads\n"
|
||||
if string(content) != want {
|
||||
t.Errorf("redirect content = %q, want %q", string(content), want)
|
||||
}
|
||||
|
||||
// Verify redirect resolves correctly
|
||||
resolved := ResolveBeadsDir(crewPath)
|
||||
if resolved != mayorRigBeads {
|
||||
t.Errorf("resolved = %q, want %q", resolved, mayorRigBeads)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type AttachmentFields struct {
|
||||
AttachedMolecule string // Root issue ID of the attached molecule
|
||||
AttachedAt string // ISO 8601 timestamp when attached
|
||||
AttachedArgs string // Natural language args passed via gt sling --args (no-tmux mode)
|
||||
DispatchedBy string // Agent ID that dispatched this work (for completion notification)
|
||||
}
|
||||
|
||||
// ParseAttachmentFields extracts attachment fields from an issue's description.
|
||||
@@ -61,6 +62,9 @@ func ParseAttachmentFields(issue *Issue) *AttachmentFields {
|
||||
case "attached_args", "attached-args", "attachedargs":
|
||||
fields.AttachedArgs = value
|
||||
hasFields = true
|
||||
case "dispatched_by", "dispatched-by", "dispatchedby":
|
||||
fields.DispatchedBy = value
|
||||
hasFields = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +92,9 @@ func FormatAttachmentFields(fields *AttachmentFields) string {
|
||||
if fields.AttachedArgs != "" {
|
||||
lines = append(lines, "attached_args: "+fields.AttachedArgs)
|
||||
}
|
||||
if fields.DispatchedBy != "" {
|
||||
lines = append(lines, "dispatched_by: "+fields.DispatchedBy)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -107,6 +114,9 @@ func SetAttachmentFields(issue *Issue, fields *AttachmentFields) string {
|
||||
"attached_args": true,
|
||||
"attached-args": true,
|
||||
"attachedargs": true,
|
||||
"dispatched_by": true,
|
||||
"dispatched-by": true,
|
||||
"dispatchedby": true,
|
||||
}
|
||||
|
||||
// Collect non-attachment lines from existing description
|
||||
@@ -499,7 +509,7 @@ func FormatSynthesisFields(fields *SynthesisFields) string {
|
||||
type RoleConfig struct {
|
||||
// SessionPattern defines how to derive tmux session name.
|
||||
// Supports placeholders: {rig}, {name}, {role}
|
||||
// Examples: "gt-mayor", "gt-{rig}-{role}", "gt-{rig}-{name}"
|
||||
// Examples: "hq-mayor", "hq-deacon", "gt-{rig}-{role}", "gt-{rig}-{name}"
|
||||
SessionPattern string
|
||||
|
||||
// WorkDirPattern defines the working directory relative to town root.
|
||||
|
||||
@@ -57,7 +57,12 @@ func LoadRoutes(beadsDir string) ([]Route, error) {
|
||||
// If the prefix already exists, it updates the path.
|
||||
func AppendRoute(townRoot string, route Route) error {
|
||||
beadsDir := filepath.Join(townRoot, ".beads")
|
||||
return AppendRouteToDir(beadsDir, route)
|
||||
}
|
||||
|
||||
// AppendRouteToDir appends a route to routes.jsonl in the given beads directory.
|
||||
// If the prefix already exists, it updates the path.
|
||||
func AppendRouteToDir(beadsDir string, route Route) error {
|
||||
// Load existing routes
|
||||
routes, err := LoadRoutes(beadsDir)
|
||||
if err != nil {
|
||||
@@ -185,3 +190,60 @@ func FindConflictingPrefixes(beadsDir string) (map[string][]string, error) {
|
||||
|
||||
return conflicts, nil
|
||||
}
|
||||
|
||||
// ExtractPrefix extracts the prefix from a bead ID.
|
||||
// For example, "ap-qtsup.16" returns "ap-", "hq-cv-abc" returns "hq-".
|
||||
// Returns empty string if no valid prefix found (empty input, no hyphen,
|
||||
// or hyphen at position 0 which would indicate an invalid prefix).
|
||||
func ExtractPrefix(beadID string) string {
|
||||
if beadID == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
idx := strings.Index(beadID, "-")
|
||||
if idx <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return beadID[:idx+1]
|
||||
}
|
||||
|
||||
// GetRigPathForPrefix returns the rig path for a given bead ID prefix.
|
||||
// The townRoot should be the Gas Town root directory (e.g., ~/gt).
|
||||
// Returns the full absolute path to the rig directory, or empty string if not found.
|
||||
// For town-level beads (path="."), returns townRoot.
|
||||
func GetRigPathForPrefix(townRoot, prefix string) string {
|
||||
beadsDir := filepath.Join(townRoot, ".beads")
|
||||
routes, err := LoadRoutes(beadsDir)
|
||||
if err != nil || routes == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, r := range routes {
|
||||
if r.Prefix == prefix {
|
||||
if r.Path == "." {
|
||||
return townRoot // Town-level beads
|
||||
}
|
||||
return filepath.Join(townRoot, r.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResolveHookDir determines the directory for running bd update on a bead.
|
||||
// Since bd update doesn't support routing or redirects, we must resolve the
|
||||
// actual rig directory from the bead's prefix. hookWorkDir is only used as
|
||||
// a fallback if prefix resolution fails.
|
||||
func ResolveHookDir(townRoot, beadID, hookWorkDir string) string {
|
||||
// Always try prefix resolution first - bd update needs the actual rig dir
|
||||
prefix := ExtractPrefix(beadID)
|
||||
if rigPath := GetRigPathForPrefix(townRoot, prefix); rigPath != "" {
|
||||
return rigPath
|
||||
}
|
||||
// Fallback to hookWorkDir if provided
|
||||
if hookWorkDir != "" {
|
||||
return hookWorkDir
|
||||
}
|
||||
return townRoot
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user