Detailed changes
@@ -370,14 +370,17 @@ it creates. You can customize this behavior with the `attribution` option:
"$schema": "https://charm.land/crush.json",
"options": {
"attribution": {
- "co_authored_by": true,
+ "trailer_style": "co-authored-by",
"generated_with": true
}
}
}
```
-- `co_authored_by`: When true (default), adds `Co-Authored-By: Crush <crush@charm.land>` to commit messages
+- `trailer_style`: Controls the attribution trailer added to commit messages (default: `co-authored-by`)
+ - `co-authored-by`: Adds `Co-Authored-By: Crush <crush@charm.land>`
+ - `assisted-by`: Adds `Assisted-by: [Model Name] via Crush` (includes the model name)
+ - `none`: No attribution trailer
- `generated_with`: When true (default), adds `💘 Generated with Crush` line to commit messages and PR descriptions
### Local Models
@@ -187,8 +187,15 @@ func coderAgent(r *recorder.Recorder, env fakeEnv, large, small fantasy.Language
if err != nil {
return nil, err
}
+
+ // Get the model name for the bash tool
+ modelName := large.Model() // fallback to ID if Name not available
+ if model := cfg.GetModel(large.Provider(), large.Model()); model != nil {
+ modelName = model.Name
+ }
+
allTools := []fantasy.AgentTool{
- tools.NewBashTool(env.permissions, env.workingDir, cfg.Options.Attribution),
+ tools.NewBashTool(env.permissions, env.workingDir, cfg.Options.Attribution, modelName),
tools.NewDownloadTool(env.permissions, env.workingDir, r.GetDefaultClient()),
tools.NewEditTool(env.lspClients, env.permissions, env.history, env.workingDir),
tools.NewMultiEditTool(env.lspClients, env.permissions, env.history, env.workingDir),
@@ -360,8 +360,16 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
allTools = append(allTools, agenticFetchTool)
}
+ // Get the model name for the agent
+ modelName := ""
+ if modelCfg, ok := c.cfg.Models[agent.Model]; ok {
+ if model := c.cfg.GetModel(modelCfg.Provider, modelCfg.Model); model != nil {
+ modelName = model.Name
+ }
+ }
+
allTools = append(allTools,
- tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
+ tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution, modelName),
tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
@@ -59,6 +59,7 @@ type bashDescriptionData struct {
BannedCommands string
MaxOutputLength int
Attribution config.Attribution
+ ModelName string
}
var bannedCommands = []string{
@@ -134,13 +135,14 @@ var bannedCommands = []string{
"ufw",
}
-func bashDescription(attribution *config.Attribution) string {
+func bashDescription(attribution *config.Attribution, modelName string) string {
bannedCommandsStr := strings.Join(bannedCommands, ", ")
var out bytes.Buffer
if err := bashDescriptionTpl.Execute(&out, bashDescriptionData{
BannedCommands: bannedCommandsStr,
MaxOutputLength: MaxOutputLength,
Attribution: *attribution,
+ ModelName: modelName,
}); err != nil {
// this should never happen.
panic("failed to execute bash description template: " + err.Error())
@@ -180,13 +182,13 @@ func blockFuncs() []shell.BlockFunc {
}
}
-func NewBashTool(permissions permission.Service, workingDir string, attribution *config.Attribution) fantasy.AgentTool {
+func NewBashTool(permissions permission.Service, workingDir string, attribution *config.Attribution, modelName string) fantasy.AgentTool {
// Set up command blocking on the persistent shell
persistentShell := shell.GetPersistentShell(workingDir)
persistentShell.SetBlockFuncs(blockFuncs())
return fantasy.NewAgentTool(
BashToolName,
- string(bashDescription(attribution)),
+ string(bashDescription(attribution, modelName)),
func(ctx context.Context, params BashParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Timeout > MaxTimeout {
params.Timeout = MaxTimeout
@@ -40,15 +40,21 @@ When user asks to create git commit:
- Use clear language, accurate reflection ("add"=new feature, "update"=enhancement, "fix"=bug fix)
- Avoid generic messages, review draft
-4. Create commit with Crush signature using HEREDOC:
+4. Create commit{{ if or (eq .Attribution.TrailerStyle "assisted-by") (eq .Attribution.TrailerStyle "co-authored-by")}} with attribution{{ end }} using HEREDOC:
git commit -m "$(cat <<'EOF'
Commit message here.
-{{ if .Attribution.GeneratedWith}}
+
+{{- if .Attribution.GeneratedWith}}
💘 Generated with Crush
-{{ end }}
-{{ if .Attribution.CoAuthoredBy}}
+{{- end}}
+{{- if eq .Attribution.TrailerStyle "assisted-by"}}
+
+ Assisted-by: {{ .ModelName }} via Crush
+{{- else if eq .Attribution.TrailerStyle "co-authored-by"}}
+
Co-Authored-By: Crush <crush@charm.land>
-{{ end }}
+{{- end}}
+
EOF
)"
@@ -0,0 +1,95 @@
+package config
+
+import (
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAttributionMigration(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ configJSON string
+ expectedTrailer TrailerStyle
+ expectedGenerate bool
+ }{
+ {
+ name: "old setting co_authored_by=true migrates to co-authored-by",
+ configJSON: `{
+ "options": {
+ "attribution": {
+ "co_authored_by": true,
+ "generated_with": false
+ }
+ }
+ }`,
+ expectedTrailer: TrailerStyleCoAuthoredBy,
+ expectedGenerate: false,
+ },
+ {
+ name: "old setting co_authored_by=false migrates to none",
+ configJSON: `{
+ "options": {
+ "attribution": {
+ "co_authored_by": false,
+ "generated_with": true
+ }
+ }
+ }`,
+ expectedTrailer: TrailerStyleNone,
+ expectedGenerate: true,
+ },
+ {
+ name: "new setting takes precedence over old setting",
+ configJSON: `{
+ "options": {
+ "attribution": {
+ "trailer_style": "assisted-by",
+ "co_authored_by": true,
+ "generated_with": false
+ }
+ }
+ }`,
+ expectedTrailer: TrailerStyleAssistedBy,
+ expectedGenerate: false,
+ },
+ {
+ name: "default when neither setting present",
+ configJSON: `{
+ "options": {
+ "attribution": {
+ "generated_with": true
+ }
+ }
+ }`,
+ expectedTrailer: TrailerStyleCoAuthoredBy,
+ expectedGenerate: true,
+ },
+ {
+ name: "default when attribution is null",
+ configJSON: `{
+ "options": {}
+ }`,
+ expectedTrailer: TrailerStyleCoAuthoredBy,
+ expectedGenerate: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ cfg, err := loadFromReaders([]io.Reader{strings.NewReader(tt.configJSON)})
+ require.NoError(t, err)
+
+ cfg.setDefaults(t.TempDir(), "")
+
+ require.Equal(t, tt.expectedTrailer, cfg.Options.Attribution.TrailerStyle)
+ require.Equal(t, tt.expectedGenerate, cfg.Options.Attribution.GeneratedWith)
+ })
+ }
+}
@@ -14,6 +14,7 @@ import (
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/env"
+ "github.com/invopop/jsonschema"
"github.com/tidwall/sjson"
)
@@ -166,9 +167,27 @@ type Permissions struct {
SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
}
+type TrailerStyle string
+
+const (
+ TrailerStyleNone TrailerStyle = "none"
+ TrailerStyleCoAuthoredBy TrailerStyle = "co-authored-by"
+ TrailerStyleAssistedBy TrailerStyle = "assisted-by"
+)
+
type Attribution struct {
- CoAuthoredBy bool `json:"co_authored_by,omitempty" jsonschema:"description=Add Co-Authored-By trailer to commit messages,default=true"`
- GeneratedWith bool `json:"generated_with,omitempty" jsonschema:"description=Add Generated with Crush line to commit messages and issues and PRs,default=true"`
+ TrailerStyle TrailerStyle `json:"trailer_style,omitempty" jsonschema:"description=Style of attribution trailer to add to commits,enum=none,enum=co-authored-by,enum=assisted-by,default=co-authored-by"`
+ CoAuthoredBy *bool `json:"co_authored_by,omitempty" jsonschema:"description=Deprecated: use trailer_style instead"`
+ GeneratedWith bool `json:"generated_with,omitempty" jsonschema:"description=Add Generated with Crush line to commit messages and issues and PRs,default=true"`
+}
+
+// JSONSchemaExtend marks the co_authored_by field as deprecated in the schema.
+func (Attribution) JSONSchemaExtend(schema *jsonschema.Schema) {
+ if schema.Properties != nil {
+ if prop, ok := schema.Properties.Get("co_authored_by"); ok {
+ prop.Deprecated = true
+ }
+ }
}
type Options struct {
@@ -363,9 +363,16 @@ func (c *Config) setDefaults(workingDir, dataDir string) {
if c.Options.Attribution == nil {
c.Options.Attribution = &Attribution{
- CoAuthoredBy: true,
+ TrailerStyle: TrailerStyleCoAuthoredBy,
GeneratedWith: true,
}
+ } else if c.Options.Attribution.TrailerStyle == "" {
+ // Migrate deprecated co_authored_by or apply default
+ if c.Options.Attribution.CoAuthoredBy != nil && !*c.Options.Attribution.CoAuthoredBy {
+ c.Options.Attribution.TrailerStyle = TrailerStyleNone
+ } else {
+ c.Options.Attribution.TrailerStyle = TrailerStyleCoAuthoredBy
+ }
}
}
@@ -5,10 +5,20 @@
"$defs": {
"Attribution": {
"properties": {
+ "trailer_style": {
+ "type": "string",
+ "enum": [
+ "none",
+ "co-authored-by",
+ "assisted-by"
+ ],
+ "description": "Style of attribution trailer to add to commits",
+ "default": "co-authored-by"
+ },
"co_authored_by": {
"type": "boolean",
- "description": "Add Co-Authored-By trailer to commit messages",
- "default": true
+ "description": "Deprecated: use trailer_style instead",
+ "deprecated": true
},
"generated_with": {
"type": "boolean",