Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ workflows:
- daily-file-diet
tracker-label: "campaign:go-file-size-reduction-project64"
memory-paths:
- "memory/campaigns/go-file-size-reduction-project64-*/**"
- "memory/campaigns/go-file-size-reduction-project64/**"
metrics-glob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json"
state: active
tags:
Expand Down
2 changes: 2 additions & 0 deletions actions/setup/js/push_repo_memory.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ async function main() {
// If no explicit campaign ID, try to extract from patterns when memoryId is "campaigns"
if (!campaignId && memoryId === "campaigns" && patterns.length > 0) {
// Try to extract campaign ID from first pattern matching "<campaign-id>/**"
// This only works for simple patterns without wildcards in the campaign ID portion
// For patterns like "campaign-id-*/**", use GH_AW_CAMPAIGN_ID environment variable
const campaignMatch = /^([^*?/]+)\/\*\*/.exec(patterns[0]);
if (campaignMatch) {
campaignId = campaignMatch[1];
Expand Down
20 changes: 20 additions & 0 deletions actions/setup/js/push_repo_memory.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,26 @@ describe("push_repo_memory.cjs - globPatternToRegex helper", () => {
expect(metricsRegex.test("security-q1/cursor.json")).toBe(false);
});

it("should match flexible campaign pattern for both dated and non-dated structures", () => {
// Pattern: go-file-size-reduction-project64*/**
// This should match BOTH:
// - go-file-size-reduction-project64-2025-12-31/ (with date suffix)
// - go-file-size-reduction-project64/ (without suffix)
const flexibleRegex = globPatternToRegex("go-file-size-reduction-project64*/**");

// Test dated structure (with suffix)
expect(flexibleRegex.test("go-file-size-reduction-project64-2025-12-31/cursor.json")).toBe(true);
expect(flexibleRegex.test("go-file-size-reduction-project64-2025-12-31/metrics/2025-12-31.json")).toBe(true);

// Test non-dated structure (without suffix)
expect(flexibleRegex.test("go-file-size-reduction-project64/cursor.json")).toBe(true);
expect(flexibleRegex.test("go-file-size-reduction-project64/metrics/2025-12-31.json")).toBe(true);

// Should not match other campaigns
expect(flexibleRegex.test("other-campaign/file.json")).toBe(false);
expect(flexibleRegex.test("security-q1/cursor.json")).toBe(false);
});

it("should match multiple file extensions", () => {
const patterns = ["*.json", "*.jsonl", "*.csv", "*.md"].map(globPatternToRegex);

Expand Down
65 changes: 34 additions & 31 deletions pkg/campaign/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,62 +10,65 @@ 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
// convertStringsToAny converts a slice of strings to a slice of any
func convertStringsToAny(strings []string) []any {
result := make([]any, len(strings))
for i, s := range strings {
result[i] = s
}
return result
}

// extractFileGlobPatterns extracts all file glob patterns from memory-paths or
// metrics-glob configuration. These patterns are 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.
// function preserves all wildcard patterns from memory-paths to support multiple
// directory structures (both dated and non-dated).
//
// 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
// - memory-paths: ["memory/campaigns/project64-*/**", "memory/campaigns/project64/**"]
// -> ["project64-*/**", "project64/**"]
// - memory-paths: ["memory/campaigns/project64-*/**"] -> ["project64-*/**"]
// - metrics-glob: "memory/campaigns/project64-*/metrics/*.json" -> ["project64-*/**"]
// - no patterns with wildcards -> ["project64/**"] (fallback to ID)
func extractFileGlobPatterns(spec *CampaignSpec) []string {
var patterns []string

// Extract all patterns from memory-paths
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
}
patterns = append(patterns, pattern)
orchestratorLog.Printf("Extracted file-glob pattern from memory-paths: %s", 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
// If we found patterns from memory-paths, return them
if len(patterns) > 0 {
return patterns
}

// Try to extract pattern from metrics-glob
// Try to extract pattern from metrics-glob as fallback
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
return []string{basePattern}
}
}
}

// Fallback to simple ID-based pattern
fallbackPattern := fmt.Sprintf("%s/**", spec.ID)
orchestratorLog.Printf("Using fallback file-glob pattern: %s", fallbackPattern)
return fallbackPattern
return []string{fallbackPattern}
}

// BuildOrchestrator constructs a minimal agentic workflow representation for a
Expand Down Expand Up @@ -259,9 +262,9 @@ 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)
// Extract file-glob patterns from memory-paths or metrics-glob to support
// multiple directory structures (e.g., both dated "campaign-id-*/**" and non-dated "campaign-id/**")
fileGlobPatterns := extractFileGlobPatterns(spec)

data := &workflow.WorkflowData{
Name: name,
Expand All @@ -283,7 +286,7 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W
map[string]any{
"id": "campaigns",
"branch-name": "memory/campaigns",
"file-glob": []any{fileGlobPattern},
"file-glob": convertStringsToAny(fileGlobPatterns),
},
},
"bash": []any{"*"},
Expand Down
51 changes: 40 additions & 11 deletions pkg/campaign/orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,21 +313,44 @@ func TestBuildOrchestrator_GovernanceOverridesSafeOutputMaxima(t *testing.T) {
}
}

func TestExtractFileGlobPattern(t *testing.T) {
func TestExtractFileGlobPatterns(t *testing.T) {
tests := []struct {
name string
spec *CampaignSpec
expectedGlob string
expectedGlobs []string
expectedLogMsg string
}{
{
name: "flexible pattern matching both dated and non-dated",
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",
},
expectedGlobs: []string{"go-file-size-reduction-project64*/**"},
expectedLogMsg: "Extracted file-glob pattern from memory-paths",
},
{
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-*/**",
expectedGlobs: []string{"go-file-size-reduction-project64-*/**"},
expectedLogMsg: "Extracted file-glob pattern from memory-paths",
},
{
name: "multiple patterns in memory-paths",
spec: &CampaignSpec{
ID: "go-file-size-reduction-project64",
MemoryPaths: []string{
"memory/campaigns/go-file-size-reduction-project64-*/**",
"memory/campaigns/go-file-size-reduction-project64/**",
},
MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json",
},
expectedGlobs: []string{"go-file-size-reduction-project64-*/**", "go-file-size-reduction-project64/**"},
expectedLogMsg: "Extracted file-glob pattern from memory-paths",
},
{
Expand All @@ -336,7 +359,7 @@ func TestExtractFileGlobPattern(t *testing.T) {
ID: "go-file-size-reduction-project64",
MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json",
},
expectedGlob: "go-file-size-reduction-project64-*/**",
expectedGlobs: []string{"go-file-size-reduction-project64-*/**"},
expectedLogMsg: "Extracted file-glob pattern from metrics-glob",
},
{
Expand All @@ -345,15 +368,15 @@ func TestExtractFileGlobPattern(t *testing.T) {
ID: "simple-campaign",
MemoryPaths: []string{"memory/campaigns/simple-campaign/**"},
},
expectedGlob: "simple-campaign/**",
expectedLogMsg: "Using fallback file-glob pattern",
expectedGlobs: []string{"simple-campaign/**"},
expectedLogMsg: "Extracted file-glob pattern from memory-paths",
},
{
name: "no memory paths or metrics glob",
spec: &CampaignSpec{
ID: "minimal-campaign",
},
expectedGlob: "minimal-campaign/**",
expectedGlobs: []string{"minimal-campaign/**"},
expectedLogMsg: "Using fallback file-glob pattern",
},
{
Expand All @@ -365,16 +388,22 @@ func TestExtractFileGlobPattern(t *testing.T) {
"memory/campaigns/multi-path-*/data/**",
},
},
expectedGlob: "multi-path-*/data/**",
expectedGlobs: []string{"multi-path-staging/**", "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)
result := extractFileGlobPatterns(tt.spec)
if len(result) != len(tt.expectedGlobs) {
t.Errorf("extractFileGlobPatterns(%q) returned %d patterns, want %d", tt.spec.ID, len(result), len(tt.expectedGlobs))
return
}
for i, expected := range tt.expectedGlobs {
if result[i] != expected {
t.Errorf("extractFileGlobPatterns(%q)[%d] = %q, want %q", tt.spec.ID, i, result[i], expected)
}
}
})
}
Expand Down