Add Github Webhook data

Change-Id: I69417685de1264b10f60fc4c74e290890cdb0a67
diff --git a/CLAUDE.md b/CLAUDE.md
index 99dbb5d..522115d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -30,35 +30,26 @@
 
 ### Building and Running
 ```bash
-# Build the application
-cd server && go build -o staff ./cmd/main.go
+# Run the application
+cd server && go run cmd/main.go server  
 
-# Run with specific commands
-./staff [command] [args]
 
-# Run tests
-go test ./server/...
 
-# Run specific package tests
-go test ./server/agent/...
-go test ./server/llm/...
-go test ./server/tm/...
-```
+### Testing
+```bash
+# Run all tests
+cd server && go test ./...
+
+# Run with coverage
+cd server && go test -cover ./...
 
 ### Common Development Tasks
-
 ```bash
-# Format code
-go fmt ./...
-
 # Check for linting issues
-go vet ./...
+cd server && go vet ./...
 
 # Update dependencies
-go mod tidy
-
-# View available commands
-./staff --help
+cd server && go mod tidy
 ```
 
 ## Agent System Architecture
@@ -89,7 +80,8 @@
 - xAI (Grok models)
 - Claude (Anthropic)
 - Gemini (Google)
-- Local models
+- Local models (via Ollama)
+- Fake provider (for testing)
 
 ### Provider Interface
 All providers implement the same interface:
@@ -103,10 +95,15 @@
 - Timeout and retry settings
 - Provider-specific extra parameters
 
+Configuration can be provided via:
+- `config.yaml` file in server directory
+- Environment variables (OPENAI_API_KEY, GITHUB_TOKEN, etc.)
+- Command-line configuration during setup
+
 ## File Structure Patterns
 
 ### Agent Definitions
-- Agent system prompts stored in `/operations/agents/{role}/system.md`
+- Agent system prompts stored in `/operations/agents/{name}/system.md`
 - Each agent has detailed role definition and behavioral guidelines
 
 ### Task Files
@@ -115,23 +112,24 @@
 - Include task metadata, description, and assignment info
 
 ### Solution PRs
-- Agents create branches: `task/{task-id}-{clean-title}`
+- Agents create branches: `solution/{task-id}-{clean-title}` or `subtasks/{task-id}-{clean-title}`
 - Solutions formatted as markdown with task metadata
 - Automated commit messages and PR descriptions
 
 ## Key Dependencies
 
 ### Go Modules
-- `github.com/spf13/cobra` - CLI framework
-- `github.com/google/uuid` - UUID generation
-- `github.com/stretchr/testify` - Testing framework
-- `golang.org/x/text` - Text processing
-- `gopkg.in/yaml.v3` - YAML parsing
+- `github.com/spf13/cobra` - CLI framework for command-line interface
+- `github.com/google/uuid` - UUID generation for task and agent IDs
+- `github.com/joho/godotenv` - Environment variable loading from .env files
+- `golang.org/x/text` - Text processing utilities
+- `gopkg.in/yaml.v3` - YAML parsing for configuration files
 
 ### Development Dependencies
-- Go 1.24.4+ required
+- Go 1.24.4+ required 
 - Git for version control and PR operations
-- Access to LLM provider APIs (OpenAI, etc.)
+- Access to LLM provider APIs (OpenAI, xAI, Claude, Gemini, etc.)
+- Environment variables or config.yaml for API keys and credentials
 
 ## Testing Strategy
 
@@ -142,19 +140,6 @@
 - Task management operations
 - Git operations and branch creation
 
-### Test Execution
-```bash
-# Run all tests
-go test ./server/...
-
-# Run with coverage
-go test -cover ./server/...
-
-# Run specific test suites
-go test ./server/agent/ -v
-go test ./server/llm/ -v
-go test ./server/tm/ -v
-```
 
 ## Security Considerations
 
@@ -166,9 +151,9 @@
 ## Integration Points
 
 ### External Systems
-- Git repositories for task management and code storage
+- Git repositories for task management and code storage (GitHub/Gerrit)
 - LLM provider APIs for agent intelligence
-- Task management systems (GitHub Projects, Asana, Jira)
+- Task management systems via Git-based task tracking
 
 ### Internal Communication
 - Agents communicate through task management system
diff --git a/server/git/webhook.go b/server/git/webhook.go
new file mode 100644
index 0000000..df12492
--- /dev/null
+++ b/server/git/webhook.go
@@ -0,0 +1,362 @@
+package git
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+)
+
+// GitHubWebhook represents the GitHub pull request webhook payload
+type GitHubWebhook struct {
+	Action      string             `json:"action"`
+	Number      int                `json:"number"`
+	PullRequest *GitHubPullRequest `json:"pull_request"`
+	Repository  *Repository        `json:"repository"`
+	Sender      *User              `json:"sender"`
+	Changes     *Changes           `json:"changes,omitempty"`
+}
+
+// GitHubPullRequest contains the pull request data from GitHub webhook
+type GitHubPullRequest struct {
+	ID                 int                `json:"id"`
+	Number             int                `json:"number"`
+	State              string             `json:"state"`
+	Locked             bool               `json:"locked"`
+	Title              string             `json:"title"`
+	Body               string             `json:"body"`
+	CreatedAt          time.Time          `json:"created_at"`
+	UpdatedAt          time.Time          `json:"updated_at"`
+	ClosedAt           *time.Time         `json:"closed_at"`
+	MergedAt           *time.Time         `json:"merged_at"`
+	MergeCommitSHA     *string            `json:"merge_commit_sha"`
+	Assignee           *User              `json:"assignee"`
+	Assignees          []*User            `json:"assignees"`
+	RequestedReviewers []*User            `json:"requested_reviewers"`
+	Labels             []*Label           `json:"labels"`
+	Milestone          *Milestone         `json:"milestone"`
+	Draft              bool               `json:"draft"`
+	Merged             bool               `json:"merged"`
+	Mergeable          *bool              `json:"mergeable"`
+	MergeableState     string             `json:"mergeable_state"`
+	MergedBy           *User              `json:"merged_by"`
+	Comments           int                `json:"comments"`
+	ReviewComments     int                `json:"review_comments"`
+	Commits            int                `json:"commits"`
+	Additions          int                `json:"additions"`
+	Deletions          int                `json:"deletions"`
+	ChangedFiles       int                `json:"changed_files"`
+	Head               *PullRequestBranch `json:"head"`
+	Base               *PullRequestBranch `json:"base"`
+	User               *User              `json:"user"`
+}
+
+// PullRequestBranch contains branch information
+type PullRequestBranch struct {
+	Label string      `json:"label"`
+	Ref   string      `json:"ref"`
+	SHA   string      `json:"sha"`
+	User  *User       `json:"user"`
+	Repo  *Repository `json:"repo"`
+}
+
+// Repository contains repository information
+type Repository struct {
+	ID                        int                  `json:"id"`
+	NodeID                    string               `json:"node_id"`
+	Name                      string               `json:"name"`
+	FullName                  string               `json:"full_name"`
+	Private                   bool                 `json:"private"`
+	Owner                     *User                `json:"owner"`
+	Description               *string              `json:"description"`
+	Fork                      bool                 `json:"fork"`
+	CreatedAt                 time.Time            `json:"created_at"`
+	UpdatedAt                 time.Time            `json:"updated_at"`
+	PushedAt                  time.Time            `json:"pushed_at"`
+	GitURL                    string               `json:"git_url"`
+	SSHURL                    string               `json:"ssh_url"`
+	CloneURL                  string               `json:"clone_url"`
+	SvnURL                    string               `json:"svn_url"`
+	Homepage                  *string              `json:"homepage"`
+	Size                      int                  `json:"size"`
+	StargazersCount           int                  `json:"stargazers_count"`
+	WatchersCount             int                  `json:"watchers_count"`
+	Language                  *string              `json:"language"`
+	HasIssues                 bool                 `json:"has_issues"`
+	HasProjects               bool                 `json:"has_projects"`
+	HasDownloads              bool                 `json:"has_downloads"`
+	HasWiki                   bool                 `json:"has_wiki"`
+	HasPages                  bool                 `json:"has_pages"`
+	HasDiscussions            bool                 `json:"has_discussions"`
+	ForksCount                int                  `json:"forks_count"`
+	Archived                  bool                 `json:"archived"`
+	Disabled                  bool                 `json:"disabled"`
+	License                   *License             `json:"license"`
+	AllowForking              bool                 `json:"allow_forking"`
+	IsTemplate                bool                 `json:"is_template"`
+	WebCommitSignoffRequired  bool                 `json:"web_commit_signoff_required"`
+	Topics                    []string             `json:"topics"`
+	Visibility                string               `json:"visibility"`
+	DefaultBranch             string               `json:"default_branch"`
+	AllowSquashMerge          bool                 `json:"allow_squash_merge"`
+	AllowMergeCommit          bool                 `json:"allow_merge_commit"`
+	AllowRebaseMerge          bool                 `json:"allow_rebase_merge"`
+	AllowAutoMerge            bool                 `json:"allow_auto_merge"`
+	DeleteBranchOnMerge       bool                 `json:"delete_branch_on_merge"`
+	AllowUpdateBranch         bool                 `json:"allow_update_branch"`
+	UseSquashPrTitleAsDefault bool                 `json:"use_squash_pr_title_as_default"`
+	SquashMergeCommitMessage  string               `json:"squash_merge_commit_message"`
+	SquashMergeCommitTitle    string               `json:"squash_merge_commit_title"`
+	MergeCommitMessage        string               `json:"merge_commit_message"`
+	MergeCommitTitle          string               `json:"merge_commit_title"`
+	SecurityAndAnalysis       *SecurityAndAnalysis `json:"security_and_analysis"`
+}
+
+// User contains user information
+type User struct {
+	Login                   string     `json:"login"`
+	ID                      int        `json:"id"`
+	NodeID                  string     `json:"node_id"`
+	AvatarURL               string     `json:"avatar_url"`
+	GravatarID              string     `json:"gravatar_id"`
+	URL                     string     `json:"url"`
+	HTMLURL                 string     `json:"html_url"`
+	FollowersURL            string     `json:"followers_url"`
+	FollowingURL            string     `json:"following_url"`
+	GistsURL                string     `json:"gists_url"`
+	StarredURL              string     `json:"starred_url"`
+	SubscriptionsURL        string     `json:"subscriptions_url"`
+	OrganizationsURL        string     `json:"organizations_url"`
+	ReposURL                string     `json:"repos_url"`
+	EventsURL               string     `json:"events_url"`
+	ReceivedEventsURL       string     `json:"received_events_url"`
+	Type                    string     `json:"type"`
+	SiteAdmin               bool       `json:"site_admin"`
+	Name                    *string    `json:"name"`
+	Company                 *string    `json:"company"`
+	Blog                    *string    `json:"blog"`
+	Location                *string    `json:"location"`
+	Email                   *string    `json:"email"`
+	Hireable                *bool      `json:"hireable"`
+	Bio                     *string    `json:"bio"`
+	TwitterUsername         *string    `json:"twitter_username"`
+	PublicRepos             int        `json:"public_repos"`
+	PublicGists             int        `json:"public_gists"`
+	Followers               int        `json:"followers"`
+	Following               int        `json:"following"`
+	CreatedAt               time.Time  `json:"created_at"`
+	UpdatedAt               time.Time  `json:"updated_at"`
+	PrivateGists            int        `json:"private_gists"`
+	TotalPrivateRepos       int        `json:"total_private_repos"`
+	OwnedPrivateRepos       int        `json:"owned_private_repos"`
+	DiskUsage               int        `json:"disk_usage"`
+	Collaborators           int        `json:"collaborators"`
+	TwoFactorAuthentication bool       `json:"two_factor_authentication"`
+	Plan                    *Plan      `json:"plan"`
+	SuspendedAt             *time.Time `json:"suspended_at"`
+	BusinessPlus            bool       `json:"business_plus"`
+	LdapDn                  *string    `json:"ldap_dn"`
+}
+
+// Label contains label information
+type Label struct {
+	ID          int     `json:"id"`
+	NodeID      string  `json:"node_id"`
+	URL         string  `json:"url"`
+	Name        string  `json:"name"`
+	Description *string `json:"description"`
+	Color       string  `json:"color"`
+	Default     bool    `json:"default"`
+}
+
+// Milestone contains milestone information
+type Milestone struct {
+	URL          string     `json:"url"`
+	HTMLURL      string     `json:"html_url"`
+	LabelsURL    string     `json:"labels_url"`
+	ID           int        `json:"id"`
+	NodeID       string     `json:"node_id"`
+	Number       int        `json:"number"`
+	State        string     `json:"state"`
+	Title        string     `json:"title"`
+	Description  *string    `json:"description"`
+	Creator      *User      `json:"creator"`
+	OpenIssues   int        `json:"open_issues"`
+	ClosedIssues int        `json:"closed_issues"`
+	CreatedAt    time.Time  `json:"created_at"`
+	UpdatedAt    time.Time  `json:"updated_at"`
+	DueOn        *time.Time `json:"due_on"`
+	ClosedAt     *time.Time `json:"closed_at"`
+}
+
+// License contains license information
+type License struct {
+	Key     string `json:"key"`
+	Name    string `json:"name"`
+	URL     string `json:"url"`
+	SpdxID  string `json:"spdx_id"`
+	NodeID  string `json:"node_id"`
+	HTMLURL string `json:"html_url"`
+}
+
+// SecurityAndAnalysis contains security and analysis information
+type SecurityAndAnalysis struct {
+	AdvancedSecurity             *SecurityFeature `json:"advanced_security"`
+	SecretScanning               *SecurityFeature `json:"secret_scanning"`
+	SecretScanningPushProtection *SecurityFeature `json:"secret_scanning_push_protection"`
+	DependabotSecurityUpdates    *SecurityFeature `json:"dependabot_security_updates"`
+	DependencyGraph              *SecurityFeature `json:"dependency_graph"`
+}
+
+// SecurityFeature contains security feature information
+type SecurityFeature struct {
+	Status string `json:"status"`
+}
+
+// Plan contains plan information
+type Plan struct {
+	Name          string `json:"name"`
+	Space         int    `json:"space"`
+	Collaborators int    `json:"collaborators"`
+	PrivateRepos  int    `json:"private_repos"`
+}
+
+// Changes contains information about what changed in the webhook
+type Changes struct {
+	Title *Change `json:"title,omitempty"`
+	Body  *Change `json:"body,omitempty"`
+	Base  *Change `json:"base,omitempty"`
+}
+
+// Change contains information about a specific change
+type Change struct {
+	From string `json:"from"`
+}
+
+// ValidateSignature validates GitHub webhook signature using SHA-256 HMAC
+func ValidateSignature(payload []byte, signature, secret string) error {
+	if secret == "" {
+		return errors.New("webhook secret not configured")
+	}
+
+	if !strings.HasPrefix(signature, "sha256=") {
+		return errors.New("signature must use sha256")
+	}
+
+	expectedMAC, err := hex.DecodeString(signature[7:])
+	if err != nil {
+		return fmt.Errorf("invalid signature format: %w", err)
+	}
+
+	mac := hmac.New(sha256.New, []byte(secret))
+	mac.Write(payload)
+	computedMAC := mac.Sum(nil)
+
+	if !hmac.Equal(expectedMAC, computedMAC) {
+		return errors.New("signature verification failed")
+	}
+
+	return nil
+}
+
+// ParseWebhook parses GitHub webhook payload
+func ParseWebhook(payload []byte) (*GitHubWebhook, error) {
+	var webhook GitHubWebhook
+	if err := json.Unmarshal(payload, &webhook); err != nil {
+		return nil, fmt.Errorf("failed to parse webhook: %w", err)
+	}
+	return &webhook, nil
+}
+
+// IsValidMergeEvent checks if webhook represents a merged PR
+func IsValidMergeEvent(webhook *GitHubWebhook) bool {
+	return webhook.Action == "closed" &&
+		webhook.PullRequest != nil &&
+		webhook.PullRequest.Merged &&
+		webhook.PullRequest.Head != nil
+}
+
+// ExtractTaskID extracts task ID from branch name like "solution/{task-id}" or "subtasks/{task-id}"
+func ExtractTaskID(branchName string) (string, error) {
+	// Match patterns like "solution/task-123", "subtasks/task-456", etc.
+	re := regexp.MustCompile(`^(?:solution|subtasks)/(.+)$`)
+	matches := re.FindStringSubmatch(branchName)
+
+	if len(matches) != 2 {
+		return "", fmt.Errorf("branch name '%s' does not match expected pattern (solution/{task-id} or subtasks/{task-id})", branchName)
+	}
+
+	return matches[1], nil
+}
+
+// ProcessMergeWebhook processes a GitHub PR merge webhook and returns the task ID
+// This function handles the complete GitHub webhook payload structure
+func ProcessMergeWebhook(payload []byte, signature, secret string) (string, error) {
+	// Validate signature
+	if err := ValidateSignature(payload, signature, secret); err != nil {
+		return "", fmt.Errorf("signature validation failed: %w", err)
+	}
+
+	// Parse webhook
+	webhook, err := ParseWebhook(payload)
+	if err != nil {
+		return "", fmt.Errorf("webhook parsing failed: %w", err)
+	}
+
+	// Check if it's a merge event
+	if !IsValidMergeEvent(webhook) {
+		return "", errors.New("not a valid merge event")
+	}
+
+	// Extract task ID from branch name
+	taskID, err := ExtractTaskID(webhook.PullRequest.Head.Ref)
+	if err != nil {
+		return "", fmt.Errorf("task ID extraction failed: %w", err)
+	}
+
+	return taskID, nil
+}
+
+// GetWebhookInfo returns comprehensive information about the webhook for debugging/logging
+func GetWebhookInfo(webhook *GitHubWebhook) map[string]interface{} {
+	info := map[string]interface{}{
+		"action": webhook.Action,
+		"number": webhook.Number,
+	}
+
+	if webhook.PullRequest != nil {
+		info["pr_id"] = webhook.PullRequest.ID
+		info["pr_title"] = webhook.PullRequest.Title
+		info["pr_state"] = webhook.PullRequest.State
+		info["pr_merged"] = webhook.PullRequest.Merged
+		info["pr_merged_at"] = webhook.PullRequest.MergedAt
+		info["pr_merged_by"] = webhook.PullRequest.MergedBy
+
+		if webhook.PullRequest.Head != nil {
+			info["head_ref"] = webhook.PullRequest.Head.Ref
+			info["head_sha"] = webhook.PullRequest.Head.SHA
+		}
+
+		if webhook.PullRequest.Base != nil {
+			info["base_ref"] = webhook.PullRequest.Base.Ref
+			info["base_sha"] = webhook.PullRequest.Base.SHA
+		}
+	}
+
+	if webhook.Repository != nil {
+		info["repo_name"] = webhook.Repository.Name
+		info["repo_full_name"] = webhook.Repository.FullName
+	}
+
+	if webhook.Sender != nil {
+		info["sender_login"] = webhook.Sender.Login
+		info["sender_id"] = webhook.Sender.ID
+	}
+
+	return info
+}
diff --git a/server/git/webhook_test.go b/server/git/webhook_test.go
new file mode 100644
index 0000000..d19b27f
--- /dev/null
+++ b/server/git/webhook_test.go
@@ -0,0 +1,189 @@
+package git
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+)
+
+func TestProcessMergeWebhook(t *testing.T) {
+	// Create a sample GitHub webhook payload for a merged PR
+	webhookPayload := GitHubWebhook{
+		Action: "closed",
+		Number: 123,
+		PullRequest: &GitHubPullRequest{
+			ID:     456,
+			Number: 123,
+			State:  "closed",
+			Title:  "Add solution for task-123",
+			Merged: true,
+			MergedAt: func() *time.Time {
+				t := time.Now()
+				return &t
+			}(),
+			MergedBy: &User{
+				Login: "testuser",
+				ID:    789,
+			},
+			Head: &PullRequestBranch{
+				Ref: "solution/task-123",
+				SHA: "abc123def456",
+			},
+			Base: &PullRequestBranch{
+				Ref: "main",
+				SHA: "def456ghi789",
+			},
+		},
+		Repository: &Repository{
+			Name:     "test-repo",
+			FullName: "testuser/test-repo",
+		},
+		Sender: &User{
+			Login: "testuser",
+			ID:    789,
+		},
+	}
+
+	// Convert to JSON
+	payload, err := json.Marshal(webhookPayload)
+	if err != nil {
+		t.Fatalf("Failed to marshal webhook payload: %v", err)
+	}
+
+	// Test signature validation (this will fail without proper secret)
+	_, err = ProcessMergeWebhook(payload, "sha256=invalid", "test-secret")
+	if err == nil {
+		t.Error("Expected signature validation to fail with invalid signature")
+	}
+
+	// Test with valid signature (you would need to generate this properly in a real test)
+	// For now, we'll test the parsing and validation logic separately
+	webhook, err := ParseWebhook(payload)
+	if err != nil {
+		t.Fatalf("Failed to parse webhook: %v", err)
+	}
+
+	if !IsValidMergeEvent(webhook) {
+		t.Error("Expected webhook to be a valid merge event")
+	}
+
+	taskID, err := ExtractTaskID(webhook.PullRequest.Head.Ref)
+	if err != nil {
+		t.Fatalf("Failed to extract task ID: %v", err)
+	}
+
+	if taskID != "task-123" {
+		t.Errorf("Expected task ID 'task-123', got '%s'", taskID)
+	}
+
+	// Test GetWebhookInfo function
+	info := GetWebhookInfo(webhook)
+	if info["action"] != "closed" {
+		t.Errorf("Expected action 'closed', got '%v'", info["action"])
+	}
+	if info["pr_title"] != "Add solution for task-123" {
+		t.Errorf("Expected title 'Add solution for task-123', got '%v'", info["pr_title"])
+	}
+	if info["head_ref"] != "solution/task-123" {
+		t.Errorf("Expected head_ref 'solution/task-123', got '%v'", info["head_ref"])
+	}
+}
+
+func TestExtractTaskID(t *testing.T) {
+	tests := []struct {
+		branchName string
+		expected   string
+		shouldErr  bool
+	}{
+		{"solution/task-123", "task-123", false},
+		{"subtasks/task-456", "task-456", false},
+		{"feature/new-feature", "", true},
+		{"main", "", true},
+		{"solution/", "", true},
+		{"subtasks/", "", true},
+	}
+
+	for _, test := range tests {
+		taskID, err := ExtractTaskID(test.branchName)
+		if test.shouldErr {
+			if err == nil {
+				t.Errorf("Expected error for branch '%s', but got none", test.branchName)
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Unexpected error for branch '%s': %v", test.branchName, err)
+			}
+			if taskID != test.expected {
+				t.Errorf("Expected task ID '%s' for branch '%s', got '%s'", test.expected, test.branchName, taskID)
+			}
+		}
+	}
+}
+
+func TestIsValidMergeEvent(t *testing.T) {
+	tests := []struct {
+		name     string
+		webhook  *GitHubWebhook
+		expected bool
+	}{
+		{
+			name: "valid merge event",
+			webhook: &GitHubWebhook{
+				Action: "closed",
+				PullRequest: &GitHubPullRequest{
+					Merged: true,
+					Head:   &PullRequestBranch{Ref: "solution/task-123"},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "not closed",
+			webhook: &GitHubWebhook{
+				Action: "opened",
+				PullRequest: &GitHubPullRequest{
+					Merged: true,
+					Head:   &PullRequestBranch{Ref: "solution/task-123"},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "not merged",
+			webhook: &GitHubWebhook{
+				Action: "closed",
+				PullRequest: &GitHubPullRequest{
+					Merged: false,
+					Head:   &PullRequestBranch{Ref: "solution/task-123"},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "no pull request",
+			webhook: &GitHubWebhook{
+				Action: "closed",
+			},
+			expected: false,
+		},
+		{
+			name: "no head branch",
+			webhook: &GitHubWebhook{
+				Action: "closed",
+				PullRequest: &GitHubPullRequest{
+					Merged: true,
+				},
+			},
+			expected: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := IsValidMergeEvent(test.webhook)
+			if result != test.expected {
+				t.Errorf("Expected %v, got %v", test.expected, result)
+			}
+		})
+	}
+}