Merge pull request 'codex/bugseti-mcp' (#36) from codex/bugseti-mcp into new
Reviewed-on: host-uk/core#36
This commit is contained in:
commit
12974f472a
13 changed files with 820 additions and 33 deletions
4
.githooks/pre-commit
Executable file
4
.githooks/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
exec core go qa full --fix
|
||||
|
|
@ -9,6 +9,7 @@ interface Config {
|
|||
notificationsEnabled: boolean;
|
||||
notificationSound: boolean;
|
||||
workspaceDir: string;
|
||||
marketplaceMcpRoot: string;
|
||||
theme: string;
|
||||
autoSeedContext: boolean;
|
||||
workHours?: {
|
||||
|
|
@ -161,6 +162,13 @@ interface Config {
|
|||
<input type="text" class="form-input" [(ngModel)]="config.workspaceDir"
|
||||
placeholder="Leave empty for default">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Marketplace MCP Root</label>
|
||||
<input type="text" class="form-input" [(ngModel)]="config.marketplaceMcpRoot"
|
||||
placeholder="Path to core-agent (optional)">
|
||||
<p class="section-description">Override the marketplace MCP root. Leave empty to auto-detect.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -308,6 +316,7 @@ export class SettingsComponent implements OnInit {
|
|||
notificationsEnabled: true,
|
||||
notificationSound: true,
|
||||
workspaceDir: '',
|
||||
marketplaceMcpRoot: '',
|
||||
theme: 'dark',
|
||||
autoSeedContext: true,
|
||||
workHours: {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func main() {
|
|||
}
|
||||
|
||||
// Initialize core services
|
||||
notifyService := bugseti.NewNotifyService()
|
||||
notifyService := bugseti.NewNotifyService(configService)
|
||||
statsService := bugseti.NewStatsService(configService)
|
||||
fetcherService := bugseti.NewFetcherService(configService, notifyService)
|
||||
queueService := bugseti.NewQueueService(configService)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ type Config struct {
|
|||
// Workspace
|
||||
WorkspaceDir string `json:"workspaceDir,omitempty"`
|
||||
DataDir string `json:"dataDir,omitempty"`
|
||||
// Marketplace MCP
|
||||
MarketplaceMCPRoot string `json:"marketplaceMcpRoot,omitempty"`
|
||||
|
||||
// Onboarding
|
||||
Onboarded bool `json:"onboarded"`
|
||||
|
|
@ -96,6 +98,7 @@ func NewConfigService() *ConfigService {
|
|||
MaxConcurrentIssues: 1,
|
||||
AutoSeedContext: true,
|
||||
DataDir: bugsetiDir,
|
||||
MarketplaceMCPRoot: "",
|
||||
UpdateChannel: "stable",
|
||||
AutoUpdate: false,
|
||||
UpdateCheckInterval: 6, // Check every 6 hours
|
||||
|
|
@ -181,6 +184,13 @@ func (c *ConfigService) GetConfig() Config {
|
|||
return *c.config
|
||||
}
|
||||
|
||||
// GetMarketplaceMCPRoot returns the configured marketplace MCP root path.
|
||||
func (c *ConfigService) GetMarketplaceMCPRoot() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.config.MarketplaceMCPRoot
|
||||
}
|
||||
|
||||
// SetConfig updates the configuration and saves it.
|
||||
func (c *ConfigService) SetConfig(config Config) error {
|
||||
c.mu.Lock()
|
||||
|
|
|
|||
236
internal/bugseti/ethics_guard.go
Normal file
236
internal/bugseti/ethics_guard.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
// Package bugseti provides services for the BugSETI distributed bug fixing application.
|
||||
package bugseti
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
maxEnvRunes = 512
|
||||
maxTitleRunes = 160
|
||||
maxNotificationRunes = 200
|
||||
maxSummaryRunes = 4000
|
||||
maxBodyRunes = 8000
|
||||
maxFileRunes = 260
|
||||
)
|
||||
|
||||
type EthicsGuard struct {
|
||||
Modal string
|
||||
Axioms map[string]any
|
||||
Loaded bool
|
||||
}
|
||||
|
||||
var (
|
||||
ethicsGuardMu sync.Mutex
|
||||
ethicsGuard *EthicsGuard
|
||||
ethicsGuardRoot string
|
||||
)
|
||||
|
||||
func getEthicsGuard(ctx context.Context) *EthicsGuard {
|
||||
return getEthicsGuardWithRoot(ctx, "")
|
||||
}
|
||||
|
||||
func getEthicsGuardWithRoot(ctx context.Context, rootHint string) *EthicsGuard {
|
||||
rootHint = strings.TrimSpace(rootHint)
|
||||
|
||||
ethicsGuardMu.Lock()
|
||||
defer ethicsGuardMu.Unlock()
|
||||
|
||||
if ethicsGuard != nil && ethicsGuardRoot == rootHint {
|
||||
return ethicsGuard
|
||||
}
|
||||
|
||||
guard := loadEthicsGuard(ctx, rootHint)
|
||||
if guard == nil {
|
||||
guard = &EthicsGuard{}
|
||||
}
|
||||
|
||||
ethicsGuard = guard
|
||||
ethicsGuardRoot = rootHint
|
||||
if ethicsGuard == nil {
|
||||
return &EthicsGuard{}
|
||||
}
|
||||
return ethicsGuard
|
||||
}
|
||||
|
||||
func guardFromMarketplace(ctx context.Context, client marketplaceClient) *EthicsGuard {
|
||||
if client == nil {
|
||||
return &EthicsGuard{}
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
ethics, err := client.EthicsCheck(ctx)
|
||||
if err != nil || ethics == nil {
|
||||
return &EthicsGuard{}
|
||||
}
|
||||
|
||||
return &EthicsGuard{
|
||||
Modal: ethics.Modal,
|
||||
Axioms: ethics.Axioms,
|
||||
Loaded: true,
|
||||
}
|
||||
}
|
||||
|
||||
func loadEthicsGuard(ctx context.Context, rootHint string) *EthicsGuard {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
client, err := newMarketplaceClient(ctx, rootHint)
|
||||
if err != nil {
|
||||
return &EthicsGuard{}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ethics, err := client.EthicsCheck(ctx)
|
||||
if err != nil || ethics == nil {
|
||||
return &EthicsGuard{}
|
||||
}
|
||||
|
||||
return &EthicsGuard{
|
||||
Modal: ethics.Modal,
|
||||
Axioms: ethics.Axioms,
|
||||
Loaded: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *EthicsGuard) SanitizeEnv(value string) string {
|
||||
return sanitizeInline(value, maxEnvRunes)
|
||||
}
|
||||
|
||||
func (g *EthicsGuard) SanitizeTitle(value string) string {
|
||||
return sanitizeInline(value, maxTitleRunes)
|
||||
}
|
||||
|
||||
func (g *EthicsGuard) SanitizeNotification(value string) string {
|
||||
return sanitizeInline(value, maxNotificationRunes)
|
||||
}
|
||||
|
||||
func (g *EthicsGuard) SanitizeSummary(value string) string {
|
||||
return sanitizeMultiline(value, maxSummaryRunes)
|
||||
}
|
||||
|
||||
func (g *EthicsGuard) SanitizeBody(value string) string {
|
||||
return sanitizeMultiline(value, maxBodyRunes)
|
||||
}
|
||||
|
||||
func (g *EthicsGuard) SanitizeFiles(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
clean := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
trimmed := sanitizeInline(value, maxFileRunes)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, "..") {
|
||||
continue
|
||||
}
|
||||
if seen[trimmed] {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = true
|
||||
clean = append(clean, trimmed)
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
func (g *EthicsGuard) SanitizeList(values []string, maxRunes int) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
if maxRunes <= 0 {
|
||||
maxRunes = maxTitleRunes
|
||||
}
|
||||
clean := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
trimmed := sanitizeInline(value, maxRunes)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
clean = append(clean, trimmed)
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
func sanitizeInline(input string, maxRunes int) string {
|
||||
return sanitizeText(input, maxRunes, false)
|
||||
}
|
||||
|
||||
func sanitizeMultiline(input string, maxRunes int) string {
|
||||
return sanitizeText(input, maxRunes, true)
|
||||
}
|
||||
|
||||
func sanitizeText(input string, maxRunes int, allowNewlines bool) string {
|
||||
if input == "" {
|
||||
return ""
|
||||
}
|
||||
if maxRunes <= 0 {
|
||||
maxRunes = maxSummaryRunes
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
count := 0
|
||||
for _, r := range input {
|
||||
if r == '\r' {
|
||||
continue
|
||||
}
|
||||
if r == '\n' {
|
||||
if allowNewlines {
|
||||
b.WriteRune(r)
|
||||
count++
|
||||
} else {
|
||||
b.WriteRune(' ')
|
||||
count++
|
||||
}
|
||||
if count >= maxRunes {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if r == '\t' {
|
||||
b.WriteRune(' ')
|
||||
count++
|
||||
if count >= maxRunes {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if r < 0x20 || r == 0x7f {
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
count++
|
||||
if count >= maxRunes {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func escapeAppleScript(value string) string {
|
||||
value = strings.ReplaceAll(value, "\\", "\\\\")
|
||||
value = strings.ReplaceAll(value, "\"", "\\\"")
|
||||
return value
|
||||
}
|
||||
|
||||
func escapePowerShellXML(value string) string {
|
||||
var buffer bytes.Buffer
|
||||
_ = xml.EscapeText(&buffer, []byte(value))
|
||||
return buffer.String()
|
||||
}
|
||||
28
internal/bugseti/ethics_guard_test.go
Normal file
28
internal/bugseti/ethics_guard_test.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package bugseti
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSanitizeInline_Good(t *testing.T) {
|
||||
input := "Hello world"
|
||||
output := sanitizeInline(input, 50)
|
||||
if output != input {
|
||||
t.Fatalf("expected %q, got %q", input, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeInline_Bad(t *testing.T) {
|
||||
input := "Hello\nworld\t\x00"
|
||||
expected := "Hello world"
|
||||
output := sanitizeInline(input, 50)
|
||||
if output != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeMultiline_Ugly(t *testing.T) {
|
||||
input := "ab\ncd\tef\x00"
|
||||
output := sanitizeMultiline(input, 5)
|
||||
if output != "ab\ncd" {
|
||||
t.Fatalf("expected %q, got %q", "ab\ncd", output)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,17 @@
|
|||
module github.com/host-uk/core/internal/bugseti
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/mark3labs/mcp-go v0.43.2
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
39
internal/bugseti/go.sum
Normal file
39
internal/bugseti/go.sum
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
246
internal/bugseti/mcp_marketplace.go
Normal file
246
internal/bugseti/mcp_marketplace.go
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
// Package bugseti provides services for the BugSETI distributed bug fixing application.
|
||||
package bugseti
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type Marketplace struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Owner MarketplaceOwner `json:"owner"`
|
||||
Plugins []MarketplacePlugin `json:"plugins"`
|
||||
}
|
||||
|
||||
type MarketplaceOwner struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type MarketplacePlugin struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
Source string `json:"source"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
type PluginInfo struct {
|
||||
Plugin MarketplacePlugin `json:"plugin"`
|
||||
Path string `json:"path"`
|
||||
Manifest map[string]any `json:"manifest,omitempty"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
Skills []string `json:"skills,omitempty"`
|
||||
}
|
||||
|
||||
type EthicsContext struct {
|
||||
Modal string `json:"modal"`
|
||||
Axioms map[string]any `json:"axioms"`
|
||||
}
|
||||
|
||||
type marketplaceClient interface {
|
||||
ListMarketplace(ctx context.Context) ([]MarketplacePlugin, error)
|
||||
PluginInfo(ctx context.Context, name string) (*PluginInfo, error)
|
||||
EthicsCheck(ctx context.Context) (*EthicsContext, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type mcpMarketplaceClient struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
func newMarketplaceClient(ctx context.Context, rootHint string) (marketplaceClient, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
command, args, err := resolveMarketplaceCommand(rootHint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mcpClient, err := client.NewStdioMCPClient(command, nil, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start marketplace MCP client: %w", err)
|
||||
}
|
||||
|
||||
initRequest := mcp.InitializeRequest{}
|
||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||
initRequest.Params.ClientInfo = mcp.Implementation{
|
||||
Name: "bugseti",
|
||||
Version: GetVersion(),
|
||||
}
|
||||
|
||||
initCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
if _, err := mcpClient.Initialize(initCtx, initRequest); err != nil {
|
||||
_ = mcpClient.Close()
|
||||
return nil, fmt.Errorf("failed to initialize marketplace MCP client: %w", err)
|
||||
}
|
||||
|
||||
return &mcpMarketplaceClient{client: mcpClient}, nil
|
||||
}
|
||||
|
||||
func (c *mcpMarketplaceClient) Close() error {
|
||||
if c == nil || c.client == nil {
|
||||
return nil
|
||||
}
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *mcpMarketplaceClient) ListMarketplace(ctx context.Context) ([]MarketplacePlugin, error) {
|
||||
var marketplace Marketplace
|
||||
if err := c.callToolStructured(ctx, "marketplace_list", nil, &marketplace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marketplace.Plugins, nil
|
||||
}
|
||||
|
||||
func (c *mcpMarketplaceClient) PluginInfo(ctx context.Context, name string) (*PluginInfo, error) {
|
||||
var info PluginInfo
|
||||
args := map[string]any{"name": name}
|
||||
if err := c.callToolStructured(ctx, "marketplace_plugin_info", args, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (c *mcpMarketplaceClient) EthicsCheck(ctx context.Context) (*EthicsContext, error) {
|
||||
var ethics EthicsContext
|
||||
if err := c.callToolStructured(ctx, "ethics_check", nil, ðics); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ðics, nil
|
||||
}
|
||||
|
||||
func (c *mcpMarketplaceClient) callToolStructured(ctx context.Context, name string, args map[string]any, target any) error {
|
||||
if c == nil || c.client == nil {
|
||||
return errors.New("marketplace client is not initialized")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
request := mcp.CallToolRequest{}
|
||||
request.Params.Name = name
|
||||
if args != nil {
|
||||
request.Params.Arguments = args
|
||||
}
|
||||
|
||||
result, err := c.client.CallTool(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil {
|
||||
return errors.New("marketplace tool returned no result")
|
||||
}
|
||||
if result.IsError {
|
||||
return fmt.Errorf("marketplace tool %s error: %s", name, toolResultMessage(result))
|
||||
}
|
||||
if result.StructuredContent == nil {
|
||||
return fmt.Errorf("marketplace tool %s returned no structured content", name)
|
||||
}
|
||||
payload, err := json.Marshal(result.StructuredContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode marketplace response: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(payload, target); err != nil {
|
||||
return fmt.Errorf("failed to decode marketplace response: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toolResultMessage(result *mcp.CallToolResult) string {
|
||||
if result == nil {
|
||||
return "unknown error"
|
||||
}
|
||||
for _, content := range result.Content {
|
||||
switch value := content.(type) {
|
||||
case mcp.TextContent:
|
||||
if value.Text != "" {
|
||||
return value.Text
|
||||
}
|
||||
case *mcp.TextContent:
|
||||
if value != nil && value.Text != "" {
|
||||
return value.Text
|
||||
}
|
||||
}
|
||||
}
|
||||
return "unknown error"
|
||||
}
|
||||
|
||||
func resolveMarketplaceCommand(rootHint string) (string, []string, error) {
|
||||
if command := strings.TrimSpace(os.Getenv("BUGSETI_MCP_COMMAND")); command != "" {
|
||||
args := strings.Fields(os.Getenv("BUGSETI_MCP_ARGS"))
|
||||
return command, args, nil
|
||||
}
|
||||
|
||||
if root := strings.TrimSpace(rootHint); root != "" {
|
||||
path := filepath.Join(root, "mcp")
|
||||
return "go", []string{"run", path}, nil
|
||||
}
|
||||
|
||||
if root := strings.TrimSpace(os.Getenv("BUGSETI_MCP_ROOT")); root != "" {
|
||||
path := filepath.Join(root, "mcp")
|
||||
return "go", []string{"run", path}, nil
|
||||
}
|
||||
|
||||
if root, ok := findCoreAgentRoot(); ok {
|
||||
return "go", []string{"run", filepath.Join(root, "mcp")}, nil
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("marketplace MCP server not configured (set BUGSETI_MCP_COMMAND or BUGSETI_MCP_ROOT)")
|
||||
}
|
||||
|
||||
func findCoreAgentRoot() (string, bool) {
|
||||
var candidates []string
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
candidates = append(candidates, cwd)
|
||||
candidates = append(candidates, filepath.Dir(cwd))
|
||||
}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
exeDir := filepath.Dir(exe)
|
||||
candidates = append(candidates, exeDir)
|
||||
candidates = append(candidates, filepath.Dir(exeDir))
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for _, base := range candidates {
|
||||
base = filepath.Clean(base)
|
||||
if seen[base] {
|
||||
continue
|
||||
}
|
||||
seen[base] = true
|
||||
|
||||
root := filepath.Join(base, "core-agent")
|
||||
if hasMcpDir(root) {
|
||||
return root, true
|
||||
}
|
||||
|
||||
root = filepath.Join(base, "..", "core-agent")
|
||||
if hasMcpDir(root) {
|
||||
return filepath.Clean(root), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func hasMcpDir(root string) bool {
|
||||
if root == "" {
|
||||
return false
|
||||
}
|
||||
info, err := os.Stat(filepath.Join(root, "mcp", "main.go"))
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
|
@ -14,13 +14,15 @@ import (
|
|||
type NotifyService struct {
|
||||
enabled bool
|
||||
sound bool
|
||||
config *ConfigService
|
||||
}
|
||||
|
||||
// NewNotifyService creates a new NotifyService.
|
||||
func NewNotifyService() *NotifyService {
|
||||
func NewNotifyService(config *ConfigService) *NotifyService {
|
||||
return &NotifyService{
|
||||
enabled: true,
|
||||
sound: true,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +47,11 @@ func (n *NotifyService) Notify(title, message string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Notification: %s - %s", title, message)
|
||||
guard := getEthicsGuardWithRoot(context.Background(), n.getMarketplaceRoot())
|
||||
safeTitle := guard.SanitizeNotification(title)
|
||||
safeMessage := guard.SanitizeNotification(message)
|
||||
|
||||
log.Printf("Notification: %s - %s", safeTitle, safeMessage)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
|
@ -53,11 +59,11 @@ func (n *NotifyService) Notify(title, message string) error {
|
|||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
err = n.notifyMacOS(ctx, title, message)
|
||||
err = n.notifyMacOS(ctx, safeTitle, safeMessage)
|
||||
case "linux":
|
||||
err = n.notifyLinux(ctx, title, message)
|
||||
err = n.notifyLinux(ctx, safeTitle, safeMessage)
|
||||
case "windows":
|
||||
err = n.notifyWindows(ctx, title, message)
|
||||
err = n.notifyWindows(ctx, safeTitle, safeMessage)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform: %s", runtime.GOOS)
|
||||
}
|
||||
|
|
@ -68,6 +74,13 @@ func (n *NotifyService) Notify(title, message string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (n *NotifyService) getMarketplaceRoot() string {
|
||||
if n == nil || n.config == nil {
|
||||
return ""
|
||||
}
|
||||
return n.config.GetMarketplaceMCPRoot()
|
||||
}
|
||||
|
||||
// NotifyIssue sends a notification about a new issue.
|
||||
func (n *NotifyService) NotifyIssue(issue *Issue) error {
|
||||
title := "New Issue Available"
|
||||
|
|
@ -84,7 +97,7 @@ func (n *NotifyService) NotifyPRStatus(repo string, prNumber int, status string)
|
|||
|
||||
// notifyMacOS sends a notification on macOS using osascript.
|
||||
func (n *NotifyService) notifyMacOS(ctx context.Context, title, message string) error {
|
||||
script := fmt.Sprintf(`display notification "%s" with title "%s"`, message, title)
|
||||
script := fmt.Sprintf(`display notification "%s" with title "%s"`, escapeAppleScript(message), escapeAppleScript(title))
|
||||
if n.sound {
|
||||
script += ` sound name "Glass"`
|
||||
}
|
||||
|
|
@ -106,6 +119,9 @@ func (n *NotifyService) notifyLinux(ctx context.Context, title, message string)
|
|||
|
||||
// notifyWindows sends a notification on Windows using PowerShell.
|
||||
func (n *NotifyService) notifyWindows(ctx context.Context, title, message string) error {
|
||||
title = escapePowerShellXML(title)
|
||||
message = escapePowerShellXML(message)
|
||||
|
||||
script := fmt.Sprintf(`
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ func (s *SeederService) SeedIssue(issue *Issue) (*IssueContext, error) {
|
|||
if err != nil {
|
||||
log.Printf("Seed skill failed, using fallback: %v", err)
|
||||
// Fallback to basic context preparation
|
||||
ctx = s.prepareBasicContext(issue)
|
||||
guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot())
|
||||
ctx = s.prepareBasicContext(issue, guard)
|
||||
}
|
||||
|
||||
ctx.PreparedAt = time.Now()
|
||||
|
|
@ -91,22 +92,20 @@ func (s *SeederService) runSeedSkill(issue *Issue, workDir string) (*IssueContex
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Look for the plugin script
|
||||
pluginPaths := []string{
|
||||
"/home/shared/hostuk/claude-plugins/agentic-flows/skills/seed-agent-developer/scripts/analyze-issue.sh",
|
||||
filepath.Join(os.Getenv("HOME"), ".claude/plugins/agentic-flows/skills/seed-agent-developer/scripts/analyze-issue.sh"),
|
||||
}
|
||||
mcpCtx, mcpCancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer mcpCancel()
|
||||
|
||||
var scriptPath string
|
||||
for _, p := range pluginPaths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
scriptPath = p
|
||||
break
|
||||
}
|
||||
marketplace, err := newMarketplaceClient(mcpCtx, s.config.GetMarketplaceMCPRoot())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer marketplace.Close()
|
||||
|
||||
if scriptPath == "" {
|
||||
return nil, fmt.Errorf("seed-agent-developer skill not found")
|
||||
guard := guardFromMarketplace(mcpCtx, marketplace)
|
||||
|
||||
scriptPath, err := findSeedSkillScript(mcpCtx, marketplace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Run the analyze-issue script
|
||||
|
|
@ -114,9 +113,9 @@ func (s *SeederService) runSeedSkill(issue *Issue, workDir string) (*IssueContex
|
|||
cmd.Dir = workDir
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("ISSUE_NUMBER=%d", issue.Number),
|
||||
fmt.Sprintf("ISSUE_REPO=%s", issue.Repo),
|
||||
fmt.Sprintf("ISSUE_TITLE=%s", issue.Title),
|
||||
fmt.Sprintf("ISSUE_URL=%s", issue.URL),
|
||||
fmt.Sprintf("ISSUE_REPO=%s", guard.SanitizeEnv(issue.Repo)),
|
||||
fmt.Sprintf("ISSUE_TITLE=%s", guard.SanitizeEnv(issue.Title)),
|
||||
fmt.Sprintf("ISSUE_URL=%s", guard.SanitizeEnv(issue.URL)),
|
||||
)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
|
@ -139,36 +138,36 @@ func (s *SeederService) runSeedSkill(issue *Issue, workDir string) (*IssueContex
|
|||
|
||||
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
|
||||
// If not JSON, treat as plain text summary
|
||||
return &IssueContext{
|
||||
return sanitizeIssueContext(&IssueContext{
|
||||
Summary: stdout.String(),
|
||||
Complexity: "unknown",
|
||||
}, nil
|
||||
}, guard), nil
|
||||
}
|
||||
|
||||
return &IssueContext{
|
||||
return sanitizeIssueContext(&IssueContext{
|
||||
Summary: result.Summary,
|
||||
RelevantFiles: result.RelevantFiles,
|
||||
SuggestedFix: result.SuggestedFix,
|
||||
RelatedIssues: result.RelatedIssues,
|
||||
Complexity: result.Complexity,
|
||||
EstimatedTime: result.EstimatedTime,
|
||||
}, nil
|
||||
}, guard), nil
|
||||
}
|
||||
|
||||
// prepareBasicContext creates a basic context without the seed skill.
|
||||
func (s *SeederService) prepareBasicContext(issue *Issue) *IssueContext {
|
||||
func (s *SeederService) prepareBasicContext(issue *Issue, guard *EthicsGuard) *IssueContext {
|
||||
// Extract potential file references from issue body
|
||||
files := extractFileReferences(issue.Body)
|
||||
|
||||
// Estimate complexity based on labels and body length
|
||||
complexity := estimateComplexity(issue)
|
||||
|
||||
return &IssueContext{
|
||||
return sanitizeIssueContext(&IssueContext{
|
||||
Summary: fmt.Sprintf("Issue #%d in %s: %s", issue.Number, issue.Repo, issue.Title),
|
||||
RelevantFiles: files,
|
||||
Complexity: complexity,
|
||||
EstimatedTime: estimateTime(complexity),
|
||||
}
|
||||
}, guard)
|
||||
}
|
||||
|
||||
// sanitizeRepoName converts owner/repo to a safe directory name.
|
||||
|
|
@ -256,6 +255,87 @@ func estimateTime(complexity string) string {
|
|||
}
|
||||
}
|
||||
|
||||
const seedSkillName = "seed-agent-developer"
|
||||
|
||||
func findSeedSkillScript(ctx context.Context, marketplace marketplaceClient) (string, error) {
|
||||
if marketplace == nil {
|
||||
return "", fmt.Errorf("marketplace client is nil")
|
||||
}
|
||||
|
||||
plugins, err := marketplace.ListMarketplace(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
info, err := marketplace.PluginInfo(ctx, plugin.Name)
|
||||
if err != nil || info == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !containsSkill(info.Skills, seedSkillName) {
|
||||
continue
|
||||
}
|
||||
|
||||
scriptPath, err := safeJoinUnder(info.Path, "skills", seedSkillName, "scripts", "analyze-issue.sh")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if stat, err := os.Stat(scriptPath); err == nil && !stat.IsDir() {
|
||||
return scriptPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("seed-agent-developer skill not found in marketplace")
|
||||
}
|
||||
|
||||
func containsSkill(skills []string, name string) bool {
|
||||
for _, skill := range skills {
|
||||
if skill == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func safeJoinUnder(base string, elems ...string) (string, error) {
|
||||
if base == "" {
|
||||
return "", fmt.Errorf("base path is empty")
|
||||
}
|
||||
baseAbs, err := filepath.Abs(base)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve base path: %w", err)
|
||||
}
|
||||
|
||||
joined := filepath.Join(append([]string{baseAbs}, elems...)...)
|
||||
rel, err := filepath.Rel(baseAbs, joined)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve relative path: %w", err)
|
||||
}
|
||||
if strings.HasPrefix(rel, "..") {
|
||||
return "", fmt.Errorf("resolved path escapes base: %s", rel)
|
||||
}
|
||||
|
||||
return joined, nil
|
||||
}
|
||||
|
||||
func sanitizeIssueContext(ctx *IssueContext, guard *EthicsGuard) *IssueContext {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
if guard == nil {
|
||||
guard = &EthicsGuard{}
|
||||
}
|
||||
|
||||
ctx.Summary = guard.SanitizeSummary(ctx.Summary)
|
||||
ctx.SuggestedFix = guard.SanitizeSummary(ctx.SuggestedFix)
|
||||
ctx.Complexity = guard.SanitizeTitle(ctx.Complexity)
|
||||
ctx.EstimatedTime = guard.SanitizeTitle(ctx.EstimatedTime)
|
||||
ctx.RelatedIssues = guard.SanitizeList(ctx.RelatedIssues, maxTitleRunes)
|
||||
ctx.RelevantFiles = guard.SanitizeFiles(ctx.RelevantFiles)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// GetWorkspaceDir returns the workspace directory for an issue.
|
||||
func (s *SeederService) GetWorkspaceDir(issue *Issue) string {
|
||||
baseDir := s.config.GetWorkspaceDir()
|
||||
|
|
|
|||
97
internal/bugseti/seeder_test.go
Normal file
97
internal/bugseti/seeder_test.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package bugseti
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeMarketplaceClient struct {
|
||||
plugins []MarketplacePlugin
|
||||
infos map[string]*PluginInfo
|
||||
listErr error
|
||||
infoErr map[string]error
|
||||
}
|
||||
|
||||
func (f *fakeMarketplaceClient) ListMarketplace(ctx context.Context) ([]MarketplacePlugin, error) {
|
||||
if f.listErr != nil {
|
||||
return nil, f.listErr
|
||||
}
|
||||
return f.plugins, nil
|
||||
}
|
||||
|
||||
func (f *fakeMarketplaceClient) PluginInfo(ctx context.Context, name string) (*PluginInfo, error) {
|
||||
if err, ok := f.infoErr[name]; ok {
|
||||
return nil, err
|
||||
}
|
||||
info, ok := f.infos[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("plugin not found")
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (f *fakeMarketplaceClient) EthicsCheck(ctx context.Context) (*EthicsContext, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (f *fakeMarketplaceClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestFindSeedSkillScript_Good(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
scriptPath := filepath.Join(root, "skills", seedSkillName, "scripts", "analyze-issue.sh")
|
||||
if err := os.MkdirAll(filepath.Dir(scriptPath), 0755); err != nil {
|
||||
t.Fatalf("failed to create script directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(scriptPath, []byte("#!/bin/bash\n"), 0755); err != nil {
|
||||
t.Fatalf("failed to write script: %v", err)
|
||||
}
|
||||
|
||||
plugin := MarketplacePlugin{Name: "seed-plugin"}
|
||||
client := &fakeMarketplaceClient{
|
||||
plugins: []MarketplacePlugin{plugin},
|
||||
infos: map[string]*PluginInfo{
|
||||
plugin.Name: {
|
||||
Plugin: plugin,
|
||||
Path: root,
|
||||
Skills: []string{seedSkillName},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
found, err := findSeedSkillScript(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatalf("expected script path, got error: %v", err)
|
||||
}
|
||||
if found != scriptPath {
|
||||
t.Fatalf("expected %q, got %q", scriptPath, found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindSeedSkillScript_Bad(t *testing.T) {
|
||||
plugin := MarketplacePlugin{Name: "empty-plugin"}
|
||||
client := &fakeMarketplaceClient{
|
||||
plugins: []MarketplacePlugin{plugin},
|
||||
infos: map[string]*PluginInfo{
|
||||
plugin.Name: {
|
||||
Plugin: plugin,
|
||||
Path: t.TempDir(),
|
||||
Skills: []string{"not-the-skill"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := findSeedSkillScript(context.Background(), client); err == nil {
|
||||
t.Fatal("expected error when skill is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeJoinUnder_Ugly(t *testing.T) {
|
||||
if _, err := safeJoinUnder("", "skills"); err == nil {
|
||||
t.Fatal("expected error for empty base path")
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,9 @@ func (s *SubmitService) Submit(submission *PRSubmission) (*PRResult, error) {
|
|||
return nil, fmt.Errorf("work directory not specified")
|
||||
}
|
||||
|
||||
guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot())
|
||||
issueTitle := guard.SanitizeTitle(issue.Title)
|
||||
|
||||
// Step 1: Ensure we have a fork
|
||||
forkOwner, err := s.ensureFork(issue.Repo)
|
||||
if err != nil {
|
||||
|
|
@ -85,7 +88,9 @@ func (s *SubmitService) Submit(submission *PRSubmission) (*PRResult, error) {
|
|||
// Step 3: Stage and commit changes
|
||||
commitMsg := submission.CommitMsg
|
||||
if commitMsg == "" {
|
||||
commitMsg = fmt.Sprintf("fix: resolve issue #%d\n\n%s\n\nFixes #%d", issue.Number, issue.Title, issue.Number)
|
||||
commitMsg = fmt.Sprintf("fix: resolve issue #%d\n\n%s\n\nFixes #%d", issue.Number, issueTitle, issue.Number)
|
||||
} else {
|
||||
commitMsg = guard.SanitizeBody(commitMsg)
|
||||
}
|
||||
if err := s.commitChanges(workDir, submission.Files, commitMsg); err != nil {
|
||||
return &PRResult{Success: false, Error: fmt.Sprintf("commit failed: %v", err)}, err
|
||||
|
|
@ -99,12 +104,15 @@ func (s *SubmitService) Submit(submission *PRSubmission) (*PRResult, error) {
|
|||
// Step 5: Create PR
|
||||
prTitle := submission.Title
|
||||
if prTitle == "" {
|
||||
prTitle = fmt.Sprintf("Fix #%d: %s", issue.Number, issue.Title)
|
||||
prTitle = fmt.Sprintf("Fix #%d: %s", issue.Number, issueTitle)
|
||||
} else {
|
||||
prTitle = guard.SanitizeTitle(prTitle)
|
||||
}
|
||||
prBody := submission.Body
|
||||
if prBody == "" {
|
||||
prBody = s.generatePRBody(issue)
|
||||
}
|
||||
prBody = guard.SanitizeBody(prBody)
|
||||
|
||||
prURL, prNumber, err := s.createPR(issue.Repo, forkOwner, branch, prTitle, prBody)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue