diff --git a/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml b/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml index 188a698a59..ba22aecd25 100644 --- a/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml +++ b/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml @@ -1624,7 +1624,7 @@ jobs: BRANCH_NAME: memory/campaigns MAX_FILE_SIZE: 10240 MAX_FILE_COUNT: 100 - FILE_GLOB_FILTER: "go-file-size-reduction-project64/**" + FILE_GLOB_FILTER: "go-file-size-reduction-project64-*/**" with: script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); diff --git a/.github/workflows/go-file-size-reduction-project64.campaign.g.md b/.github/workflows/go-file-size-reduction-project64.campaign.g.md index 0112fff9e8..cb69b9a016 100644 --- a/.github/workflows/go-file-size-reduction-project64.campaign.g.md +++ b/.github/workflows/go-file-size-reduction-project64.campaign.g.md @@ -32,7 +32,7 @@ tools: repo-memory: - branch-name: memory/campaigns file-glob: - - go-file-size-reduction-project64/** + - go-file-size-reduction-project64-*/** id: campaigns --- diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 233b8f39bf..31bd8d077f 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -353,38 +353,38 @@ jobs: "name": "noop" }, { - "description": "Post a message to a Slack channel. Message must be 200 characters or less. Supports basic Slack markdown: *bold*, _italic_, ~strike~, `code`, ```code block```, \u003equote, and links \u003curl|text\u003e. Requires GH_AW_SLACK_CHANNEL_ID environment variable to be set.", + "description": "Add a comment to a Notion page", "inputSchema": { "additionalProperties": false, "properties": { - "message": { - "description": "The message to post (max 200 characters, supports Slack markdown)", + "comment": { + "description": "The comment text to add", "type": "string" } }, "required": [ - "message" + "comment" ], "type": "object" }, - "name": "post_to_slack_channel" + "name": "notion_add_comment" }, { - "description": "Add a comment to a Notion page", + "description": "Post a message to a Slack channel. Message must be 200 characters or less. Supports basic Slack markdown: *bold*, _italic_, ~strike~, `code`, ```code block```, \u003equote, and links \u003curl|text\u003e. Requires GH_AW_SLACK_CHANNEL_ID environment variable to be set.", "inputSchema": { "additionalProperties": false, "properties": { - "comment": { - "description": "The comment text to add", + "message": { + "description": "The message to post (max 200 characters, supports Slack markdown)", "type": "string" } }, "required": [ - "comment" + "message" ], "type": "object" }, - "name": "notion_add_comment" + "name": "post_to_slack_channel" } ] EOF diff --git a/pkg/campaign/orchestrator.go b/pkg/campaign/orchestrator.go index 2ccd2c2105..2aeefcc6b0 100644 --- a/pkg/campaign/orchestrator.go +++ b/pkg/campaign/orchestrator.go @@ -10,6 +10,64 @@ import ( var orchestratorLog = logger.New("campaign:orchestrator") +// extractFileGlobPattern extracts the file glob pattern from memory-paths or +// metrics-glob configuration. This pattern is used for the file-glob filter in +// repo-memory configuration to match files that the agent creates. +// +// For campaigns that use dated directory patterns (e.g., campaign-id-*/), this +// function preserves the wildcard pattern instead of using just the campaign ID. +// +// Examples: +// - memory-paths: ["memory/campaigns/project64-*/**"] -> "project64-*/**" +// - metrics-glob: "memory/campaigns/project64-*/metrics/*.json" -> "project64-*/**" +// - no patterns with wildcards -> "project64/**" (fallback to ID) +func extractFileGlobPattern(spec *CampaignSpec) string { + // Try to extract pattern from memory-paths first + // Prefer patterns with wildcards in the directory name (e.g., campaign-id-*) + var firstValidPattern string + for _, memPath := range spec.MemoryPaths { + // Remove "memory/campaigns/" prefix if present + pattern := strings.TrimPrefix(memPath, "memory/campaigns/") + // If pattern has both wildcards and slashes, it's a valid pattern + if strings.Contains(pattern, "*") && strings.Contains(pattern, "/") { + // Check if wildcard is in the directory name (not just in **) + if strings.Contains(strings.Split(pattern, "/")[0], "*") { + // This pattern has a wildcard in the directory name - prefer it + orchestratorLog.Printf("Extracted file-glob pattern from memory-paths (with wildcard): %s", pattern) + return pattern + } + // Save this as a fallback valid pattern + if firstValidPattern == "" { + firstValidPattern = pattern + } + } + } + + // If we found a valid pattern (even without directory wildcard), use it + if firstValidPattern != "" { + orchestratorLog.Printf("Extracted file-glob pattern from memory-paths: %s", firstValidPattern) + return firstValidPattern + } + + // Try to extract pattern from metrics-glob + if spec.MetricsGlob != "" { + pattern := strings.TrimPrefix(spec.MetricsGlob, "memory/campaigns/") + if strings.Contains(pattern, "*") { + // Extract the base directory pattern (everything before /metrics/ or first file-specific part) + if idx := strings.Index(pattern, "/metrics/"); idx > 0 { + basePattern := pattern[:idx] + "/**" + orchestratorLog.Printf("Extracted file-glob pattern from metrics-glob: %s", basePattern) + return basePattern + } + } + } + + // Fallback to simple ID-based pattern + fallbackPattern := fmt.Sprintf("%s/**", spec.ID) + orchestratorLog.Printf("Using fallback file-glob pattern: %s", fallbackPattern) + return fallbackPattern +} + // BuildOrchestrator constructs a minimal agentic workflow representation for a // given CampaignSpec. The resulting WorkflowData is compiled via the standard // CompileWorkflowDataWithValidation pipeline, and the orchestratorPath @@ -201,6 +259,10 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W orchestratorLog.Printf("Campaign orchestrator '%s' built successfully with safe outputs enabled", spec.ID) + // Extract file-glob pattern from memory-paths or metrics-glob to support + // dated campaign directory patterns like "campaign-id-*/**" + fileGlobPattern := extractFileGlobPattern(spec) + data := &workflow.WorkflowData{ Name: name, Description: description, @@ -221,7 +283,7 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W map[string]any{ "id": "campaigns", "branch-name": "memory/campaigns", - "file-glob": []any{fmt.Sprintf("%s/**", spec.ID)}, + "file-glob": []any{fileGlobPattern}, }, }, "bash": []any{"*"}, diff --git a/pkg/campaign/orchestrator_test.go b/pkg/campaign/orchestrator_test.go index 1511ad99af..021204538a 100644 --- a/pkg/campaign/orchestrator_test.go +++ b/pkg/campaign/orchestrator_test.go @@ -312,3 +312,134 @@ func TestBuildOrchestrator_GovernanceOverridesSafeOutputMaxima(t *testing.T) { t.Fatalf("unexpected update-project max: got %d, want %d", data.SafeOutputs.UpdateProjects.Max, 4) } } + +func TestExtractFileGlobPattern(t *testing.T) { + tests := []struct { + name string + spec *CampaignSpec + expectedGlob string + expectedLogMsg string + }{ + { + name: "dated pattern in memory-paths", + spec: &CampaignSpec{ + ID: "go-file-size-reduction-project64", + MemoryPaths: []string{"memory/campaigns/go-file-size-reduction-project64-*/**"}, + MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json", + }, + expectedGlob: "go-file-size-reduction-project64-*/**", + expectedLogMsg: "Extracted file-glob pattern from memory-paths", + }, + { + name: "dated pattern in metrics-glob only", + spec: &CampaignSpec{ + ID: "go-file-size-reduction-project64", + MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json", + }, + expectedGlob: "go-file-size-reduction-project64-*/**", + expectedLogMsg: "Extracted file-glob pattern from metrics-glob", + }, + { + name: "simple pattern without wildcards", + spec: &CampaignSpec{ + ID: "simple-campaign", + MemoryPaths: []string{"memory/campaigns/simple-campaign/**"}, + }, + expectedGlob: "simple-campaign/**", + expectedLogMsg: "Using fallback file-glob pattern", + }, + { + name: "no memory paths or metrics glob", + spec: &CampaignSpec{ + ID: "minimal-campaign", + }, + expectedGlob: "minimal-campaign/**", + expectedLogMsg: "Using fallback file-glob pattern", + }, + { + name: "multiple memory paths with wildcard", + spec: &CampaignSpec{ + ID: "multi-path", + MemoryPaths: []string{ + "memory/campaigns/multi-path-staging/**", + "memory/campaigns/multi-path-*/data/**", + }, + }, + expectedGlob: "multi-path-*/data/**", + expectedLogMsg: "Extracted file-glob pattern from memory-paths", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractFileGlobPattern(tt.spec) + if result != tt.expectedGlob { + t.Errorf("extractFileGlobPattern(%q) = %q, want %q", tt.spec.ID, result, tt.expectedGlob) + } + }) + } +} + +func TestBuildOrchestrator_FileGlobMatchesMemoryPaths(t *testing.T) { + // This test verifies that the file-glob pattern in repo-memory configuration + // matches the pattern defined in memory-paths, including wildcards + spec := &CampaignSpec{ + ID: "go-file-size-reduction-project64", + Name: "Go File Size Reduction Campaign", + Description: "Test campaign with dated memory paths", + ProjectURL: "https://github.com/orgs/githubnext/projects/64", + Workflows: []string{"daily-file-diet"}, + MemoryPaths: []string{"memory/campaigns/go-file-size-reduction-project64-*/**"}, + MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json", + TrackerLabel: "campaign:go-file-size-reduction-project64", + } + + mdPath := ".github/workflows/go-file-size-reduction-project64.campaign.md" + data, _ := BuildOrchestrator(spec, mdPath) + + if data == nil { + t.Fatalf("expected non-nil WorkflowData") + } + + // Extract repo-memory configuration from Tools + repoMemoryConfig, ok := data.Tools["repo-memory"] + if !ok { + t.Fatalf("expected repo-memory to be configured in Tools") + } + + repoMemoryArray, ok := repoMemoryConfig.([]any) + if !ok || len(repoMemoryArray) == 0 { + t.Fatalf("expected repo-memory to be an array with at least one entry") + } + + repoMemoryEntry, ok := repoMemoryArray[0].(map[string]any) + if !ok { + t.Fatalf("expected repo-memory entry to be a map") + } + + fileGlob, ok := repoMemoryEntry["file-glob"] + if !ok { + t.Fatalf("expected file-glob to be present in repo-memory entry") + } + + fileGlobArray, ok := fileGlob.([]any) + if !ok || len(fileGlobArray) == 0 { + t.Fatalf("expected file-glob to be an array with at least one entry") + } + + fileGlobPattern, ok := fileGlobArray[0].(string) + if !ok { + t.Fatalf("expected file-glob pattern to be a string") + } + + // Verify that the file-glob pattern includes the wildcard for dated directories + expectedPattern := "go-file-size-reduction-project64-*/**" + if fileGlobPattern != expectedPattern { + t.Errorf("file-glob pattern = %q, want %q", fileGlobPattern, expectedPattern) + } + + // Verify that the pattern would match dated directories + if !strings.Contains(fileGlobPattern, "*") { + t.Errorf("file-glob pattern should include wildcard for dated directories, got %q", fileGlobPattern) + } +}