From c4743ff38a59796c04a65d14898fb95c8a6a74f0 Mon Sep 17 00:00:00 2001 From: Michael Powers Date: Wed, 28 Jan 2026 17:22:10 -0500 Subject: [PATCH] refactor!(api,cli): split resume selector from bootstrap skipping BREAKING CHANGE: The -r flag now only sets the resume=true selector and no longer skips rule discovery and bootstrap scripts. To achieve the previous behavior of skipping bootstrap, users must now use both -r and --skip-bootstrap flags together. The change separates concerns: - -r: Sets resume selector for task filtering - --skip-bootstrap: Controls whether to discover rules, skills, and run bootstrap scripts This allows users to independently control task filtering and bootstrap behavior, providing more flexibility in workflow configurations. API changes: - Added WithBootstrap() option in options.go to control bootstrap behavior programmatically - WithResume() now only sets the resume selector and no longer affects bootstrap discovery - Context.doBootstrap field controls rule/skill discovery and bootstrap script execution independently from resume selector --- docs/how-to/use-selectors.md | 15 ++- docs/how-to/use-with-ai-agents.md | 6 +- docs/reference/cli.md | 44 ++++--- integration_test.go | 58 ++++----- main.go | 9 +- pkg/codingcontext/README.md | 4 +- pkg/codingcontext/context.go | 22 ++-- pkg/codingcontext/context_test.go | 115 +++++++++++++++--- pkg/codingcontext/options.go | 11 +- pkg/codingcontext/taskparser/expander_test.go | 2 +- 10 files changed, 203 insertions(+), 83 deletions(-) diff --git a/docs/how-to/use-selectors.md b/docs/how-to/use-selectors.md index c59def2..24d2e1d 100644 --- a/docs/how-to/use-selectors.md +++ b/docs/how-to/use-selectors.md @@ -113,12 +113,21 @@ coding-context -s source=github code-review ## Resume Mode -The `-r` flag is shorthand for `-s resume=true` plus skipping all rules: +The `-r` flag sets the resume selector to "true", which can be used to filter tasks by their frontmatter `resume` field: ```bash -# These are equivalent: +# Set resume selector coding-context -r fix-bug -coding-context -s resume=true fix-bug # but also skips rules + +# Equivalent to: +coding-context -s resume=true fix-bug +``` + +**Note:** The `-r` flag only sets the selector. To skip rule discovery and bootstrap scripts, use the `--skip-bootstrap` flag: + +```bash +# Skip rules and bootstrap (common in resume scenarios) +coding-context -r --skip-bootstrap fix-bug ``` Use resume mode when continuing work in a new session to save tokens. diff --git a/docs/how-to/use-with-ai-agents.md b/docs/how-to/use-with-ai-agents.md index fd643f0..a0da177 100644 --- a/docs/how-to/use-with-ai-agents.md +++ b/docs/how-to/use-with-ai-agents.md @@ -73,8 +73,8 @@ Use context in iterative workflows: coding-context -s resume=false fix-bug > context-initial.txt cat context-initial.txt | ai-agent > analysis.txt -# Step 2: Implementation (skip rules with -r) -coding-context -r fix-bug > context-resume.txt +# Step 2: Implementation (skip rules with --skip-bootstrap) +coding-context -r --skip-bootstrap fix-bug > context-resume.txt cat context-resume.txt analysis.txt | ai-agent > implementation.txt ``` @@ -239,7 +239,7 @@ If your context exceeds token limits: coding-context -s priority=high fix-bug ``` -2. **Use resume mode to skip rules:** +2. **Use bootstrap disabled to skip rules:** ```bash coding-context -r fix-bug ``` diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 852ae41..4c53b3a 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -221,21 +221,37 @@ coding-context -a copilot -w implement-feature **Type:** Boolean flag **Default:** False -Enable resume mode. This does two things: -1. Skips outputting all rule files (saves tokens) -2. Automatically adds `-s resume=true` selector +Set the resume selector to "true". This automatically adds `-s resume=true` selector, which can be used to filter tasks by their frontmatter `resume` field. -Use this when continuing work in a new session where context has already been established. +**Note:** This flag only sets the selector. To skip rule discovery and bootstrap scripts, use the `--skip-bootstrap` flag instead. **Example:** ```bash -# Initial session -coding-context -s resume=false fix-bug | ai-agent - -# Resume session +# Set resume selector to select resume-specific tasks coding-context -r fix-bug | ai-agent + +# Equivalent to: +coding-context -s resume=true fix-bug | ai-agent +``` + +### `--skip-bootstrap` + +**Type:** Boolean flag +**Default:** False (bootstrap enabled by default) + +Skip bootstrap: skip discovering rules, skills, and running bootstrap scripts. When present, rule discovery, skill discovery, and bootstrap script execution are skipped. + +**Example:** +```bash +# Skip rule discovery and bootstrap (saves tokens and time) +coding-context --skip-bootstrap fix-bug | ai-agent + +# Enable bootstrap (default behavior, omit --skip-bootstrap flag) +coding-context fix-bug | ai-agent ``` +**Note:** This flag is independent of the `-r` flag. Use `-r` to set the resume selector, and `--skip-bootstrap` to skip bootstrap operations. + ### `-s =` **Type:** Key-value pair @@ -296,12 +312,12 @@ coding-context -w fix-bug # Combine with other options coding-context -a copilot -w -s languages=go -p issue=123 fix-bug -# Resume mode with write rules: rules are skipped, only task output to stdout -coding-context -a copilot -w -r fix-bug +# Resume selector with bootstrap disabled: rules are skipped, only task output to stdout +coding-context -a copilot -w -r --skip-bootstrap fix-bug ``` -**Note on Resume Mode:** -When using `-w` with `-r` (resume mode), no rules file is written since rules are not collected in resume mode. Only the task prompt is output to stdout. +**Note on Bootstrap:** +When using `-w` with `--skip-bootstrap` (bootstrap disabled), no rules file is written since rules are not collected. Only the task prompt is output to stdout. **Use case:** This mode is particularly useful when working with AI coding agents that read rules from specific configuration files. Instead of including all rules in the prompt (consuming tokens), you can write them to the agent's config file once and only send the task prompt. @@ -435,8 +451,8 @@ coding-context -d https://cdn.company.com/rules.tar.gz code-review coding-context -s resume=false implement-feature > context.txt cat context.txt | ai-agent > plan.txt -# Continue work (skips rules) -coding-context -r implement-feature | ai-agent +# Continue work (skip rules and bootstrap) +coding-context -r --skip-bootstrap implement-feature | ai-agent ``` ### Piping to AI Agents diff --git a/integration_test.go b/integration_test.go index e4633fb..00d768a 100644 --- a/integration_test.go +++ b/integration_test.go @@ -830,65 +830,65 @@ This is the resume task prompt for continuing the bug fix. t.Errorf("normal mode: resume task content should not be in stdout") } - // Test 2: Run in resume mode (with -s resume=true selector) + // Test 2: Run with resume selector and bootstrap disabled (with -s resume=true and --skip-bootstrap) // Capture stdout and stderr separately to verify bootstrap scripts don't run - cmd = exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-s", "resume=true", "fix-bug-resume") + cmd = exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-s", "resume=true", "--skip-bootstrap", "fix-bug-resume") stdout.Reset() stderr.Reset() cmd.Stdout = &stdout cmd.Stderr = &stderr if err = cmd.Run(); err != nil { - t.Fatalf("failed to run binary in resume mode: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String()) + t.Fatalf("failed to run binary with resume selector and bootstrap disabled: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String()) } output = stdout.String() stderrOutput = stderr.String() - // In resume mode, rules should NOT be included + // With bootstrap disabled, rules should NOT be included if strings.Contains(output, "# Coding Standards") { - t.Errorf("resume mode: rule content should not be in stdout") + t.Errorf("bootstrap disabled: rule content should not be in stdout") } - // In resume mode, bootstrap scripts should NOT run + // With bootstrap disabled, bootstrap scripts should NOT run if strings.Contains(stderrOutput, "RULE_BOOTSTRAP_RAN") { - t.Errorf("resume mode: rule bootstrap script should not run (found in stderr: %s)", stderrOutput) + t.Errorf("bootstrap disabled: rule bootstrap script should not run (found in stderr: %s)", stderrOutput) } - // In resume mode, should use the resume task + // With resume selector, should use the resume task if !strings.Contains(output, "# Fix Bug (Resume)") { - t.Errorf("resume mode: resume task content not found in stdout") + t.Errorf("resume selector: resume task content not found in stdout") } if strings.Contains(output, "# Fix Bug (Initial)") { - t.Errorf("resume mode: normal task content should not be in stdout") + t.Errorf("resume selector: normal task content should not be in stdout") } - // Test 3: Run in resume mode (with -r flag) - cmd = exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-r", "fix-bug-resume") + // Test 3: Run with -r flag (sets resume selector) and --skip-bootstrap (disables bootstrap) + cmd = exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-r", "--skip-bootstrap", "fix-bug-resume") stdout.Reset() stderr.Reset() cmd.Stdout = &stdout cmd.Stderr = &stderr if err = cmd.Run(); err != nil { - t.Fatalf("failed to run binary in resume mode with -r flag: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String()) + t.Fatalf("failed to run binary with -r flag and --skip-bootstrap: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String()) } output = stdout.String() stderrOutput = stderr.String() - // In resume mode with -r flag, rules should NOT be included + // With bootstrap disabled, rules should NOT be included if strings.Contains(output, "# Coding Standards") { - t.Errorf("resume mode (-r flag): rule content should not be in stdout") + t.Errorf("bootstrap disabled (--skip-bootstrap): rule content should not be in stdout") } - // In resume mode with -r flag, bootstrap scripts should NOT run + // With bootstrap disabled, bootstrap scripts should NOT run if strings.Contains(stderrOutput, "RULE_BOOTSTRAP_RAN") { - t.Errorf("resume mode (-r flag): rule bootstrap script should not run (found in stderr: %s)", stderrOutput) + t.Errorf("bootstrap disabled (--skip-bootstrap): rule bootstrap script should not run (found in stderr: %s)", stderrOutput) } - // In resume mode with -r flag, should use the resume task + // With -r flag, should use the resume task if !strings.Contains(output, "# Fix Bug (Resume)") { - t.Errorf("resume mode (-r flag): resume task content not found in stdout") + t.Errorf("resume selector (-r flag): resume task content not found in stdout") } if strings.Contains(output, "# Fix Bug (Initial)") { - t.Errorf("resume mode (-r flag): normal task content should not be in stdout") + t.Errorf("resume selector (-r flag): normal task content should not be in stdout") } } @@ -1221,7 +1221,7 @@ func TestSingleExpansion(t *testing.T) { taskContent := `Task with parameter: ${param1} And a value that looks like expansion syntax but should not be expanded: ${"nested"}` - if err := os.WriteFile(taskFile, []byte(taskContent), 0644); err != nil { + if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil { t.Fatalf("failed to create task file: %v", err) } @@ -1252,14 +1252,14 @@ func TestCommandExpansionOnce(t *testing.T) { // Create a command file with a parameter commandFile := filepath.Join(commandsDir, "test-cmd.md") commandContent := `Command param: ${cmd_param}` - if err := os.WriteFile(commandFile, []byte(commandContent), 0644); err != nil { + if err := os.WriteFile(commandFile, []byte(commandContent), 0o644); err != nil { t.Fatalf("failed to create command file: %v", err) } // Create a task that calls the command with a param containing expansion syntax taskFile := filepath.Join(dirs.tasksDir, "test-cmd-task.md") taskContent := `/test-cmd cmd_param="!` + "`echo injected`" + `"` - if err := os.WriteFile(taskFile, []byte(taskContent), 0644); err != nil { + if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil { t.Fatalf("failed to create task file: %v", err) } @@ -1420,12 +1420,12 @@ This is the task prompt for resume mode. // Create a temporary home directory for this test tmpHome := t.TempDir() - // Run with -w flag, -r flag (resume mode), and -a copilot + // Run with -w flag, -r flag (sets resume selector), --skip-bootstrap (disables bootstrap), and -a copilot wd, err := os.Getwd() if err != nil { t.Fatalf("failed to get working directory: %v", err) } - cmd := exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-a", "copilot", "-w", "-r", "test-task-resume") + cmd := exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-a", "copilot", "-w", "-r", "--skip-bootstrap", "test-task-resume") var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr @@ -1448,7 +1448,7 @@ This is the task prompt for resume mode. // Verify that the rules were NOT printed to stdout if strings.Contains(output, "# Test Rule") { - t.Errorf("rules should not be in stdout when using -w flag with resume mode") + t.Errorf("rules should not be in stdout when using -w flag with bootstrap disabled") } // Verify that the task IS printed to stdout @@ -1459,17 +1459,17 @@ This is the task prompt for resume mode. t.Errorf("task description not found in stdout") } - // Verify that NO rules file was created in resume mode + // Verify that NO rules file was created when bootstrap is disabled expectedRulesPath := filepath.Join(tmpHome, ".github", "agents", "AGENTS.md") if _, err := os.Stat(expectedRulesPath); err == nil { - t.Errorf("rules file should NOT be created in resume mode with -w flag, but found at %s", expectedRulesPath) + t.Errorf("rules file should NOT be created when bootstrap is disabled with -w flag, but found at %s", expectedRulesPath) } else if !os.IsNotExist(err) { t.Fatalf("unexpected error checking for rules file: %v", err) } // Verify that the logger did NOT report writing rules if strings.Contains(stderrOutput, "Rules written") { - t.Errorf("stderr should NOT contain 'Rules written' message in resume mode") + t.Errorf("stderr should NOT contain 'Rules written' message when bootstrap is disabled") } } diff --git a/main.go b/main.go index af9cde4..b2edc8d 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ func main() { var workDir string var resume bool + var skipBootstrap bool // When true, skips bootstrap (default false means bootstrap enabled) var writeRules bool var agent codingcontext.Agent params := make(taskparser.Params) @@ -32,7 +33,8 @@ func main() { var manifestURL string flag.StringVar(&workDir, "C", ".", "Change to directory before doing anything.") - flag.BoolVar(&resume, "r", false, "Resume mode: skip outputting rules and select task with 'resume: true' in frontmatter.") + flag.BoolVar(&resume, "r", false, "Resume mode: set 'resume=true' selector to filter tasks by their frontmatter resume field.") + flag.BoolVar(&skipBootstrap, "skip-bootstrap", false, "Skip bootstrap: skip discovering rules, skills, and running bootstrap scripts.") flag.BoolVar(&writeRules, "w", false, "Write rules to the agent's user rules path and only print the prompt to stdout. Requires agent (via task 'agent' field or -a flag).") flag.Var(&agent, "a", "Target agent to use. Required when using -w to write rules to the agent's user rules path. Supported agents: cursor, opencode, copilot, claude, gemini, augment, windsurf, codex.") flag.Var(¶ms, "p", "Parameter to substitute in the prompt. Can be specified multiple times as key=value.") @@ -87,6 +89,7 @@ func main() { codingcontext.WithSearchPaths(searchPaths...), codingcontext.WithLogger(logger), codingcontext.WithResume(resume), + codingcontext.WithBootstrap(!skipBootstrap), // Invert: skipBootstrap=false means bootstrap enabled codingcontext.WithAgent(agent), codingcontext.WithManifestURL(manifestURL), codingcontext.WithUserPrompt(userPrompt), @@ -107,8 +110,8 @@ func main() { os.Exit(1) } - // Skip writing rules file in resume mode since no rules are collected - if !resume { + // Skip writing rules file if bootstrap is disabled since no rules are collected + if !skipBootstrap { relativePath := result.Agent.UserRulePath() if relativePath == "" { logger.Error("Error", "error", fmt.Errorf("no user rule path available for agent")) diff --git a/pkg/codingcontext/README.md b/pkg/codingcontext/README.md index de359d4..3b01f6b 100644 --- a/pkg/codingcontext/README.md +++ b/pkg/codingcontext/README.md @@ -86,6 +86,7 @@ func main() { codingcontext.WithSelectors(sel), codingcontext.WithAgent(codingcontext.AgentCursor), codingcontext.WithResume(false), + codingcontext.WithBootstrap(true), codingcontext.WithUserPrompt("Additional context or instructions"), codingcontext.WithManifestURL("https://example.com/manifest.txt"), codingcontext.WithLogger(slog.New(slog.NewTextHandler(os.Stderr, nil))), @@ -293,7 +294,8 @@ Creates a new Context with the given options. - `WithParams(params taskparams.Params)` - Set parameters for substitution (import `taskparams` package) - `WithSelectors(selectors selectors.Selectors)` - Set selectors for filtering rules (import `selectors` package) - `WithAgent(agent Agent)` - Set target agent (excludes that agent's own rules) -- `WithResume(resume bool)` - Enable resume mode (skips rules) +- `WithResume(resume bool)` - Set resume selector to "true" (for filtering tasks by frontmatter resume field) +- `WithBootstrap(doBootstrap bool)` - Control whether to discover rules, skills, and run bootstrap scripts (default: true) - `WithUserPrompt(userPrompt string)` - Set user prompt to append to task - `WithManifestURL(manifestURL string)` - Set manifest URL for additional search paths - `WithLogger(logger *slog.Logger)` - Set logger diff --git a/pkg/codingcontext/context.go b/pkg/codingcontext/context.go index f82d6d9..9b81b28 100644 --- a/pkg/codingcontext/context.go +++ b/pkg/codingcontext/context.go @@ -34,6 +34,7 @@ type Context struct { logger *slog.Logger cmdRunner func(cmd *exec.Cmd) error resume bool + doBootstrap bool // Controls whether to discover rules, skills, and run bootstrap scripts agent Agent userPrompt string // User-provided prompt to append to task } @@ -41,11 +42,12 @@ type Context struct { // New creates a new Context with the given options func New(opts ...Option) *Context { c := &Context{ - params: make(taskparser.Params), - includes: make(selectors.Selectors), - rules: make([]markdown.Markdown[markdown.RuleFrontMatter], 0), - skills: skills.AvailableSkills{Skills: make([]skills.Skill, 0)}, - logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), + params: make(taskparser.Params), + includes: make(selectors.Selectors), + rules: make([]markdown.Markdown[markdown.RuleFrontMatter], 0), + skills: skills.AvailableSkills{Skills: make([]skills.Skill, 0)}, + logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), + doBootstrap: true, // Default to true for backward compatibility cmdRunner: func(cmd *exec.Cmd) error { return cmd.Run() }, @@ -522,9 +524,8 @@ func (cc *Context) cleanupDownloadedDirectories() { } func (cc *Context) findExecuteRuleFiles(ctx context.Context, homeDir string) error { - // Skip rule file discovery if resume mode is enabled - // Check cc.resume directly first, then fall back to selector check for backward compatibility - if cc.resume || (cc.includes != nil && cc.includes.GetValue("resume", "true")) { + // Skip rule file discovery if bootstrap is disabled + if !cc.doBootstrap { return nil } @@ -607,9 +608,8 @@ func (cc *Context) runBootstrapScript(ctx context.Context, path string) error { // discoverSkills searches for skill directories and loads only their metadata (name and description) // for progressive disclosure. Skills are folders containing a SKILL.md file. func (cc *Context) discoverSkills() error { - // Skip skill discovery if resume mode is enabled - // Check cc.resume directly first, then fall back to selector check for backward compatibility - if cc.resume || (cc.includes != nil && cc.includes.GetValue("resume", "true")) { + // Skip skill discovery if bootstrap is disabled + if !cc.doBootstrap { return nil } diff --git a/pkg/codingcontext/context_test.go b/pkg/codingcontext/context_test.go index f33ed7d..56eb765 100644 --- a/pkg/codingcontext/context_test.go +++ b/pkg/codingcontext/context_test.go @@ -191,6 +191,35 @@ func TestNew(t *testing.T) { if !c.resume { t.Error("expected resume to be true") } + if !c.doBootstrap { + t.Error("expected doBootstrap to be true by default") + } + }, + }, + { + name: "with bootstrap disabled", + opts: []Option{ + WithBootstrap(false), + }, + check: func(t *testing.T, c *Context) { + if c.doBootstrap { + t.Error("expected doBootstrap to be false") + } + }, + }, + { + name: "resume and bootstrap are independent", + opts: []Option{ + WithResume(true), + WithBootstrap(false), + }, + check: func(t *testing.T, c *Context) { + if !c.resume { + t.Error("expected resume to be true") + } + if c.doBootstrap { + t.Error("expected doBootstrap to be false") + } }, }, { @@ -492,7 +521,24 @@ func TestContext_Run_Rules(t *testing.T) { }, }, { - name: "resume mode skips rule discovery", + name: "bootstrap disabled skips rule discovery", + setup: func(t *testing.T, dir string) { + createTask(t, dir, "bootstrap-task", "", "Task content") + createRule(t, dir, ".agents/rules/rule1.md", "", "Rule content") + }, + opts: []Option{ + WithBootstrap(false), + }, + taskName: "bootstrap-task", + wantErr: false, + check: func(t *testing.T, result *Result) { + if len(result.Rules) != 0 { + t.Errorf("expected 0 rules when bootstrap is disabled, got %d", len(result.Rules)) + } + }, + }, + { + name: "resume mode does not skip rule discovery", setup: func(t *testing.T, dir string) { createTask(t, dir, "resume-task", "", "Task content") createRule(t, dir, ".agents/rules/rule1.md", "", "Rule content") @@ -503,8 +549,8 @@ func TestContext_Run_Rules(t *testing.T) { taskName: "resume-task", wantErr: false, check: func(t *testing.T, result *Result) { - if len(result.Rules) != 0 { - t.Errorf("expected 0 rules in resume mode, got %d", len(result.Rules)) + if len(result.Rules) != 1 { + t.Errorf("expected 1 rule when resume is true but bootstrap is enabled, got %d", len(result.Rules)) } }, }, @@ -524,21 +570,21 @@ func TestContext_Run_Rules(t *testing.T) { }, }, { - name: "resume mode skips bootstrap scripts", + name: "bootstrap disabled skips bootstrap scripts", setup: func(t *testing.T, dir string) { createTask(t, dir, "no-bootstrap", "", "Task") createRule(t, dir, ".agents/rules/rule1.md", "", "Rule") createBootstrapScript(t, dir, ".agents/rules/rule1.md", "#!/bin/sh\nexit 1") }, opts: []Option{ - WithResume(true), + WithBootstrap(false), }, taskName: "no-bootstrap", wantErr: false, check: func(t *testing.T, result *Result) { - // In resume mode, rules aren't discovered, so bootstrap won't run + // When bootstrap is disabled, rules aren't discovered, so bootstrap scripts won't run if len(result.Rules) != 0 { - t.Error("expected no rules in resume mode") + t.Error("expected no rules when bootstrap is disabled") } }, }, @@ -1093,23 +1139,23 @@ func TestContext_Run_Integration(t *testing.T) { }, }, { - name: "resume mode workflow skips rules but includes task", + name: "bootstrap disabled workflow skips rules but includes task", setup: func(t *testing.T, dir string) { - createTask(t, dir, "resume", "", "Resume this task") + createTask(t, dir, "bootstrap", "", "Continue this task") createRule(t, dir, ".agents/rules/rule1.md", "", "Should be skipped") createBootstrapScript(t, dir, ".agents/rules/rule1.md", "#!/bin/sh\necho 'should not run'") }, opts: []Option{ - WithResume(true), + WithBootstrap(false), }, - taskName: "resume", + taskName: "bootstrap", wantErr: false, check: func(t *testing.T, result *Result) { - if !strings.Contains(result.Task.Content, "Resume this task") { + if !strings.Contains(result.Task.Content, "Continue this task") { t.Errorf("unexpected task content: %q", result.Task.Content) } if len(result.Rules) != 0 { - t.Errorf("expected 0 rules in resume mode, got %d", len(result.Rules)) + t.Errorf("expected 0 rules when bootstrap is disabled, got %d", len(result.Rules)) } }, }, @@ -2311,7 +2357,7 @@ description: %s wantErr: true, }, { - name: "resume mode skips skill discovery", + name: "bootstrap disabled skips skill discovery", setup: func(t *testing.T, dir string) { // Create task createTask(t, dir, "test-task", "", "Task content") @@ -2324,7 +2370,7 @@ description: %s skillContent := `--- name: test-skill -description: A test skill that should not be discovered in resume mode +description: A test skill that should not be discovered when bootstrap is disabled --- # Test Skill @@ -2336,12 +2382,47 @@ This is a test skill. t.Fatalf("failed to create skill file: %v", err) } }, - opts: []Option{WithResume(true)}, + opts: []Option{WithBootstrap(false)}, taskName: "test-task", wantErr: false, checkFunc: func(t *testing.T, result *Result) { if len(result.Skills.Skills) != 0 { - t.Errorf("expected 0 skills in resume mode, got %d", len(result.Skills.Skills)) + t.Errorf("expected 0 skills when bootstrap is disabled, got %d", len(result.Skills.Skills)) + } + }, + }, + { + name: "resume mode does not skip skill discovery", + setup: func(t *testing.T, dir string) { + // Create task + createTask(t, dir, "test-task", "", "Task content") + + // Create skill directory with SKILL.md + skillDir := filepath.Join(dir, ".agents", "skills", "test-skill") + if err := os.MkdirAll(skillDir, 0o755); err != nil { + t.Fatalf("failed to create skill directory: %v", err) + } + + skillContent := `--- +name: test-skill +description: A test skill that should be discovered even in resume mode +--- + +# Test Skill + +This is a test skill. +` + skillPath := filepath.Join(skillDir, "SKILL.md") + if err := os.WriteFile(skillPath, []byte(skillContent), 0o644); err != nil { + t.Fatalf("failed to create skill file: %v", err) + } + }, + opts: []Option{WithResume(true)}, + taskName: "test-task", + wantErr: false, + checkFunc: func(t *testing.T, result *Result) { + if len(result.Skills.Skills) != 1 { + t.Errorf("expected 1 skill when resume is true but bootstrap is enabled, got %d", len(result.Skills.Skills)) } }, }, diff --git a/pkg/codingcontext/options.go b/pkg/codingcontext/options.go index d7f2bf8..c075127 100644 --- a/pkg/codingcontext/options.go +++ b/pkg/codingcontext/options.go @@ -45,13 +45,22 @@ func WithLogger(logger *slog.Logger) Option { } } -// WithResume enables resume mode, which skips rule discovery and bootstrap scripts +// WithResume sets the resume selector to "true", which can be used to filter tasks +// by their frontmatter resume field. This does not affect rule/skill discovery or bootstrap scripts. func WithResume(resume bool) Option { return func(c *Context) { c.resume = resume } } +// WithBootstrap controls whether to discover rules, skills, and run bootstrap scripts. +// When set to false, rule discovery, skill discovery, and bootstrap script execution are skipped. +func WithBootstrap(doBootstrap bool) Option { + return func(c *Context) { + c.doBootstrap = doBootstrap + } +} + // WithAgent sets the target agent, which excludes that agent's own rules func WithAgent(agent Agent) Option { return func(c *Context) { diff --git a/pkg/codingcontext/taskparser/expander_test.go b/pkg/codingcontext/taskparser/expander_test.go index ad5d325..e4dd993 100644 --- a/pkg/codingcontext/taskparser/expander_test.go +++ b/pkg/codingcontext/taskparser/expander_test.go @@ -136,7 +136,7 @@ func TestExpandCommands(t *testing.T) { }, { name: "command output not trimmed", - content: "!`echo -n hello` world", + content: "!`printf 'hello'` world", expected: "hello world", }, {