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
22 changes: 11 additions & 11 deletions demo/substack-spec-v01.dot

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion internal/attractor/engine/cli_only_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import "strings"
// cliOnlyModelIDs lists models that MUST route through CLI backend regardless
// of provider backend configuration. These models have no API endpoint.
var cliOnlyModelIDs = map[string]bool{
"gpt-5.4-spark": true,
"gpt-5.3-codex-spark": true,
"gpt-5.4-spark": true,
}

// isCLIOnlyModel returns true if the given model ID (with or without provider
Expand Down
2 changes: 1 addition & 1 deletion internal/attractor/engine/cli_only_models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ func TestIsCLIOnlyModel(t *testing.T) {
want bool
}{
{"gpt-5.4-spark", true},
{"GPT-5.3-CODEX-SPARK", true}, // case-insensitive
{"GPT-5.3-CODEX-SPARK", true}, // case-insensitive
{"openai/gpt-5.4-spark", true}, // with provider prefix
{"gpt-5.4", false}, // regular codex
{"gpt-5.4", false},
Expand Down
2 changes: 1 addition & 1 deletion internal/attractor/engine/input_materialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ func isLikelyArtifactInputPath(path string) bool {
segments := strings.Split(normalized, "/")
for _, seg := range segments {
switch seg {
case ".git", ".jj", "logs", "benchmarks", "worktree", "node_modules", ".pnpm-store", ".venv", "venv", "__pycache__", ".pytest_cache", "dist-info", "managed":
case ".git", ".jj", "logs", "benchmarks", "worktree", ".worktrees", "node_modules", ".pnpm-store", ".venv", "venv", "__pycache__", ".pytest_cache", "dist-info", "managed", ".cargo-target":
return true
}
}
Expand Down
46 changes: 43 additions & 3 deletions internal/attractor/engine/input_reference_scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,17 @@ func looksLikeReferenceToken(token string) bool {
if strings.ContainsAny(token, "<>|") {
return false
}
// Reject tokens with spaces - they are natural language, not paths/globs.
if strings.ContainsAny(token, " \t") {
return false
}
if strings.Contains(token, "/") || strings.Contains(token, "\\") {
return true
}
if windowsAbsPathRE.MatchString(token) {
return true
}
if strings.ContainsAny(token, "*?[") {
if looksLikeGlobPattern(token) {
return true
}
return false
Expand All @@ -182,7 +186,7 @@ func looksLikeStructuredReferenceToken(token string) bool {
if strings.Contains(token, "](") || strings.ContainsAny(token, "<>|") {
return false
}
if strings.ContainsAny(token, "*?[") {
if looksLikeGlobPattern(token) {
return true
}
// Structured captures (markdown links/quoted tokens) may contain local file
Expand All @@ -194,8 +198,44 @@ func looksLikeStructuredReferenceToken(token string) bool {
}

func classifyReferenceKind(pattern string) InputReferenceKind {
if strings.ContainsAny(pattern, "*?[") {
if looksLikeGlobPattern(pattern) {
return InputReferenceKindGlob
}
return InputReferenceKindPath
}

// looksLikeGlobPattern returns true if token contains valid glob metacharacters.
// Tokens containing * or ? are always treated as globs. Tokens containing [
// are only treated as globs if the bracket forms a valid character class (i.e.
// has a matching ]) AND the token also has path-like context (a path separator,
// * or ?) - this prevents natural language fragments like
// "DEFAULT_TOOL_LIMITS[tool_name" and programming constructs like
// "map[string]any" from being classified as globs.
func looksLikeGlobPattern(token string) bool {
if strings.ContainsAny(token, "*?") {
return true
}
if !strings.Contains(token, "[") {
return false
}
// Validate that every [ has a matching ] (valid character class).
hasValidBracket := false
for i := 0; i < len(token); i++ {
if token[i] == '[' {
j := strings.IndexByte(token[i+1:], ']')
if j < 0 {
// Unmatched [ - not a valid glob.
return false
}
hasValidBracket = true
// Skip past the matched ]
i += j + 1
}
}
if !hasValidBracket {
return false
}
// Brackets alone (e.g. "map[string]any") are not enough to be a glob.
// Require a path separator to disambiguate from programming constructs.
return strings.ContainsAny(token, "/\\")
}
53 changes: 53 additions & 0 deletions internal/attractor/engine/input_reference_scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,59 @@ func TestInputReferenceScan_IgnoresURLsAndParsesWindowsGlobToken(t *testing.T) {
requireRefKind(t, got, "docs/spec.md", InputReferenceKindPath)
}

func TestInputReferenceScan_RejectsNaturalLanguageWithBrackets(t *testing.T) {
scanner := deterministicInputReferenceScanner{}
// These tokens from issue #48 should NOT be classified as globs.
badTokens := []string{
`you are wielding ([ch]) [weapon`,
`DEFAULT_TOOL_LIMITS[tool_name`,
`array[index`,
`map[string]any`,
}
for _, token := range badTokens {
refs := scanner.Scan("test.md", []byte(token))
for _, ref := range refs {
if ref.Kind == InputReferenceKindGlob {
t.Errorf("token %q should not be classified as glob, but got pattern=%q kind=%q", token, ref.Pattern, ref.Kind)
}
}
}
}

func TestInputReferenceScan_AcceptsValidGlobBrackets(t *testing.T) {
scanner := deterministicInputReferenceScanner{}
// These ARE valid globs with matched brackets.
content := strings.Join([]string{
`"src/[abc]/*.go"`,
`"docs/[a-z]*.md"`,
}, "\n")
refs := scanner.Scan("test.md", []byte(content))
got := map[string]InputReferenceKind{}
for _, ref := range refs {
got[ref.Pattern] = ref.Kind
}
requireRefKind(t, got, "src/[abc]/*.go", InputReferenceKindGlob)
requireRefKind(t, got, "docs/[a-z]*.md", InputReferenceKindGlob)
}

func TestIsLikelyArtifactInputPath_ExcludesWorktrees(t *testing.T) {
cases := []struct {
path string
want bool
}{
{".worktrees/rogue-logs/worktree/.cargo-target/foo.rs", true},
{".worktrees/some-branch/src/main.go", true},
{"src/.cargo-target/debug/build/foo.o", true},
{"src/main.go", false},
{"docs/spec.md", false},
}
for _, tc := range cases {
if got := isLikelyArtifactInputPath(tc.path); got != tc.want {
t.Errorf("isLikelyArtifactInputPath(%q) = %v, want %v", tc.path, got, tc.want)
}
}
}

func requireRefKind(t *testing.T, got map[string]InputReferenceKind, pattern string, want InputReferenceKind) {
t.Helper()
kind, ok := got[pattern]
Expand Down