Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.
@@ -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.');
|
||||
39
.github/workflows/ci.yml
vendored
39
.github/workflows/ci.yml
vendored
@@ -33,6 +33,36 @@ 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
|
||||
@@ -49,9 +79,6 @@ 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
|
||||
|
||||
@@ -69,9 +96,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 +121,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/
|
||||
|
||||
172
CHANGELOG.md
172
CHANGELOG.md
@@ -7,6 +7,178 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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)
|
||||
|
||||
578
README.md
578
README.md
@@ -1,372 +1,436 @@
|
||||
# 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.
|
||||
|
||||
## 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** - [claude.ai/code](https://claude.ai/code)
|
||||
|
||||
### 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
|
||||
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
|
||||
gt convoy list # Check progress
|
||||
# 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
|
||||
```
|
||||
|
||||
### Pick Your Roles
|
||||
## Common Workflows
|
||||
|
||||
Gas Town is modular. Run what you need:
|
||||
### Mayor Workflow (Recommended)
|
||||
|
||||
- **Polecats only**: Manual spawning, no monitoring
|
||||
- **+ Witness**: Automatic worker lifecycle, stuck detection
|
||||
- **+ Refinery**: Merge queue, code review
|
||||
- **+ Mayor**: Cross-project coordination
|
||||
**Best for:** Coordinating complex, multi-issue work
|
||||
|
||||
## Cooking Formulas
|
||||
```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]
|
||||
```
|
||||
|
||||
Formulas define structured workflows. Cook them, sling them to agents.
|
||||
**Commands:**
|
||||
|
||||
### Basic Example
|
||||
```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
|
||||
```
|
||||
|
||||
### Beads Formula Workflow
|
||||
|
||||
**Best for:** Predefined, repeatable processes
|
||||
|
||||
Formulas are TOML-defined workflows stored in `.beads/formulas/`.
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
## 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
|
||||
gt agents # List active agents
|
||||
gt sling <issue> <rig> # Assign work to agent
|
||||
gt sling <issue> <rig> --agent codex # Override runtime for this sling/spawn
|
||||
gt mayor attach # Start Mayor session
|
||||
gt mayor start --agent gemini # Run Mayor with a specific agent alias
|
||||
gt prime # Alternative to mayor attach
|
||||
```
|
||||
|
||||
# Work assignment
|
||||
gt sling <bead> <rig> # Assign work to polecat
|
||||
bd ready # Show available work
|
||||
bd list --status=in_progress # Active work
|
||||
### Convoy (Work Tracking)
|
||||
|
||||
# Communication
|
||||
gt mail inbox # Check messages
|
||||
gt mail send <addr> -s "..." -m "..."
|
||||
```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
|
||||
```
|
||||
|
||||
# Lifecycle
|
||||
gt handoff # Request session cycle
|
||||
gt peek <agent> # Check agent health
|
||||
### Configuration
|
||||
|
||||
# Diagnostics
|
||||
gt doctor # Health check
|
||||
gt doctor --fix # Auto-repair
|
||||
```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
|
||||
```
|
||||
|
||||
## 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!
|
||||
|
||||
@@ -130,6 +130,29 @@ 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:
|
||||
@@ -184,7 +207,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
|
||||
|
||||
@@ -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,28 @@ 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`
|
||||
|
||||
**Custom agents**: Define per-town in `mayor/town.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
|
||||
```
|
||||
|
||||
### Rig Management
|
||||
|
||||
```bash
|
||||
@@ -242,12 +395,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 +483,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
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -71,15 +71,162 @@ 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",
|
||||
}
|
||||
|
||||
for _, pattern := range runtimePatterns {
|
||||
matches, err := filepath.Glob(filepath.Join(beadsDir, pattern))
|
||||
if err != nil {
|
||||
continue // Invalid pattern, skip
|
||||
}
|
||||
for _, match := range matches {
|
||||
os.RemoveAll(match) // Best effort, ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
if _, err := os.Stat(rigBeadsPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("no rig .beads found at %s", rigBeadsPath)
|
||||
}
|
||||
|
||||
// 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
|
||||
redirectPath := strings.Repeat("../", depth) + ".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 = strings.Repeat("../", depth) + 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 +396,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)
|
||||
@@ -975,6 +1129,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 +1190,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 +1819,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"
|
||||
@@ -141,6 +142,17 @@ func TestIntegration(t *testing.T) {
|
||||
|
||||
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"})
|
||||
@@ -1490,3 +1502,253 @@ 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 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -52,6 +52,143 @@ func TestGetPrefixForRig_NoRoutesFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
beadID string
|
||||
expected string
|
||||
}{
|
||||
{"ap-qtsup.16", "ap-"},
|
||||
{"hq-cv-abc", "hq-"},
|
||||
{"gt-mol-xyz", "gt-"},
|
||||
{"bd-123", "bd-"},
|
||||
{"", ""},
|
||||
{"nohyphen", ""},
|
||||
{"-startswithhyphen", ""}, // Leading hyphen = invalid prefix
|
||||
{"-", ""}, // Just hyphen = invalid
|
||||
{"a-", "a-"}, // Trailing hyphen is valid
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.beadID, func(t *testing.T) {
|
||||
result := ExtractPrefix(tc.beadID)
|
||||
if result != tc.expected {
|
||||
t.Errorf("ExtractPrefix(%q) = %q, want %q", tc.beadID, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRigPathForPrefix(t *testing.T) {
|
||||
// Create a temporary directory with routes.jsonl
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
routesContent := `{"prefix": "ap-", "path": "ai_platform/mayor/rig"}
|
||||
{"prefix": "gt-", "path": "gastown/mayor/rig"}
|
||||
{"prefix": "hq-", "path": "."}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "routes.jsonl"), []byte(routesContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
prefix string
|
||||
expected string
|
||||
}{
|
||||
{"ap-", filepath.Join(tmpDir, "ai_platform/mayor/rig")},
|
||||
{"gt-", filepath.Join(tmpDir, "gastown/mayor/rig")},
|
||||
{"hq-", tmpDir}, // Town-level beads return townRoot
|
||||
{"unknown-", ""}, // Unknown prefix returns empty
|
||||
{"", ""}, // Empty prefix returns empty
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.prefix, func(t *testing.T) {
|
||||
result := GetRigPathForPrefix(tmpDir, tc.prefix)
|
||||
if result != tc.expected {
|
||||
t.Errorf("GetRigPathForPrefix(%q, %q) = %q, want %q", tmpDir, tc.prefix, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRigPathForPrefix_NoRoutesFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
// No routes.jsonl file
|
||||
|
||||
result := GetRigPathForPrefix(tmpDir, "ap-")
|
||||
if result != "" {
|
||||
t.Errorf("Expected empty string when no routes file, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveHookDir(t *testing.T) {
|
||||
// Create a temporary directory with routes.jsonl
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
routesContent := `{"prefix": "ap-", "path": "ai_platform/mayor/rig"}
|
||||
{"prefix": "hq-", "path": "."}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "routes.jsonl"), []byte(routesContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
beadID string
|
||||
hookWorkDir string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "prefix resolution takes precedence over hookWorkDir",
|
||||
beadID: "ap-test",
|
||||
hookWorkDir: "/custom/path",
|
||||
expected: filepath.Join(tmpDir, "ai_platform/mayor/rig"),
|
||||
},
|
||||
{
|
||||
name: "resolves rig path from prefix",
|
||||
beadID: "ap-test",
|
||||
hookWorkDir: "",
|
||||
expected: filepath.Join(tmpDir, "ai_platform/mayor/rig"),
|
||||
},
|
||||
{
|
||||
name: "town-level bead returns townRoot",
|
||||
beadID: "hq-test",
|
||||
hookWorkDir: "",
|
||||
expected: tmpDir,
|
||||
},
|
||||
{
|
||||
name: "unknown prefix uses hookWorkDir as fallback",
|
||||
beadID: "xx-unknown",
|
||||
hookWorkDir: "/fallback/path",
|
||||
expected: "/fallback/path",
|
||||
},
|
||||
{
|
||||
name: "unknown prefix without hookWorkDir falls back to townRoot",
|
||||
beadID: "xx-unknown",
|
||||
hookWorkDir: "",
|
||||
expected: tmpDir,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ResolveHookDir(tmpDir, tc.beadID, tc.hookWorkDir)
|
||||
if result != tc.expected {
|
||||
t.Errorf("ResolveHookDir(%q, %q, %q) = %q, want %q",
|
||||
tmpDir, tc.beadID, tc.hookWorkDir, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentBeadIDsWithPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
)
|
||||
|
||||
// SessionName is the tmux session name for Boot.
|
||||
// Note: We use "gt-boot" instead of "gt-deacon-boot" to avoid tmux prefix
|
||||
// matching collisions. Tmux matches session names by prefix, so "gt-deacon-boot"
|
||||
// would match when checking for "gt-deacon", causing HasSession("gt-deacon")
|
||||
// Note: We use "gt-boot" instead of "hq-deacon-boot" to avoid tmux prefix
|
||||
// matching collisions. Tmux matches session names by prefix, so "hq-deacon-boot"
|
||||
// would match when checking for "hq-deacon", causing HasSession("hq-deacon")
|
||||
// to return true when only Boot is running.
|
||||
const SessionName = "gt-boot"
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "gt prime && gt mail check --inject && gt nudge deacon session-started"
|
||||
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt prime && gt mail check --inject && gt nudge deacon session-started"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "gt prime"
|
||||
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt prime"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "gt mail check --inject"
|
||||
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt mail check --inject"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -42,7 +42,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "gt costs record"
|
||||
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt costs record"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "gt prime && gt nudge deacon session-started"
|
||||
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt prime && gt nudge deacon session-started"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "gt prime"
|
||||
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt prime"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "gt mail check --inject"
|
||||
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt mail check --inject"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -42,7 +42,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "gt costs record"
|
||||
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt costs record"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
// RoleTypeFor returns the RoleType for a given role name.
|
||||
func RoleTypeFor(role string) RoleType {
|
||||
switch role {
|
||||
case "polecat", "witness", "refinery":
|
||||
case "polecat", "witness", "refinery", "deacon":
|
||||
return Autonomous
|
||||
default:
|
||||
return Interactive
|
||||
@@ -35,8 +35,8 @@ func RoleTypeFor(role string) RoleType {
|
||||
}
|
||||
|
||||
// EnsureSettings ensures .claude/settings.json exists in the given directory.
|
||||
// If the file doesn't exist, it copies the appropriate template based on role type.
|
||||
// If the file already exists, it's left unchanged.
|
||||
// For worktrees, we use sparse checkout to exclude source repo's .claude/ directory,
|
||||
// so our settings.json is the only one Claude Code sees.
|
||||
func EnsureSettings(workDir string, roleType RoleType) error {
|
||||
claudeDir := filepath.Join(workDir, ".claude")
|
||||
settingsPath := filepath.Join(claudeDir, "settings.json")
|
||||
|
||||
@@ -127,24 +127,29 @@ func init() {
|
||||
|
||||
// categorizeSession determines the agent type from a session name.
|
||||
func categorizeSession(name string) *AgentSession {
|
||||
// Must start with gt- prefix
|
||||
session := &AgentSession{Name: name}
|
||||
|
||||
// Town-level agents use hq- prefix: hq-mayor, hq-deacon
|
||||
if strings.HasPrefix(name, "hq-") {
|
||||
suffix := strings.TrimPrefix(name, "hq-")
|
||||
if suffix == "mayor" {
|
||||
session.Type = AgentMayor
|
||||
return session
|
||||
}
|
||||
if suffix == "deacon" {
|
||||
session.Type = AgentDeacon
|
||||
return session
|
||||
}
|
||||
return nil // Unknown hq- session
|
||||
}
|
||||
|
||||
// Rig-level agents use gt- prefix
|
||||
if !strings.HasPrefix(name, "gt-") {
|
||||
return nil
|
||||
}
|
||||
|
||||
session := &AgentSession{Name: name}
|
||||
suffix := strings.TrimPrefix(name, "gt-")
|
||||
|
||||
// Town-level agents: gt-mayor, gt-deacon (simple format, one per machine)
|
||||
if suffix == "mayor" {
|
||||
session.Type = AgentMayor
|
||||
return session
|
||||
}
|
||||
if suffix == "deacon" {
|
||||
session.Type = AgentDeacon
|
||||
return session
|
||||
}
|
||||
|
||||
// Witness sessions: legacy format gt-witness-<rig> (fallback)
|
||||
if strings.HasPrefix(suffix, "witness-") {
|
||||
session.Type = AgentWitness
|
||||
|
||||
@@ -443,6 +443,73 @@ func TestBeadsRemoveRoute(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSlingCrossRigRoutingResolution verifies that sling can resolve rig paths
|
||||
// for cross-rig bead hooking using ExtractPrefix and GetRigPathForPrefix.
|
||||
// This is the fix for https://github.com/steveyegge/gastown/issues/148
|
||||
func TestSlingCrossRigRoutingResolution(t *testing.T) {
|
||||
townRoot := setupRoutingTestTown(t)
|
||||
|
||||
tests := []struct {
|
||||
beadID string
|
||||
expectedPath string // Relative to townRoot, or "." for town-level
|
||||
}{
|
||||
{"gt-mol-abc", "gastown/mayor/rig"},
|
||||
{"tr-task-xyz", "testrig/mayor/rig"},
|
||||
{"hq-cv-123", "."}, // Town-level beads
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.beadID, func(t *testing.T) {
|
||||
// Step 1: Extract prefix from bead ID
|
||||
prefix := beads.ExtractPrefix(tc.beadID)
|
||||
if prefix == "" {
|
||||
t.Fatalf("ExtractPrefix(%q) returned empty", tc.beadID)
|
||||
}
|
||||
|
||||
// Step 2: Resolve rig path from prefix
|
||||
rigPath := beads.GetRigPathForPrefix(townRoot, prefix)
|
||||
if rigPath == "" {
|
||||
t.Fatalf("GetRigPathForPrefix(%q, %q) returned empty", townRoot, prefix)
|
||||
}
|
||||
|
||||
// Step 3: Verify the path is correct
|
||||
var expectedFull string
|
||||
if tc.expectedPath == "." {
|
||||
expectedFull = townRoot
|
||||
} else {
|
||||
expectedFull = filepath.Join(townRoot, tc.expectedPath)
|
||||
}
|
||||
|
||||
if rigPath != expectedFull {
|
||||
t.Errorf("GetRigPathForPrefix resolved to %q, want %q", rigPath, expectedFull)
|
||||
}
|
||||
|
||||
// Step 4: Verify the .beads directory exists at that path
|
||||
beadsDir := filepath.Join(rigPath, ".beads")
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
t.Errorf(".beads directory doesn't exist at resolved path: %s", beadsDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSlingCrossRigUnknownPrefix verifies behavior for unknown prefixes.
|
||||
func TestSlingCrossRigUnknownPrefix(t *testing.T) {
|
||||
townRoot := setupRoutingTestTown(t)
|
||||
|
||||
// An unknown prefix should return empty string
|
||||
unknownBeadID := "xx-unknown-123"
|
||||
prefix := beads.ExtractPrefix(unknownBeadID)
|
||||
if prefix != "xx-" {
|
||||
t.Fatalf("ExtractPrefix(%q) = %q, want %q", unknownBeadID, prefix, "xx-")
|
||||
}
|
||||
|
||||
rigPath := beads.GetRigPathForPrefix(townRoot, prefix)
|
||||
if rigPath != "" {
|
||||
t.Errorf("GetRigPathForPrefix for unknown prefix returned %q, want empty", rigPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBeadsGetPrefixForRig verifies prefix lookup by rig name.
|
||||
func TestBeadsGetPrefixForRig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user