Files
gastown/internal/beads/audit.go
Steve Yegge dce8d8bfae refactor: Split beads.go into focused files
- beads.go (512 lines): Core types and bd CLI wrapper operations
- fields.go (327 lines): AttachmentFields and MRFields parsing/formatting
- handoff.go (218 lines): Handoff bead ops, ClearMail, molecule attach/detach
- audit.go (98 lines): Detach audit logging

No functional changes - just reorganization for maintainability.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 01:29:35 -08:00

99 lines
3.1 KiB
Go

// Package beads provides audit logging for molecule operations.
package beads
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// DetachAuditEntry represents an audit log entry for a detach operation.
type DetachAuditEntry struct {
Timestamp string `json:"timestamp"`
Operation string `json:"operation"` // "detach", "burn", "squash"
PinnedBeadID string `json:"pinned_bead_id"`
DetachedMolecule string `json:"detached_molecule"`
DetachedBy string `json:"detached_by,omitempty"` // Agent that triggered detach
Reason string `json:"reason,omitempty"` // Optional reason for detach
PreviousState string `json:"previous_state,omitempty"`
}
// DetachOptions specifies optional context for a detach operation.
type DetachOptions struct {
Operation string // "detach", "burn", "squash" - defaults to "detach"
Agent string // Who is performing the detach
Reason string // Optional reason for the detach
}
// DetachMoleculeWithAudit removes molecule attachment from a pinned bead and logs the operation.
// Returns the updated issue.
func (b *Beads) DetachMoleculeWithAudit(pinnedBeadID string, opts DetachOptions) (*Issue, error) {
// Fetch the pinned bead first to get previous state
issue, err := b.Show(pinnedBeadID)
if err != nil {
return nil, fmt.Errorf("fetching pinned bead: %w", err)
}
// Get current attachment info for audit
attachment := ParseAttachmentFields(issue)
if attachment == nil {
return issue, nil // Nothing to detach
}
// Log the detach operation
operation := opts.Operation
if operation == "" {
operation = "detach"
}
entry := DetachAuditEntry{
Timestamp: currentTimestamp(),
Operation: operation,
PinnedBeadID: pinnedBeadID,
DetachedMolecule: attachment.AttachedMolecule,
DetachedBy: opts.Agent,
Reason: opts.Reason,
PreviousState: issue.Status,
}
if err := b.LogDetachAudit(entry); err != nil {
// Log error but don't fail the detach operation
fmt.Fprintf(os.Stderr, "Warning: failed to write audit log: %v\n", err)
}
// Clear attachment fields by passing nil
newDesc := SetAttachmentFields(issue, nil)
// Update the issue
if err := b.Update(pinnedBeadID, UpdateOptions{Description: &newDesc}); err != nil {
return nil, fmt.Errorf("updating pinned bead: %w", err)
}
// Re-fetch to return updated state
return b.Show(pinnedBeadID)
}
// LogDetachAudit appends an audit entry to the audit log file.
// The audit log is stored in .beads/audit.log as JSONL format.
func (b *Beads) LogDetachAudit(entry DetachAuditEntry) error {
auditPath := filepath.Join(b.workDir, ".beads", "audit.log")
// Marshal entry to JSON
data, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("marshaling audit entry: %w", err)
}
// Append to audit log file
f, err := os.OpenFile(auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("opening audit log: %w", err)
}
defer f.Close()
if _, err := f.Write(append(data, '\n')); err != nil {
return fmt.Errorf("writing audit entry: %w", err)
}
return nil
}