From 4b75b15d536f5cea5863fdb08cac2afd08f9c70b Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 29 Dec 2025 21:08:49 -0800 Subject: [PATCH] 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. --- internal/cmd/commit.go | 237 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 internal/cmd/commit.go diff --git a/internal/cmd/commit.go b/internal/cmd/commit.go new file mode 100644 index 00000000..fee2f590 --- /dev/null +++ b/internal/cmd/commit.go @@ -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() +}