diff --git a/internal/cmd/commit.go b/internal/cmd/commit.go deleted file mode 100644 index fee2f590..00000000 --- a/internal/cmd/commit.go +++ /dev/null @@ -1,237 +0,0 @@ -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() -}