diff --git a/docs/reference/search-paths.md b/docs/reference/search-paths.md index 2c6d529..3fd4128 100644 --- a/docs/reference/search-paths.md +++ b/docs/reference/search-paths.md @@ -308,13 +308,9 @@ moves to the next rule directory, preserving search-path order. error. Rules after the failing bootstrap are not published. A rule parse or expansion error is also fatal and `Run` returns an error before any bootstrap scripts from that directory run. -- **Lenient search paths:** A rule bootstrap failure logs a warning, excludes only - the failing rule, and continues with later rules in the same directory. - -A rule parse or expansion error still stops discovery of the remainder of that -directory. On a lenient path, rules discovered before that error remain eligible -for bootstrap and publication, and processing continues with the next rule -directory or search path. +- **Lenient search paths:** A rule bootstrap, parse, or expansion failure logs a + warning, excludes only the failing rule, and continues with later rules in the + same directory. ## Working Directory diff --git a/pkg/codingcontext/context.go b/pkg/codingcontext/context.go index 3e24192..3d3e438 100644 --- a/pkg/codingcontext/context.go +++ b/pkg/codingcontext/context.go @@ -827,9 +827,8 @@ func (cc *Context) cleanupDownloadedDirectories() { // findExecuteRuleFiles discovers rule files in each rule directory, then // bootstraps and publishes them. Discovery of one directory finishes before -// any of that directory's bootstrap scripts run. A lenient bootstrap failure -// skips only that rule; a parse or expand error still stops discovery of the -// rest of that directory. +// any of that directory's bootstrap scripts run. On a lenient path, a parse, +// expand, or bootstrap failure skips only that rule. func (cc *Context) findExecuteRuleFiles(ctx context.Context) error { if !cc.doBootstrap { return nil @@ -843,22 +842,27 @@ func (cc *Context) findExecuteRuleFiles(ctx context.Context) error { for _, dir := range namespacedRulePaths(sp.Path) { var pending []pendingRule - discoveryErr := cc.visitMarkdownInDir(dir, func(path string, baseFm *markdown.BaseFrontMatter) error { + if err := cc.visitMarkdownInDir(dir, func(path string, baseFm *markdown.BaseFrontMatter) error { rule, err := cc.discoverPendingRule(path, baseFm) if err != nil { - return err + if !sp.Lenient { + return err + } + + cc.logger.Warn( + "skipping rule file after discovery failure", + "path", path, + "error", err, + ) + + return nil } pending = append(pending, rule) return nil - }) - if discoveryErr != nil { - if !sp.Lenient { - return discoveryErr - } - - cc.logger.Warn("stopping rule discovery after error", "path", dir, "error", discoveryErr) + }); err != nil { + return err } if err := cc.bootstrapAndPublishRules(ctx, pending, sp.Lenient); err != nil { diff --git a/pkg/codingcontext/context_bootstrap_isolation_test.go b/pkg/codingcontext/context_bootstrap_isolation_test.go index 184f257..119bd54 100644 --- a/pkg/codingcontext/context_bootstrap_isolation_test.go +++ b/pkg/codingcontext/context_bootstrap_isolation_test.go @@ -222,11 +222,13 @@ func TestRun_LenientRuleParseFailurePreservesEarlierRules(t *testing.T) { result, err := cc.Run(context.Background(), "test-task") require.NoError(t, err) - require.Equal(t, []string{"before"}, bootstrapIsolationRuleNames(result.Rules)) + require.Equal(t, []string{"before", "after"}, bootstrapIsolationRuleNames(result.Rules)) require.Equal(t, []string{"01-before-bootstrap"}, calls) - require.Contains(t, logs.String(), "stopping rule discovery after error") + require.Contains(t, logs.String(), "skipping rule file after discovery failure") require.Contains(t, logs.String(), "02-failing.md") - require.NotContains(t, result.Prompt, "After content") + require.Contains(t, result.Prompt, "Before content") + require.NotContains(t, result.Prompt, "Failing content") + require.Contains(t, result.Prompt, "After content") } func TestRun_LenientRuleParseFailureDoesNotBlockLaterLenientSearchPath(t *testing.T) { @@ -259,13 +261,13 @@ func TestRun_LenientRuleParseFailureDoesNotBlockLaterLenientSearchPath(t *testin result, err := cc.Run(context.Background(), "test-task") require.NoError(t, err) - require.Equal(t, []string{"before", "target"}, bootstrapIsolationRuleNames(result.Rules)) + require.Equal(t, []string{"before", "after", "target"}, bootstrapIsolationRuleNames(result.Rules)) require.Equal(t, []string{"01-before-bootstrap", "01-target-bootstrap"}, calls) require.Contains(t, result.Prompt, "Before content") require.NotContains(t, result.Prompt, "Failing content") - require.NotContains(t, result.Prompt, "After content") + require.Contains(t, result.Prompt, "After content") require.Contains(t, result.Prompt, "Target content") - require.Contains(t, logs.String(), "stopping rule discovery after error") + require.Contains(t, logs.String(), "skipping rule file after discovery failure") require.Contains(t, logs.String(), "02-failing.md") }