feat: add gt commit command with agent identity trailers (bd-luso)

Adds a new 'gt commit' command that wraps git commit and automatically
injects agent identity trailers into commit messages:

- Executed-By: agent identity (e.g., beads/crew/dave)
- Rig: the rig name
- Role: crew, polecat, witness, etc.
- Molecule: pinned molecule ID if any

This enables forensic analysis and audit trails for agent-mediated commits.

Supports common git commit flags: -a, --amend, --no-edit, --allow-empty.
Use --no-trailers to skip adding identity trailers.
This commit is contained in:
Steve Yegge
2025-12-29 21:08:49 -08:00
parent a087f7d8fc
commit 4b75b15d53

237
internal/cmd/commit.go Normal file
View File

@@ -0,0 +1,237 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
var (
commitMessage string
commitAll bool
commitAmend bool
commitNoEdit bool
commitAllowEmpty bool
commitDryRun bool
commitNoTrailers bool
commitIncludeMol bool
)
var commitCmd = &cobra.Command{
Use: "commit",
Short: "Git commit with agent identity trailers",
Long: `Create a git commit with agent identity metadata.
This command wraps git commit and automatically adds trailers identifying
the agent that performed the work:
Executed-By: beads/crew/dave
Rig: beads
Role: crew
For polecats:
Executed-By: beads/polecat/Nux-1766978911613
Rig: beads
Role: polecat
If a molecule is attached to your hook, it's also included:
Molecule: bd-xyz
This enables forensic analysis and audit trails for agent-mediated commits.
Examples:
gt commit -m "Fix authentication bug"
gt commit -a -m "Refactor login flow"
gt commit --no-trailers -m "Manual commit" # Skip agent trailers
gt commit --include-mol -m "Work on feature" # Include molecule even if not pinned`,
RunE: runCommit,
}
func init() {
commitCmd.Flags().StringVarP(&commitMessage, "message", "m", "", "Commit message")
commitCmd.Flags().BoolVarP(&commitAll, "all", "a", false, "Stage all modified files")
commitCmd.Flags().BoolVar(&commitAmend, "amend", false, "Amend the previous commit")
commitCmd.Flags().BoolVar(&commitNoEdit, "no-edit", false, "Use the selected commit message without editing")
commitCmd.Flags().BoolVar(&commitAllowEmpty, "allow-empty", false, "Allow empty commit")
commitCmd.Flags().BoolVarP(&commitDryRun, "dry-run", "n", false, "Show what would be committed")
commitCmd.Flags().BoolVar(&commitNoTrailers, "no-trailers", false, "Skip adding agent identity trailers")
commitCmd.Flags().BoolVar(&commitIncludeMol, "include-mol", true, "Include molecule ID if attached")
rootCmd.AddCommand(commitCmd)
}
// MoleculeStatus represents the output of gt mol status --json
type MoleculeStatus struct {
HasMolecule bool `json:"has_molecule"`
MoleculeID string `json:"molecule_id,omitempty"`
Title string `json:"title,omitempty"`
Status string `json:"status,omitempty"`
}
func runCommit(cmd *cobra.Command, args []string) error {
// Build git commit arguments
gitArgs := []string{"commit"}
if commitAll {
gitArgs = append(gitArgs, "-a")
}
if commitAmend {
gitArgs = append(gitArgs, "--amend")
}
if commitNoEdit {
gitArgs = append(gitArgs, "--no-edit")
}
if commitAllowEmpty {
gitArgs = append(gitArgs, "--allow-empty")
}
if commitDryRun {
gitArgs = append(gitArgs, "--dry-run")
}
// Build the commit message with trailers
message := commitMessage
if message == "" && !commitAmend && !commitNoEdit {
return fmt.Errorf("commit message required (-m)")
}
// Add agent trailers unless disabled
if !commitNoTrailers && message != "" {
trailers := buildAgentTrailers()
if len(trailers) > 0 {
message = appendTrailers(message, trailers)
}
}
// Add message to git args
if message != "" {
gitArgs = append(gitArgs, "-m", message)
}
// Add any extra args passed through
gitArgs = append(gitArgs, args...)
// Show what we're doing
if commitDryRun {
fmt.Printf("%s Would run: git %s\n", style.Bold.Render("🔍"), strings.Join(gitArgs, " "))
if !commitNoTrailers {
trailers := buildAgentTrailers()
if len(trailers) > 0 {
fmt.Printf("\n%s Trailers that would be added:\n", style.Bold.Render("📋"))
for _, t := range trailers {
fmt.Printf(" %s\n", t)
}
}
}
return nil
}
// Execute git commit
gitCmd := exec.Command("git", gitArgs...)
gitCmd.Stdout = os.Stdout
gitCmd.Stderr = os.Stderr
gitCmd.Stdin = os.Stdin
return gitCmd.Run()
}
// buildAgentTrailers constructs the trailers for agent identity.
func buildAgentTrailers() []string {
var trailers []string
// Get agent identity
cwd, err := os.Getwd()
if err != nil {
return trailers
}
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
return trailers
}
roleInfo, err := GetRoleWithContext(cwd, townRoot)
if err != nil {
return trailers
}
// Skip if not in an agent context (unknown/human)
if roleInfo.Role == RoleUnknown || roleInfo.Role == "" {
return trailers
}
// Build Executed-By trailer
ctx := RoleContext{
Role: roleInfo.Role,
Rig: roleInfo.Rig,
Polecat: roleInfo.Polecat,
TownRoot: townRoot,
WorkDir: cwd,
}
identity := buildAgentIdentity(ctx)
if identity != "" && identity != "overseer" {
trailers = append(trailers, fmt.Sprintf("Executed-By: %s", identity))
}
// Add Rig trailer
if roleInfo.Rig != "" {
trailers = append(trailers, fmt.Sprintf("Rig: %s", roleInfo.Rig))
}
// Add Role trailer
if roleInfo.Role != "" {
trailers = append(trailers, fmt.Sprintf("Role: %s", roleInfo.Role))
}
// Check for pinned molecule
if commitIncludeMol {
if molID := getPinnedMolecule(); molID != "" {
trailers = append(trailers, fmt.Sprintf("Molecule: %s", molID))
}
}
return trailers
}
// getPinnedMolecule checks if there's a molecule attached to the agent's hook.
func getPinnedMolecule() string {
// Try gt mol status --json
cmd := exec.Command("gt", "mol", "status", "--json")
out, err := cmd.Output()
if err != nil {
return ""
}
var status MoleculeStatus
if err := json.Unmarshal(out, &status); err != nil {
return ""
}
if status.HasMolecule && status.MoleculeID != "" {
return status.MoleculeID
}
return ""
}
// appendTrailers adds git trailers to a commit message.
// Trailers are separated from the message body by a blank line.
func appendTrailers(message string, trailers []string) string {
// Trim trailing whitespace from message
message = strings.TrimRight(message, "\n\r\t ")
// Add blank line separator and trailers
var sb strings.Builder
sb.WriteString(message)
sb.WriteString("\n\n")
for _, trailer := range trailers {
sb.WriteString(trailer)
sb.WriteString("\n")
}
return sb.String()
}