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
34 changes: 29 additions & 5 deletions actions/setup/js/push_repo_memory.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ async function main() {
}
}

// ============================================================================
// CAMPAIGN-SPECIFIC VALIDATION FUNCTIONS
// ============================================================================
// The following functions implement validation for the campaign convention:
// When memoryId is "campaigns" and file-glob matches "<campaign-id>/**",
// enforce specific JSON schemas for cursor.json and metrics/*.json files.
//
// This is a domain-specific convention used by Campaign Workflows to maintain
// durable state in repo-memory. See docs/guides/campaigns/ for details.
// ============================================================================

/** @param {any} obj @param {string} campaignId @param {string} relPath */
function validateCampaignCursor(obj, campaignId, relPath) {
if (!isPlainObject(obj)) {
Expand Down Expand Up @@ -128,9 +139,20 @@ async function main() {
// The artifactDir IS the memory directory (no nested structure needed)
const sourceMemoryPath = artifactDir;

// Campaign mode enforcement (agentic campaigns):
// We treat repo-memory ID "campaigns" with a single file-glob like "<campaign-id>/**" as a strong contract.
// In this mode, cursor.json and at least one metrics snapshot are required.
// ============================================================================
// CAMPAIGN MODE DETECTION
// ============================================================================
// Campaign Workflows use a convention-based pattern in repo-memory:
// - memoryId: "campaigns"
// - file-glob: "<campaign-id>/**"
//
// When this pattern is detected, we enforce campaign-specific validation:
// 1. cursor.json must exist and follow the cursor schema
// 2. At least one metrics/*.json file must exist and follow the metrics schema
//
// This ensures campaigns maintain durable state consistency across workflow runs.
// Non-campaign repo-memory configurations bypass this validation entirely.
// ============================================================================
const singlePattern = fileGlobFilter.trim().split(/\s+/).filter(Boolean);
const campaignPattern = singlePattern.length === 1 ? singlePattern[0] : "";
const campaignMatch = memoryId === "campaigns" ? /^([^*?]+)\/\*\*$/.exec(campaignPattern) : null;
Expand Down Expand Up @@ -242,7 +264,8 @@ async function main() {
throw new Error("File size validation failed");
}

// Campaign JSON contract checks (only for the campaign subtree).
// Campaign-specific JSON validation (only when campaign mode is active)
// This enforces the campaign state file schemas for cursor and metrics
if (isCampaignMode && relativeFilePath.startsWith(`${campaignId}/`)) {
if (relativeFilePath === `${campaignId}/cursor.json`) {
const obj = tryParseJSONFile(fullPath);
Expand Down Expand Up @@ -271,7 +294,8 @@ async function main() {
return;
}

// Campaign mode validation: ensure required files were found
// Campaign mode validation: ensure required state files were found
// This enforcement is only active when campaign mode is detected
if (isCampaignMode) {
if (!campaignCursorFound) {
core.error(`Missing required campaign cursor file: ${campaignId}/cursor.json`);
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath
}

// Emit experimental warning for campaigns feature
// Campaign workflows (.campaign.md) are compiled by the campaign system in pkg/campaign/
// This warning is part of the general workflow compilation pipeline and simply
// detects campaign files to inform users about the experimental status.
if strings.HasSuffix(markdownPath, ".campaign.md") {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Using experimental feature: campaigns - This is a preview feature for multi-workflow orchestration. The campaign spec format, CLI commands, and repo-memory conventions may change in future releases. Workflows may break or require migration when the feature stabilizes."))
c.IncrementWarningCount()
Expand Down
5 changes: 4 additions & 1 deletion pkg/workflow/safe_output_validation_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,10 @@ var ValidationConfig = map[string]TypeValidationConfig{
"update_project": {
DefaultMax: 10,
Fields: map[string]FieldValidation{
"project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"},
"project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"},
// campaign_id is an optional field used by Campaign Workflows to tag project items.
// When provided, the update-project safe output applies a "campaign:<id>" label.
// This is part of the campaign tracking convention but not required for general use.
"campaign_id": {Type: "string", Sanitize: true, MaxLength: 128},
"content_type": {Type: "string", Enum: []string{"issue", "pull_request"}},
"content_number": {OptionalPositiveInteger: true},
Expand Down
23 changes: 19 additions & 4 deletions pkg/workflow/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,25 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) {
data.ParsedTools = NewTools(data.Tools)

if data.Permissions == "" {
// Default behavior: keep existing workflows stable with read-all.
// Campaign orchestrators intentionally omit permissions from the generated
// .campaign.g.md frontmatter, so we compute explicit, minimal read permissions
// for the agent job at compile time.
// ============================================================================
// PERMISSIONS DEFAULTS
// ============================================================================
// Default behavior: keep existing workflows stable with read-all permissions.
//
// CAMPAIGN-SPECIFIC HANDLING:
// Campaign orchestrator workflows (.campaign.g.md files) are auto-generated
// by the BuildOrchestrator function in pkg/campaign/orchestrator.go.
// These generated workflows intentionally omit explicit permissions in their
// frontmatter, so we compute minimal read permissions here at compile time.
//
// This is part of the campaign orchestrator generation pattern where:
// 1. Campaign specs (.campaign.md) define high-level campaign configuration
// 2. BuildOrchestrator generates orchestrator workflows (.campaign.g.md)
// 3. The compiler applies default permissions to the generated workflow
//
// This separation allows campaign configuration to remain declarative while
// ensuring generated orchestrators have appropriate permissions.
// ============================================================================
if strings.HasSuffix(markdownPath, ".campaign.g.md") {
perms := NewPermissions()
// Campaign orchestrators always need to read repository contents and tracker issues.
Expand Down