feat(handoff): route crew lifecycle requests to deacon

Crew workers now use deacon for lifecycle management instead of
requiring manual session termination. When a crew worker runs
'gt handoff', it sends a lifecycle request to the deacon which
handles session kill/restart like it does for Mayor and Witness.

Changes:
- Route crew manager to deacon/ instead of "human"
- Add getCrewIdentity() to extract <rig>-crew-<name> from session
- Include full crew identity in LIFECYCLE subject for daemon parsing
- Remove special case that skipped lifecycle flow for crew

Also fixes pre-existing test failures in daemon/lifecycle_test.go
where BeadsMessage field names were out of sync with the struct.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-21 01:17:32 -08:00
parent bdbba025f5
commit dbecfe1d38
2 changed files with 54 additions and 46 deletions

View File

@@ -30,8 +30,8 @@ func TestParseLifecycleRequest_Cycle(t *testing.T) {
for _, tc := range tests {
msg := &BeadsMessage{
Title: tc.title,
Sender: "test-sender",
Subject: tc.title,
From: "test-sender",
}
result := d.parseLifecycleRequest(msg)
if result == nil {
@@ -64,8 +64,8 @@ func TestParseLifecycleRequest_PrefixMatchesCycle(t *testing.T) {
for _, tc := range tests {
msg := &BeadsMessage{
Title: tc.title,
Sender: "test-sender",
Subject: tc.title,
From: "test-sender",
}
result := d.parseLifecycleRequest(msg)
if result == nil {
@@ -91,8 +91,8 @@ func TestParseLifecycleRequest_NotLifecycle(t *testing.T) {
for _, title := range tests {
msg := &BeadsMessage{
Title: title,
Sender: "test-sender",
Subject: title,
From: "test-sender",
}
result := d.parseLifecycleRequest(msg)
if result != nil {
@@ -116,8 +116,8 @@ func TestParseLifecycleRequest_ExtractsFrom(t *testing.T) {
for _, tc := range tests {
msg := &BeadsMessage{
Title: tc.title,
Sender: tc.sender,
Subject: tc.title,
From: tc.sender,
}
result := d.parseLifecycleRequest(msg)
if result == nil {
@@ -135,8 +135,8 @@ func TestParseLifecycleRequest_FallsBackToSender(t *testing.T) {
// When the title doesn't contain a parseable "from", use sender
msg := &BeadsMessage{
Title: "LIFECYCLE: requesting cycle", // no role before "requesting"
Sender: "fallback-sender",
Subject: "LIFECYCLE: requesting cycle", // no role before "requesting"
From: "fallback-sender",
}
result := d.parseLifecycleRequest(msg)
if result == nil {
@@ -200,23 +200,23 @@ func TestIdentityToSession_Unknown(t *testing.T) {
func TestBeadsMessage_Serialization(t *testing.T) {
msg := BeadsMessage{
ID: "msg-123",
Title: "Test Message",
Description: "A test message body",
Sender: "test-sender",
Assignee: "test-assignee",
Priority: 1,
Status: "open",
ID: "msg-123",
Subject: "Test Message",
Body: "A test message body",
From: "test-sender",
To: "test-recipient",
Priority: "high",
Type: "message",
}
// Verify all fields are accessible
if msg.ID != "msg-123" {
t.Errorf("ID mismatch")
}
if msg.Title != "Test Message" {
t.Errorf("Title mismatch")
if msg.Subject != "Test Message" {
t.Errorf("Subject mismatch")
}
if msg.Status != "open" {
t.Errorf("Status mismatch")
if msg.From != "test-sender" {
t.Errorf("From mismatch")
}
}