SaylorTwift HF Staff commited on
Commit
1244914
·
verified ·
1 Parent(s): eb51191

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/ISSUE_TEMPLATE/bug_report.yml +117 -0
  2. .github/ISSUE_TEMPLATE/config.yml +11 -0
  3. .github/ISSUE_TEMPLATE/documentation.yml +100 -0
  4. .github/ISSUE_TEMPLATE/feature_request.yml +155 -0
  5. .github/ISSUE_TEMPLATE/performance_issue.yml +150 -0
  6. .github/ISSUE_TEMPLATE/provider_integration.yml +129 -0
  7. .github/contribution.md +69 -0
  8. .github/dependabot.yml +26 -0
  9. .github/labels.json +349 -0
  10. .github/release-drafter.yml +132 -0
  11. .github/scripts/bounty/README.md +155 -0
  12. .github/scripts/bounty/src/api.ts +110 -0
  13. .github/scripts/bounty/src/rules.ts +187 -0
  14. .github/scripts/bounty/src/sync-all-issues.ts +77 -0
  15. .github/scripts/bounty/src/sync-issue.ts +133 -0
  16. .github/scripts/bounty/src/sync-pr.ts +82 -0
  17. .github/scripts/bounty/src/types.ts +82 -0
  18. .github/scripts/bounty/tests/rules.test.ts +369 -0
  19. .github/scripts/bounty/tests/sync-all-issues.test.ts +138 -0
  20. .github/scripts/bounty/tests/sync-issue.test.ts +144 -0
  21. .github/scripts/bounty/tests/sync-pr.test.ts +241 -0
  22. .github/workflows/autofix.yml +62 -0
  23. .github/workflows/bounty.yml +64 -0
  24. .github/workflows/ci.yml +296 -0
  25. .github/workflows/labels.yml +39 -0
  26. .github/workflows/release-drafter.yml +53 -0
  27. .github/workflows/release.yml +155 -0
  28. .github/workflows/stale.yml +51 -0
  29. benchmarks/README.md +320 -0
  30. benchmarks/cli.ts +394 -0
  31. benchmarks/command-generator.ts +98 -0
  32. benchmarks/evals/echo/task.yml +26 -0
  33. benchmarks/evals/multi_file_patch/multi_file_patch_tasks.csv +5 -0
  34. benchmarks/evals/multi_file_patch/task.yml +21 -0
  35. benchmarks/evals/patch_exact_match/patch_exact_match_tasks.csv +7 -0
  36. benchmarks/evals/patch_exact_match/task.yml +217 -0
  37. benchmarks/evals/redundant_cd_with_cwd/task.yml +45 -0
  38. benchmarks/evals/search_over_find/task.yml +58 -0
  39. benchmarks/evals/sem_search/README.md +33 -0
  40. benchmarks/evals/sem_search/task.yml +138 -0
  41. benchmarks/evals/semantic_search_quality/README.md +197 -0
  42. benchmarks/evals/semantic_search_quality/llm_judge.ts +400 -0
  43. benchmarks/evals/semantic_search_quality/run_eval.sh +95 -0
  44. benchmarks/evals/semantic_search_quality/test_context.json +22 -0
  45. benchmarks/evals/semantic_search_quality/test_queries.ts +232 -0
  46. benchmarks/evals/suggest/task.yml +47 -0
  47. benchmarks/model.ts +38 -0
  48. benchmarks/parse.ts +52 -0
  49. benchmarks/task-executor.ts +184 -0
  50. benchmarks/tsconfig.json +47 -0
.github/ISSUE_TEMPLATE/bug_report.yml ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 🐛 Bug Report
2
+ description: Report a bug or unexpected behavior in Forge
3
+ title: "[Bug]: "
4
+ labels: ["type: bug"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for taking the time to report a bug! Please fill out the sections below to help us reproduce and fix the issue.
10
+
11
+ - type: textarea
12
+ id: description
13
+ attributes:
14
+ label: Bug Description
15
+ description: A clear and concise description of what the bug is.
16
+ placeholder: Describe the bug...
17
+ validations:
18
+ required: true
19
+
20
+ - type: textarea
21
+ id: reproduction
22
+ attributes:
23
+ label: Steps to Reproduce
24
+ description: Steps to reproduce the behavior
25
+ placeholder: |
26
+ 1. Run command '...'
27
+ 2. Enter prompt '...'
28
+ 3. See error
29
+ validations:
30
+ required: true
31
+
32
+ - type: textarea
33
+ id: expected
34
+ attributes:
35
+ label: Expected Behavior
36
+ description: What did you expect to happen?
37
+ placeholder: Describe what should happen...
38
+ validations:
39
+ required: true
40
+
41
+ - type: textarea
42
+ id: actual
43
+ attributes:
44
+ label: Actual Behavior
45
+ description: What actually happened?
46
+ placeholder: Describe what actually happened...
47
+ validations:
48
+ required: true
49
+
50
+ - type: input
51
+ id: version
52
+ attributes:
53
+ label: Forge Version
54
+ description: What version of Forge are you running? (`forge --version`)
55
+ placeholder: e.g., 0.1.0
56
+ validations:
57
+ required: true
58
+
59
+ - type: input
60
+ id: os
61
+ attributes:
62
+ label: Operating System & Version
63
+ description: What operating system and version are you using?
64
+ placeholder: e.g., macOS 14.1, Ubuntu 22.04, Windows 11
65
+ validations:
66
+ required: false
67
+
68
+ - type: dropdown
69
+ id: provider
70
+ attributes:
71
+ label: AI Provider
72
+ description: Which AI provider are you using?
73
+ options:
74
+ - OpenAI
75
+ - Anthropic
76
+ - OpenRouter
77
+ - Google Vertex AI
78
+ - Cerebras
79
+ - x-ai
80
+ - z.ai
81
+ - Requesty
82
+ - IO Intelligence
83
+ - Groq
84
+ - Amazon Bedrock
85
+ - OpenAI-compatible
86
+ - Other
87
+ validations:
88
+ required: false
89
+
90
+ - type: input
91
+ id: model
92
+ attributes:
93
+ label: Model
94
+ description: Which model are you using?
95
+ placeholder: e.g., claude-3.7-sonnet, gpt-4, o3-mini-high
96
+
97
+ - type: dropdown
98
+ id: installation
99
+ attributes:
100
+ label: Installation Method
101
+ description: How did you install Forge?
102
+ options:
103
+ - npx forgecode@latest
104
+ - npm install -g forgecode
105
+ - curl -fsSL https://forgecode.dev/cli | sh
106
+ - Built from source
107
+ - Other
108
+ validations:
109
+ required: true
110
+
111
+ - type: textarea
112
+ id: config
113
+ attributes:
114
+ label: Configuration
115
+ description: |
116
+ Please share relevant parts of your `forge.yaml` configuration (remove sensitive data like API keys).
117
+ render: yaml
.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: 💬 Discord Community
4
+ url: https://discord.gg/kRZBPpkgwq
5
+ about: Join our Discord server for questions, discussions, and community support
6
+ - name: 📖 Documentation
7
+ url: https://forgecode.dev/docs/
8
+ about: Check out our comprehensive documentation
9
+ - name: ❓ Discussions
10
+ url: https://github.com/antinomyhq/forge/discussions
11
+ about: Ask questions and discuss ideas with the community
.github/ISSUE_TEMPLATE/documentation.yml ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 📚 Documentation Issue
2
+ description: Report problems with documentation or suggest improvements
3
+ title: "[Docs]: "
4
+ labels: ["type: docs"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for helping improve our documentation! Clear docs help everyone.
10
+
11
+ - type: dropdown
12
+ id: issue-type
13
+ attributes:
14
+ label: Documentation Issue Type
15
+ description: What type of documentation issue is this?
16
+ options:
17
+ - Missing documentation
18
+ - Incorrect/outdated information
19
+ - Unclear explanation
20
+ - Broken link
21
+ - Typo/grammar
22
+ - Code example doesn't work
23
+ - Need more examples
24
+ - Need better organization
25
+ - Other
26
+ validations:
27
+ required: true
28
+
29
+ - type: input
30
+ id: location
31
+ attributes:
32
+ label: Documentation Location
33
+ description: Where is the documentation issue? (URL, file path, or section name)
34
+ placeholder: https://github.com/antinomyhq/forge/blob/main/docs/...
35
+ validations:
36
+ required: true
37
+
38
+ - type: textarea
39
+ id: current-content
40
+ attributes:
41
+ label: Current Content
42
+ description: What does the current documentation say? (Quote or describe)
43
+ placeholder: Currently, the documentation says...
44
+
45
+ - type: textarea
46
+ id: issue-description
47
+ attributes:
48
+ label: Issue Description
49
+ description: What is wrong or confusing about the current documentation?
50
+ placeholder: The issue is that...
51
+ validations:
52
+ required: true
53
+
54
+ - type: textarea
55
+ id: suggested-improvement
56
+ attributes:
57
+ label: Suggested Improvement
58
+ description: How would you improve this documentation?
59
+ placeholder: It would be better if...
60
+ validations:
61
+ required: true
62
+
63
+ - type: dropdown
64
+ id: audience
65
+ attributes:
66
+ label: Target Audience
67
+ description: Who would benefit from this documentation improvement?
68
+ options:
69
+ - New users/beginners
70
+ - Experienced users
71
+ - Contributors/developers
72
+ - All users
73
+ validations:
74
+ required: true
75
+
76
+ - type: textarea
77
+ id: examples
78
+ attributes:
79
+ label: Example or Reference
80
+ description: Provide examples or references that could help improve the documentation
81
+ placeholder: |
82
+ For example:
83
+ ```yaml
84
+ # forge.yaml example
85
+ model: claude-3.7-sonnet
86
+ ```
87
+
88
+ - type: checkboxes
89
+ id: contribution
90
+ attributes:
91
+ label: Contribution
92
+ description: Can you help improve this documentation?
93
+ options:
94
+ - label: I'm willing to submit a PR with the documentation improvements
95
+
96
+ - type: textarea
97
+ id: additional
98
+ attributes:
99
+ label: Additional Context
100
+ description: Any other information that would help improve the documentation
.github/ISSUE_TEMPLATE/feature_request.yml ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: ✨ Feature Request
2
+ description: Suggest a new feature or enhancement for Forge
3
+ title: "[Feature]: "
4
+ labels: ["type: feature"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for suggesting a new feature!
10
+
11
+ **Before you continue, please ensure you can clearly answer:**
12
+ 1. **Why do you NEED this feature?** (Not just "nice to have")
13
+ 2. **What is NOT possible right now?** (Current limitations)
14
+ 3. **What WILL be possible with this feature?** (Concrete outcomes)
15
+
16
+ Vague or incomplete requests will be closed. Take your time to write a compelling case.
17
+
18
+ - type: textarea
19
+ id: why-needed
20
+ attributes:
21
+ label: "1. Why Do You NEED This Feature?"
22
+ description: |
23
+ **Be specific.** Explain the real-world problem or blocker you're facing.
24
+ Not "it would be cool if..." but "I cannot accomplish X because..."
25
+ Why is this important to your workflow? What is the impact of NOT having this?
26
+ placeholder: |
27
+ I NEED this feature because:
28
+ - Current workflow requires me to...
29
+ - This costs me X hours per week...
30
+ - Without this, I cannot accomplish...
31
+ - The business impact is...
32
+ validations:
33
+ required: true
34
+
35
+ - type: textarea
36
+ id: current-limitation
37
+ attributes:
38
+ label: "2. What Is NOT Possible Right Now?"
39
+ description: |
40
+ **Describe the current limitations in detail.**
41
+ What have you tried? What doesn't work? Why can't you work around it?
42
+ Include specific examples, commands, or workflows that fail.
43
+ placeholder: |
44
+ Currently, I cannot:
45
+ - [Specific action/workflow that fails]
46
+ - [What happens when you try]
47
+ - [Why existing features don't solve this]
48
+
49
+ I've tried:
50
+ - [Workaround 1 and why it doesn't work]
51
+ - [Workaround 2 and why it doesn't work]
52
+ validations:
53
+ required: true
54
+
55
+ - type: textarea
56
+ id: future-possibilities
57
+ attributes:
58
+ label: "3. What WILL Be Possible With This Feature?"
59
+ description: |
60
+ **Describe concrete outcomes and capabilities.**
61
+ What specific tasks/workflows will this enable?
62
+ How will your work change? What new things can you build/do?
63
+ placeholder: |
64
+ With this feature, I will be able to:
65
+ - [Specific new capability 1]
66
+ - [Specific new capability 2]
67
+ - [Measurable improvement: time saved, errors reduced, etc.]
68
+
69
+ Example workflow:
70
+ ```
71
+ # Show the exact commands or steps you'll use
72
+ forge new-feature --example
73
+ ```
74
+ validations:
75
+ required: true
76
+
77
+ - type: textarea
78
+ id: solution
79
+ attributes:
80
+ label: Proposed Solution (User Experience)
81
+ description: |
82
+ **Focus on the user experience, not implementation details.**
83
+ How would a user interact with this feature? What would they see/do?
84
+ Focus on what the user experiences, not how it would be built.
85
+ placeholder: |
86
+ From a user's perspective, the feature should work like this:
87
+ - When I [action], I would see/get [result]
88
+ - The interaction would be: [step-by-step user flow]
89
+ - Example: "When I run `forge search docs`, I'd see matching results from Google Workspace"
90
+ validations:
91
+ required: true
92
+
93
+ - type: textarea
94
+ id: alternatives
95
+ attributes:
96
+ label: Alternatives Considered
97
+ description: Have you considered any alternative solutions or workarounds?
98
+ placeholder: I've also thought about...
99
+
100
+ - type: dropdown
101
+ id: category
102
+ attributes:
103
+ label: Feature Category
104
+ description: Which area does this feature relate to?
105
+ options:
106
+ - AI/LLM Integration
107
+ - Code Generation/Editing
108
+ - File Operations
109
+ - Configuration
110
+ - CLI/UX
111
+ - Documentation
112
+ - Performance
113
+ - Security
114
+ - MCP Integration
115
+ - Multi-Agent Workflows
116
+ - Shell Plugin
117
+ - Testing
118
+ - Other
119
+ validations:
120
+ required: true
121
+
122
+ - type: dropdown
123
+ id: priority
124
+ attributes:
125
+ label: Priority/Impact
126
+ description: How important is this feature to your workflow?
127
+ options:
128
+ - Critical - Blocking my work
129
+ - High - Would significantly improve my workflow
130
+ - Medium - Would be nice to have
131
+ - Low - Minor improvement
132
+ validations:
133
+ required: true
134
+
135
+ - type: textarea
136
+ id: examples
137
+ attributes:
138
+ label: Examples from Other Tools
139
+ description: Are there similar features in other tools that could serve as inspiration?
140
+ placeholder: Tool X does this by...
141
+
142
+ - type: textarea
143
+ id: additional
144
+ attributes:
145
+ label: Additional Context
146
+ description: Add any other context, screenshots, or examples about the feature request.
147
+
148
+ - type: checkboxes
149
+ id: requirements-check
150
+ attributes:
151
+ label: "Pre-submission Checklist"
152
+ options:
153
+ - label: "I have searched existing issues and confirmed this is not a duplicate"
154
+ required: true
155
+ - label: "I am willing to submit a PR to implement this feature"
.github/ISSUE_TEMPLATE/performance_issue.yml ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: ⚡ Performance Issue
2
+ description: Report performance problems, slowness, or resource usage issues
3
+ title: "[Performance]: "
4
+ labels: ["type: performance"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for reporting a performance issue! Please provide detailed information to help us investigate.
10
+
11
+ - type: textarea
12
+ id: description
13
+ attributes:
14
+ label: Performance Issue Description
15
+ description: Describe the performance problem you're experiencing
16
+ placeholder: Forge becomes slow when...
17
+ validations:
18
+ required: true
19
+
20
+ - type: dropdown
21
+ id: issue-type
22
+ attributes:
23
+ label: Issue Type
24
+ description: What type of performance issue are you experiencing?
25
+ options:
26
+ - Slow response time
27
+ - High CPU usage
28
+ - High memory usage
29
+ - Network latency
30
+ - File I/O slowness
31
+ - Startup time
32
+ - Tool execution time
33
+ - Other
34
+ validations:
35
+ required: true
36
+
37
+ - type: textarea
38
+ id: reproduction
39
+ attributes:
40
+ label: Steps to Reproduce
41
+ description: What actions lead to the performance issue?
42
+ placeholder: |
43
+ 1. Run command '...'
44
+ 2. Process large file/directory
45
+ 3. Notice slowdown after...
46
+ validations:
47
+ required: true
48
+
49
+ - type: textarea
50
+ id: expected-performance
51
+ attributes:
52
+ label: Expected Performance
53
+ description: What performance did you expect?
54
+ placeholder: I expected the operation to complete in...
55
+ validations:
56
+ required: true
57
+
58
+ - type: textarea
59
+ id: actual-performance
60
+ attributes:
61
+ label: Actual Performance
62
+ description: What performance are you experiencing?
63
+ placeholder: The operation takes... and uses...
64
+ validations:
65
+ required: true
66
+
67
+ - type: textarea
68
+ id: measurements
69
+ attributes:
70
+ label: Performance Measurements
71
+ description: |
72
+ If possible, provide specific measurements (time, memory, CPU usage, etc.)
73
+ You can use tools like `time`, `htop`, Activity Monitor, or Task Manager
74
+ placeholder: |
75
+ - Execution time: 45 seconds
76
+ - Memory usage: 2.5 GB
77
+ - CPU usage: 180%
78
+ render: markdown
79
+
80
+ - type: input
81
+ id: project-size
82
+ attributes:
83
+ label: Project Size
84
+ description: Approximate size of your project (number of files, lines of code, etc.)
85
+ placeholder: e.g., 500 files, ~50k lines of code
86
+
87
+ - type: textarea
88
+ id: environment
89
+ attributes:
90
+ label: Environment Details
91
+ description: |
92
+ Provide details about your environment:
93
+ - Forge version (`forge --version`)
94
+ - Operating system and version
95
+ - CPU, RAM
96
+ - AI provider and model
97
+ - Shell
98
+ placeholder: |
99
+ - Forge: 0.1.0
100
+ - OS: macOS 14.1
101
+ - CPU: M2 Pro
102
+ - RAM: 16GB
103
+ - Provider: Anthropic
104
+ - Model: claude-3.7-sonnet
105
+ - Shell: zsh
106
+ validations:
107
+ required: true
108
+
109
+ - type: textarea
110
+ id: config
111
+ attributes:
112
+ label: Configuration
113
+ description: Share relevant parts of your `forge.yaml` (remove sensitive data)
114
+ render: yaml
115
+
116
+ - type: textarea
117
+ id: profiling
118
+ attributes:
119
+ label: Profiling Data
120
+ description: |
121
+ If you have any profiling data or verbose logs, please share them here.
122
+ Run with `--verbose` flag for detailed output.
123
+ render: shell
124
+
125
+ - type: dropdown
126
+ id: frequency
127
+ attributes:
128
+ label: Frequency
129
+ description: How often does this performance issue occur?
130
+ options:
131
+ - Always
132
+ - Often (>50% of the time)
133
+ - Sometimes (25-50% of the time)
134
+ - Rarely (<25% of the time)
135
+ - Only in specific scenarios
136
+ validations:
137
+ required: true
138
+
139
+ - type: textarea
140
+ id: workarounds
141
+ attributes:
142
+ label: Workarounds
143
+ description: Have you found any workarounds for this issue?
144
+ placeholder: I can work around this by...
145
+
146
+ - type: textarea
147
+ id: additional
148
+ attributes:
149
+ label: Additional Context
150
+ description: Any other relevant information about the performance issue
.github/ISSUE_TEMPLATE/provider_integration.yml ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 🔌 Provider Integration Request
2
+ description: Request support for a new AI provider
3
+ title: "[Provider]: "
4
+ labels: ["type: feature"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for suggesting a new AI provider! Please provide detailed information about the provider.
10
+
11
+ - type: input
12
+ id: provider-name
13
+ attributes:
14
+ label: Provider Name
15
+ description: What is the name of the AI provider?
16
+ placeholder: e.g., Cohere, Mistral AI, Together AI
17
+ validations:
18
+ required: true
19
+
20
+ - type: input
21
+ id: website
22
+ attributes:
23
+ label: Provider Website
24
+ description: Link to the provider's website
25
+ placeholder: https://...
26
+ validations:
27
+ required: true
28
+
29
+ - type: input
30
+ id: api-docs
31
+ attributes:
32
+ label: API Documentation
33
+ description: Link to the provider's API documentation
34
+ placeholder: https://docs...
35
+
36
+ - type: dropdown
37
+ id: api-type
38
+ attributes:
39
+ label: API Compatibility
40
+ description: What type of API does this provider offer?
41
+ options:
42
+ - OpenAI-compatible
43
+ - Anthropic-compatible
44
+ - Custom API
45
+ - Other
46
+ validations:
47
+ required: true
48
+
49
+ - type: textarea
50
+ id: models
51
+ attributes:
52
+ label: Available Models
53
+ description: List the models this provider offers that would be useful in Forge
54
+ placeholder: |
55
+ - model-1 (description)
56
+ - model-2 (description)
57
+ validations:
58
+ required: true
59
+
60
+ - type: textarea
61
+ id: features
62
+ attributes:
63
+ label: Key Features
64
+ description: What are the key features or advantages of this provider?
65
+ placeholder: |
66
+ - Fast inference
67
+ - Specialized models
68
+ - Cost-effective
69
+ - Open source models
70
+ validations:
71
+ required: true
72
+
73
+ - type: dropdown
74
+ id: authentication
75
+ attributes:
76
+ label: Authentication Method
77
+ description: How does authentication work for this provider?
78
+ options:
79
+ - API Key
80
+ - OAuth
81
+ - Bearer Token
82
+ - Basic Auth
83
+ - Other
84
+ validations:
85
+ required: true
86
+
87
+ - type: textarea
88
+ id: pricing
89
+ attributes:
90
+ label: Pricing Information
91
+ description: Link to pricing information or describe the pricing model
92
+ placeholder: https://provider.com/pricing or "Free tier available, paid plans start at..."
93
+
94
+ - type: dropdown
95
+ id: priority
96
+ attributes:
97
+ label: Community Interest
98
+ description: How much demand do you think there is for this provider?
99
+ options:
100
+ - High - Many users would benefit
101
+ - Medium - Some users would benefit
102
+ - Low - Niche use case
103
+ validations:
104
+ required: true
105
+
106
+ - type: textarea
107
+ id: use-case
108
+ attributes:
109
+ label: Your Use Case
110
+ description: Why do you want to use this provider with Forge?
111
+ placeholder: I would use this provider because...
112
+ validations:
113
+ required: true
114
+
115
+ - type: checkboxes
116
+ id: contribution
117
+ attributes:
118
+ label: Contribution
119
+ description: Are you willing to help with this integration?
120
+ options:
121
+ - label: I'm willing to submit a PR to implement this integration
122
+ - label: I can help with testing
123
+ - label: I have access to this provider's API for testing
124
+
125
+ - type: textarea
126
+ id: additional
127
+ attributes:
128
+ label: Additional Context
129
+ description: Any other information about this provider that would be helpful
.github/contribution.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code-Forge 🛠️
2
+
3
+ A sophisticated AI-powered coding assistant platform built in Rust, designed to provide intelligent code generation, manipulation, and analysis capabilities through a modular and extensible architecture.
4
+
5
+ ## ✨ Features
6
+
7
+ - 🤖 **AI-Powered Code Generation** - Advanced code generation and manipulation using modern AI models
8
+ - 🔍 **Smart Code Analysis** - Language-aware parsing and analysis for multiple programming languages
9
+ - 🛠️ **Extensive Tool System** - Rich set of development tools including file operations, shell commands, and code outline generation
10
+ - 💾 **Persistent Conversations** - Maintain context and history across coding sessions
11
+ - 🔒 **Secure Operations** - Built-in security measures for file system and shell operations
12
+ - 🔌 **Extensible Architecture** - Modular design supporting easy addition of new features and languages
13
+
14
+ ## 🚀 Setup
15
+
16
+ ### Prerequisites
17
+
18
+ - Rust toolchain (1.75+)
19
+ - SQLite
20
+ - Tree-sitter (for code analysis)
21
+
22
+ ### Installation
23
+
24
+ ```bash
25
+ # Build the project
26
+ cargo build --release
27
+
28
+ # Run the server
29
+ cargo run --release
30
+ ```
31
+
32
+ ## 🏗️ Project Structure
33
+
34
+ ```
35
+ code-forge/
36
+ ├── crates/
37
+ │ ├── forge_main/ # CLI and main application logic
38
+ │ ├── forge_domain/ # Core domain models and interfaces
39
+ │ ├── forge_services/ # HTTP API and database management
40
+ │ ├── forge_tool/ # Tool implementations
41
+ │ └── forge_walker/ # File system operations
42
+ ```
43
+
44
+ ## 🛠️ Core Components
45
+
46
+ - **Domain Layer** (`forge_domain`) - Core business logic and interfaces
47
+ - **Tool Layer** (`forge_tool`) - Development tools implementation
48
+ - **Server Layer** (`forge_services`) - API endpoints and persistence
49
+ - **Main Application** (`forge_main`) - CLI and application coordination
50
+
51
+ ## 🔧 Configuration
52
+
53
+ The application requires several environment variables for proper operation:
54
+
55
+ ```bash
56
+ # Required environment variables
57
+ DATABASE_URL="sqlite:path/to/database.db"
58
+ OPENROUTER_API_KEY="your-api-key"
59
+ ```
60
+
61
+ ## 📚 Documentation
62
+
63
+ Internal documentation:
64
+ - [Onboarding Guide](docs/onboarding.md)
65
+ - [Architecture Overview](docs/architecture.md)
66
+
67
+ ## 🔒 Proprietary Software
68
+
69
+ This is proprietary software. All rights reserved.
.github/dependabot.yml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/optimizing-pr-creation-version-updates#setting-up-a-cooldown-period-for-dependency-updates
2
+
3
+ version: 2
4
+ updates:
5
+ - package-ecosystem: cargo
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ labels: ["type: chore"]
10
+ # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#groups--
11
+ groups:
12
+ major:
13
+ update-types: ["major"]
14
+ minor:
15
+ update-types: ["minor"]
16
+ patch:
17
+ update-types: ["patch"]
18
+ - package-ecosystem: github-actions
19
+ directory: /
20
+ schedule:
21
+ interval: weekly
22
+ labels: ["type: chore"]
23
+ groups:
24
+ actions:
25
+ # Combine all images of last week
26
+ patterns: ["*"]
.github/labels.json ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "release: skip changelog",
4
+ "color": "d73a4a",
5
+ "description": "Skip this change from release notes.",
6
+ "aliases": ["skip"]
7
+ },
8
+ {
9
+ "name": "release: breaking",
10
+ "color": "d73a4a",
11
+ "description": "Introduces a breaking change.",
12
+ "aliases": [
13
+ "breaking change",
14
+ "breaking-change",
15
+ "Type: Breaking Change",
16
+ "type: breaking",
17
+ "breaking"
18
+ ]
19
+ },
20
+
21
+ {
22
+ "name": "good first issue",
23
+ "color": "7057ff",
24
+ "description": "Good for newcomers.",
25
+ "aliases": [
26
+ "beginner-friendly",
27
+ "beginner",
28
+ "good for beginner",
29
+ "Good for beginners",
30
+ "good-starter-issue",
31
+ "starter-issue",
32
+ "starter",
33
+ "status: good starter issue"
34
+ ]
35
+ },
36
+ {
37
+ "name": "help",
38
+ "color": "0e8a16",
39
+ "aliases": [
40
+ "help wanted",
41
+ "Patch Welcome",
42
+ "status: help wanted",
43
+ "Status: PR Welcome",
44
+ "Status: Ready for PR"
45
+ ]
46
+ },
47
+ {
48
+ "name": "state: approved",
49
+ "color": "C3F7F7",
50
+ "description": "Approved to proceed.",
51
+ "aliases": ["approved"]
52
+ },
53
+ {
54
+ "name": "state: blocked",
55
+ "color": "C3F7F7",
56
+ "description": "Something is blocking action.",
57
+ "aliases": ["blocked", "Status: Blocked", "status: blocked", "blocker"]
58
+ },
59
+ {
60
+ "name": "state: pending",
61
+ "color": "C3F7F7",
62
+ "description": "Action is underway but awaiting other tasks or data before completion.",
63
+ "aliases": [
64
+ "in progress",
65
+ "on hold",
66
+ "pending",
67
+ "Status: In Progress",
68
+ "Status: Review Needed",
69
+ "watchlist"
70
+ ]
71
+ },
72
+ {
73
+ "name": "state: inactive",
74
+ "color": "C3F7F7",
75
+ "description": "No current action needed/possible; issue fixed, out of scope, or superseded.",
76
+ "aliases": [
77
+ "duplicate",
78
+ "invalid",
79
+ "stale",
80
+ "Status: Abandoned",
81
+ "status: duplicate",
82
+ "status: wontfix",
83
+ "wont-fix",
84
+ "wontfix"
85
+ ]
86
+ },
87
+ {
88
+ "name": "type: bug",
89
+ "color": "FA1DD4",
90
+ "description": "Something isn't working.",
91
+ "aliases": ["bug", "Type: Bug"]
92
+ },
93
+ {
94
+ "name": "type: chore",
95
+ "color": "FA1DD4",
96
+ "description": "Routine tasks like conversions, reorganization, and maintenance work.",
97
+ "aliases": [
98
+ "chore",
99
+ "dependabot",
100
+ "dependencies",
101
+ "dependency",
102
+ "greenkeeper",
103
+ "legal",
104
+ "maintenance",
105
+ "Type: Maintenance",
106
+ "type: maintenance"
107
+ ]
108
+ },
109
+ {
110
+ "name": "type: discussion",
111
+ "color": "FA1DD4",
112
+ "description": "Questions, proposals and info that requires discussion.",
113
+ "aliases": [
114
+ "discussion",
115
+ "feedback",
116
+ "help wanted",
117
+ "idea",
118
+ "Idea",
119
+ "Proposal",
120
+ "proposal",
121
+ "question",
122
+ "type: design",
123
+ "Type: Question",
124
+ "type: question"
125
+ ]
126
+ },
127
+ {
128
+ "name": "type: docs",
129
+ "color": "FA1DD4",
130
+ "description": "Related to documentation and information.",
131
+ "aliases": [
132
+ "doc",
133
+ "docs",
134
+ "document",
135
+ "documentation",
136
+ "documents",
137
+ "Type: Documentation",
138
+ "type: documentation",
139
+ "specification"
140
+ ]
141
+ },
142
+ {
143
+ "name": "type: feature",
144
+ "color": "FA1DD4",
145
+ "description": "Brand new functionality, features, pages, workflows, endpoints, etc.",
146
+ "aliases": [
147
+ "addition",
148
+ "enhancement",
149
+ "feature",
150
+ "type: enhancement",
151
+ "Type: Feature",
152
+ "performance"
153
+ ]
154
+ },
155
+ {
156
+ "name": "type: fix",
157
+ "color": "FA1DD4",
158
+ "description": "Iterations on existing features or infrastructure.",
159
+ "aliases": [
160
+ "fix",
161
+ "improvement",
162
+ "optimization",
163
+ "Type: Fix",
164
+ "bug"
165
+ ]
166
+ },
167
+ {
168
+ "name": "type: refactor",
169
+ "color": "FA1DD4",
170
+ "description": "Code refactoring and restructuring.",
171
+ "aliases": [
172
+ "refactor",
173
+ "Type: Refactoring",
174
+ "refactoring"
175
+ ]
176
+ },
177
+ {
178
+ "name": "type: security",
179
+ "color": "FA1DD4",
180
+ "description": "Something is vulnerable or not secure.",
181
+ "aliases": ["security"]
182
+ },
183
+ {
184
+ "name": "type: performance",
185
+ "color": "FA1DD4",
186
+ "description": "Improved performance."
187
+ },
188
+ {
189
+ "name": "type: testing",
190
+ "color": "FA1DD4",
191
+ "description": "Related to testing.",
192
+ "aliases": ["test", "testing"]
193
+ },
194
+ {
195
+ "name": "work: chaotic",
196
+ "color": "0288d1",
197
+ "description": "The situation is chaotic, novel practices used.",
198
+ "aliases": ["chaotic"]
199
+ },
200
+ {
201
+ "name": "work: complex",
202
+ "color": "4fc3f7",
203
+ "description": "The situation is complex, emergent practices used.",
204
+ "aliases": ["complex"]
205
+ },
206
+ {
207
+ "name": "work: complicated",
208
+ "color": "81d4fa",
209
+ "description": "The situation is complicated, good practices used.",
210
+ "aliases": ["complicated"]
211
+ },
212
+ {
213
+ "name": "work: obvious",
214
+ "color": "e1f5fe",
215
+ "description": "The situation is obvious, best practices used.",
216
+ "aliases": ["obvious"]
217
+ },
218
+ {
219
+ "name": "ci: lint",
220
+ "color": "F9D0C4",
221
+ "description": "Automatically fix the linters issues and make a commit",
222
+ "aliases": ["ci:lint"]
223
+ },
224
+ {
225
+ "name": "ci: benchmark",
226
+ "color": "F9D0C4",
227
+ "description": "Runs benchmarks",
228
+ "aliases": ["ci:benchmark"]
229
+ },
230
+ {
231
+ "name": "type: provider",
232
+ "color": "0075ca",
233
+ "description": "Updates provider.json configuration.",
234
+ "aliases": ["provider", "update: provider"]
235
+ },
236
+ {
237
+ "name": "ci: test-jit",
238
+ "color": "F9D0C4",
239
+ "description": "Run all integration tests for the JIT optimized engine",
240
+ "aliases": ["ci:test-jit"]
241
+ },
242
+ {
243
+ "name": "os: windows",
244
+ "color": "0078d4",
245
+ "description": "Windows-specific issue or feature.",
246
+ "aliases": ["windows", "win32", "platform: windows"]
247
+ },
248
+ {
249
+ "name": "onlydust-wave",
250
+ "color": "FF6D00",
251
+ "description": "Related to OnlyDust Wave Hackathon"
252
+ },
253
+ {
254
+ "name": "ci: build all targets",
255
+ "color": "F9D0C4",
256
+ "description": "Build release binaries for all target platforms in CI",
257
+ "aliases": ["ci:build-all-targets", "build-all-targets"]
258
+ },
259
+ {
260
+ "name": "bounty",
261
+ "color": "F9C74F",
262
+ "description": "This issue has a bounty attached.",
263
+ "aliases": ["bounty:open"]
264
+ },
265
+ {
266
+ "name": "bounty: $100",
267
+ "color": "F9C74F",
268
+ "description": "This issue has a $100 bounty attached.",
269
+ "aliases": ["bounty:100", "bounty: 💰 $100"]
270
+ },
271
+ {
272
+ "name": "bounty: $200",
273
+ "color": "F9C74F",
274
+ "description": "This issue has a $200 bounty attached.",
275
+ "aliases": ["bounty:200", "bounty: 💰 $200"]
276
+ },
277
+ {
278
+ "name": "bounty: $300",
279
+ "color": "F9C74F",
280
+ "description": "This issue has a $300 bounty attached.",
281
+ "aliases": ["bounty:300", "bounty: 💰 $300"]
282
+ },
283
+ {
284
+ "name": "bounty: $500",
285
+ "color": "F9C74F",
286
+ "description": "This issue has a $500 bounty attached.",
287
+ "aliases": ["bounty:500", "bounty: 💰 $500"]
288
+ },
289
+ {
290
+ "name": "bounty: $800",
291
+ "color": "F9C74F",
292
+ "description": "This issue has a $800 bounty attached.",
293
+ "aliases": ["bounty:800", "bounty: 💰 $800"]
294
+ },
295
+ {
296
+ "name": "bounty: $1300",
297
+ "color": "F9C74F",
298
+ "description": "This issue has a $1300 bounty attached.",
299
+ "aliases": ["bounty:1300", "bounty: 💰 $1300"]
300
+ },
301
+ {
302
+ "name": "bounty: $2100",
303
+ "color": "F9C74F",
304
+ "description": "This issue has a $2100 bounty attached.",
305
+ "aliases": ["bounty:2100", "bounty: 💰 $2100"]
306
+ },
307
+ {
308
+ "name": "bounty: $3400",
309
+ "color": "F9C74F",
310
+ "description": "This issue has a $3400 bounty attached.",
311
+ "aliases": ["bounty:3400", "bounty: 💰 $3400"]
312
+ },
313
+ {
314
+ "name": "bounty: $5500",
315
+ "color": "F9C74F",
316
+ "description": "This issue has a $5500 bounty attached.",
317
+ "aliases": ["bounty:5500", "bounty: 💰 $5500"]
318
+ },
319
+ {
320
+ "name": "bounty: claimed",
321
+ "color": "F9C74F",
322
+ "description": "This bounty has been claimed and is being worked on.",
323
+ "aliases": ["bounty:claimed"]
324
+ },
325
+ {
326
+ "name": "bounty: rewarded",
327
+ "color": "F9C74F",
328
+ "description": "This bounty has been completed and the reward paid out.",
329
+ "aliases": ["bounty:rewarded", "bounty:earned"]
330
+ },
331
+ {
332
+ "name": "severity: low",
333
+ "color": "FFEB3B",
334
+ "description": "Minor impact; does not significantly affect functionality.",
335
+ "aliases": ["severity:low"]
336
+ },
337
+ {
338
+ "name": "severity: high",
339
+ "color": "FF9800",
340
+ "description": "Significant impact; core functionality is impaired.",
341
+ "aliases": ["severity:high"]
342
+ },
343
+ {
344
+ "name": "severity: critical",
345
+ "color": "B71C1C",
346
+ "description": "Severe impact; system is unusable or data is at risk.",
347
+ "aliases": ["severity:critical", "critical"]
348
+ }
349
+ ]
.github/release-drafter.yml ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name-template: "v$RESOLVED_VERSION"
2
+ tag-template: "v$RESOLVED_VERSION"
3
+ categories:
4
+ - title: "🚀 Features"
5
+ labels:
6
+ - "type: feature"
7
+
8
+ - title: "📝 Documentation"
9
+ labels:
10
+ - "type: docs"
11
+
12
+ - title: "🐛 Bug Fixes"
13
+ labels:
14
+ - "type: bug"
15
+ - "type: fix"
16
+ - "type: provider"
17
+
18
+ - title: "🚀 Performance"
19
+ labels:
20
+ - "type: performance"
21
+
22
+ - title: "🧰 Maintenance"
23
+ labels:
24
+ - "type: chore"
25
+ - "type: testing"
26
+ - "type: refactor"
27
+ - "ratatui"
28
+
29
+ - title: "💥 Breaking Changes"
30
+ labels:
31
+ - "release: breaking"
32
+
33
+ - title: "🔒 Security"
34
+ labels:
35
+ - "type: security"
36
+
37
+ change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
38
+ change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
39
+
40
+ version-resolver:
41
+ major:
42
+ labels:
43
+ - "release: breaking"
44
+ minor:
45
+ labels:
46
+ - "type: feature"
47
+ # Add direct commit message pattern matching for features
48
+ commits:
49
+ - "feat:"
50
+ - "feature:"
51
+ patch:
52
+ labels:
53
+ - "type: fix"
54
+ - "type: performance"
55
+ - "type: chore"
56
+ - "type: docs"
57
+ - "type: bug"
58
+ - "type: testing"
59
+ - "type: refactor"
60
+ - "type: provider"
61
+ # Add direct commit message pattern matching for patches
62
+ commits:
63
+ - "fix:"
64
+ - "perf:"
65
+ - "chore:"
66
+ - "docs:"
67
+ - "refactor:"
68
+ default: patch
69
+
70
+ exclude-labels:
71
+ - "release: skip changelog"
72
+
73
+ template: |
74
+ ## Changes
75
+
76
+ $CHANGES
77
+ autolabeler:
78
+ - label: "type: chore"
79
+ title:
80
+ - /chore/i
81
+
82
+ - label: "type: refactor"
83
+ title:
84
+ - /refactor/i
85
+ - /refactoring/i
86
+
87
+ - label: "type: fix"
88
+ title:
89
+ - /fix/i
90
+ - /hotfix/i
91
+ - /bugfix/i
92
+ - /patch/i
93
+
94
+ - label: "type: feature"
95
+ title:
96
+ - /feat/i
97
+ - /feature/i
98
+ - /enhancement/i
99
+ - /new/i
100
+
101
+ - label: "type: docs"
102
+ title:
103
+ - /doc/i
104
+ - /docs/i
105
+
106
+ - label: "type: performance"
107
+ title:
108
+ - /perf/i
109
+
110
+ - label: "ci: benchmark"
111
+ title:
112
+ - /perf/i
113
+ files:
114
+ - "/benches/"
115
+
116
+ - label: "type: provider"
117
+ files:
118
+ - "provider.json"
119
+
120
+ - label: "os: windows"
121
+ title:
122
+ - /windows/i
123
+ - /win32/i
124
+ - /winapi/i
125
+ - /powershell/i
126
+ - /scoop/i
127
+ - /chocolatey/i
128
+ - /winget/i
129
+ body:
130
+ - /windows[ -](only|specific|terminal|path|installer|build)/i
131
+ - /multiline paste.*windows/i
132
+ - /windows.*multiline paste/i
.github/scripts/bounty/README.md ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Bounty Automation
2
+
3
+ Automates the full lifecycle of issue bounties — from label propagation when a PR is opened, through claiming when work begins, to rewarding when a PR is merged.
4
+
5
+ ## Flow
6
+
7
+ ```
8
+ Issue created
9
+ └── maintainer adds bounty: $N label
10
+ │
11
+ ├── parse-sync-generic-bounty.ts → adds generic bounty label
12
+ │
13
+ ▼
14
+ Issue assigned to contributor
15
+ └── parse-sync-claimed.ts → adds bounty: claimed
16
+ │
17
+ ▼
18
+ Contributor opens PR with "Closes #N" / "Fixes #N" / "Resolves #N"
19
+ └── parse-propagate-label.ts → copies bounty: $N to PR
20
+ → posts comment on issue: "PR #X opened by @author"
21
+ │
22
+ ▼
23
+ PR is merged
24
+ └── parse-mark-rewarded.ts → adds bounty: rewarded to PR
25
+ → adds bounty: rewarded to linked issue(s)
26
+ → removes bounty: claimed from linked issue(s)
27
+ │
28
+ ▼
29
+ Bounty lifecycle complete
30
+
31
+ Removal path:
32
+ maintainer removes last bounty: $N label
33
+ └── parse-sync-generic-bounty.ts → removes generic bounty label
34
+ ```
35
+
36
+ ## Labels
37
+
38
+ | Label | Applied to | Set by |
39
+ | -------------------------------- | ---------- | ----------------------------------------------------------- |
40
+ | `bounty: $100` … `bounty: $5500` | Issue | Maintainer (manually) |
41
+ | `bounty` | Issue | `parse-sync-generic-bounty.ts` on value label add/remove |
42
+ | `bounty: claimed` | Issue | `parse-sync-claimed.ts` on assignment |
43
+ | `bounty: rewarded` | Issue + PR | `parse-mark-rewarded.ts` on merge |
44
+
45
+ Bounty values follow the Fibonacci sequence: **$100, $200, $300, $500, $800, $1300, $2100, $3400, $5500**.
46
+
47
+ ## Pipeline
48
+
49
+ Each workflow job runs a single Node process (`run.ts`) that executes all three stages in memory — no temp files, no shell piping:
50
+
51
+ ```
52
+ parse → ParsedIntent (in memory) → plan → BatchPlan (in memory) → execute
53
+ ```
54
+
55
+ **Stage 1 — parse** (`parse-*.ts`): Reads the GitHub Actions event payload from `GITHUB_EVENT_PATH` and returns a `ParsedIntent` object. Pure — makes no API calls.
56
+
57
+ **Stage 2 — plan** (`plan.ts`): Receives the `ParsedIntent`, fetches current labels only for targets not already known from the event payload, diffs desired vs actual state, and returns a `BatchPlan`. Minimises API calls by reusing label data already present in the event.
58
+
59
+ **Stage 3 — execute** (`execute.ts`): Receives the `BatchPlan` and applies all mutations. Label additions per target are sent as a single batched `POST /labels` call. Each removal is a separate `DELETE` (GitHub has no bulk-remove endpoint). Comments are posted last.
60
+
61
+ This design means:
62
+ - The parse stage is trivially unit-testable (pure function, no mocks needed).
63
+ - The plan stage is testable with a minimal mock that only needs `getLabels`.
64
+ - The execute stage is testable with a mock that tracks calls — no HTTP.
65
+ - API calls are minimised: additions are batched per target; label state already in the event payload is never re-fetched.
66
+
67
+ ## Scripts
68
+
69
+ The workflow invokes a single orchestrator script (`run.ts`) per job. The parse, plan, and execute modules are called directly in the same Node process, passing objects in memory.
70
+
71
+ ### `run.ts`
72
+
73
+ The entrypoint used by `bounty.yml`. Accepts a `--script` flag to select which parse module to run, then calls `plan` and `execute` in sequence — all in-process.
74
+
75
+ ```sh
76
+ npx tsx .github/scripts/bounty/run.ts \
77
+ --script <parse-script> \
78
+ --repo <owner/repo> \
79
+ --token <github-token> \
80
+ [--pr <number> | --issue <number>]
81
+ ```
82
+
83
+ ### `parse-propagate-label.ts`
84
+
85
+ Triggered by: `pull_request` — opened, edited, reopened.
86
+
87
+ 1. Parses the PR body for closing keywords (`closes`, `fixes`, `resolves`, case-insensitive).
88
+ 2. Returns a `ParsedIntent` with a `labelCopies` field — the plan stage fetches each linked issue's labels and copies any `bounty: $N` ones onto the PR.
89
+ 3. Includes a comment mutation per linked issue (the plan stage drops it if the issue has no bounty labels).
90
+
91
+ ### `parse-sync-claimed.ts`
92
+
93
+ Triggered by: `issues` — assigned, unassigned.
94
+
95
+ - **assigned**: returns `add: ["bounty: claimed"]` if the issue has a `bounty: $N` label.
96
+ - **unassigned**: returns `remove: ["bounty: claimed"]` only when no assignees remain.
97
+ - Issue labels are already in the event payload and supplied as `knownLabels` — no extra fetch.
98
+
99
+ ### `parse-sync-generic-bounty.ts`
100
+
101
+ Triggered by: `issues` — labeled, unlabeled.
102
+
103
+ Keeps the generic `bounty` label in sync with value labels. Inspects `event.label` (the label that just changed) and only acts when it matches `bounty: $`.
104
+
105
+ - **labeled**: returns `add: ["bounty"]`.
106
+ - **unlabeled**: returns `remove: ["bounty"]` only when no value labels remain (guards against mid-tier-swap removal when a maintainer swaps one value label for another).
107
+ - Issue labels from the event are supplied as `knownLabels` — no extra fetch.
108
+
109
+ ### `parse-mark-rewarded.ts`
110
+
111
+ Triggered by: `pull_request_target` — closed (merged only).
112
+
113
+ 1. Returns empty intent if the PR was not merged or has no `bounty: $N` label.
114
+ 2. Returns `add: ["bounty: rewarded"]` for the PR (labels known from event, no fetch).
115
+ 3. Parses the PR body for linked issues; returns `add: ["bounty: rewarded"], remove: ["bounty: claimed"]` for each (plan stage fetches their labels).
116
+
117
+ Uses `pull_request_target` so the job has write access to issues and PRs from forks.
118
+
119
+ ### `plan.ts`
120
+
121
+ Receives a `ParsedIntent` from the parse stage. Resolves `labelCopies` by fetching source issue labels. Fetches current labels for any target not already in `knownLabels`. Filters out no-op adds and removes. Returns a `BatchPlan`.
122
+
123
+ ### `execute.ts`
124
+
125
+ Receives a `BatchPlan` from the plan stage. For each mutation: one batched `POST` for all additions, one `DELETE` per removal, one `POST` per comment.
126
+
127
+ ## Shared Module
128
+
129
+ `github-api.ts` defines:
130
+ - Event payload types (`PullRequestEvent`, `IssuesEvent`)
131
+ - Pipeline types (`ParsedIntent`, `BatchPlan`, `TargetMutation`)
132
+ - The `GitHubApi` interface (injectable for testing)
133
+ - `GitHubRestApi` — the production implementation using `node:https`
134
+
135
+ All scripts import types from `github-api.ts` and accept a `GitHubApi` instance in their `run()` / `plan()` / `execute()` function signature, making every step independently mockable.
136
+
137
+ ## Tests
138
+
139
+ Unit tests live alongside each script (`*.test.ts`) and use Node's built-in `node:test` runner.
140
+
141
+ ```sh
142
+ npm run test:bounty
143
+ ```
144
+
145
+ - Parse tests: pure — no mock needed, just call `parse()` with a synthetic event.
146
+ - Plan and execute tests: use a mock `GitHubApi` that tracks calls and returns preset label lists.
147
+ - The CLI entrypoint in each script (yargs parsing + `GITHUB_EVENT_PATH` read) is guarded behind an `import.meta.url` check so it does not execute on import.
148
+
149
+ ## Workflow Source
150
+
151
+ `bounty.yml` is auto-generated from Rust source in `crates/forge_ci`. Do not edit it by hand — modify `crates/forge_ci/src/workflows/bounty.rs` and regenerate with:
152
+
153
+ ```sh
154
+ cargo test -p forge_ci
155
+ ```
.github/scripts/bounty/src/api.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // GitHub API abstraction for v2 bounty sync.
2
+ // Uses the native `fetch` API (available in Node 18+).
3
+
4
+ import type { Issue, Label, PullRequest } from "./types.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Interface — injectable for testing
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /// Minimal GitHub REST API surface needed by the bounty sync commands.
11
+ export interface GitHubApi {
12
+ /// Fetch a full issue by number. Throws on non-2xx.
13
+ getIssue(number: number): Promise<Issue>;
14
+ /// Fetch a full pull request by number. Throws on non-2xx.
15
+ getPullRequest(number: number): Promise<PullRequest>;
16
+ /// Fetch all open issues that have any label matching the given prefix.
17
+ /// Handles pagination automatically and excludes pull requests.
18
+ listIssuesWithLabelPrefix(prefix: string): Promise<Issue[]>;
19
+ /// Add one or more labels to an issue or PR. Batched into a single request.
20
+ addLabels(target: number, labels: string[]): Promise<void>;
21
+ /// Remove a single label from an issue or PR.
22
+ removeLabel(target: number, label: string): Promise<void>;
23
+ /// Post a comment on an issue or PR.
24
+ addComment(target: number, body: string): Promise<void>;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Production implementation
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /// Calls the real GitHub REST API v3 using `fetch`.
32
+ export class GitHubRestApi implements GitHubApi {
33
+ private readonly base: string;
34
+ private readonly headers: Record<string, string>;
35
+
36
+ constructor(
37
+ private readonly owner: string,
38
+ private readonly repo: string,
39
+ token: string
40
+ ) {
41
+ this.base = `https://api.github.com/repos/${owner}/${repo}`;
42
+ this.headers = {
43
+ Authorization: `Bearer ${token}`,
44
+ Accept: "application/vnd.github+json",
45
+ "X-GitHub-Api-Version": "2022-11-28",
46
+ "User-Agent": "bounty-bot/v2",
47
+ "Content-Type": "application/json",
48
+ };
49
+ }
50
+
51
+ private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
52
+ const res = await fetch(`${this.base}${path}`, {
53
+ method,
54
+ headers: this.headers,
55
+ body: body !== undefined ? JSON.stringify(body) : undefined,
56
+ });
57
+
58
+ if (!res.ok) {
59
+ const text = await res.text().catch(() => "");
60
+ throw new Error(`GitHub API ${method} ${path} → ${res.status}: ${text}`);
61
+ }
62
+
63
+ // 204 No Content
64
+ if (res.status === 204) return {} as T;
65
+
66
+ return res.json() as Promise<T>;
67
+ }
68
+
69
+ async getIssue(number: number): Promise<Issue> {
70
+ return this.request<Issue>("GET", `/issues/${number}`);
71
+ }
72
+
73
+ async getPullRequest(number: number): Promise<PullRequest> {
74
+ return this.request<PullRequest>("GET", `/pulls/${number}`);
75
+ }
76
+
77
+ async listIssuesWithLabelPrefix(prefix: string): Promise<Issue[]> {
78
+ const results: Issue[] = [];
79
+ let page = 1;
80
+ while (true) {
81
+ const batch = await this.request<Issue[]>(
82
+ "GET",
83
+ `/issues?state=open&per_page=100&page=${page}`
84
+ );
85
+ if (batch.length === 0) break;
86
+ for (const issue of batch) {
87
+ // Exclude pull requests (GitHub returns them in /issues)
88
+ if (issue.pull_request !== undefined) continue;
89
+ if (issue.labels.some((l) => l.name.startsWith(prefix))) {
90
+ results.push(issue);
91
+ }
92
+ }
93
+ if (batch.length < 100) break;
94
+ page++;
95
+ }
96
+ return results;
97
+ }
98
+
99
+ async addLabels(target: number, labels: string[]): Promise<void> {
100
+ await this.request("POST", `/issues/${target}/labels`, { labels });
101
+ }
102
+
103
+ async removeLabel(target: number, label: string): Promise<void> {
104
+ await this.request("DELETE", `/issues/${target}/labels/${encodeURIComponent(label)}`);
105
+ }
106
+
107
+ async addComment(target: number, body: string): Promise<void> {
108
+ await this.request("POST", `/issues/${target}/comments`, { body });
109
+ }
110
+ }
.github/scripts/bounty/src/rules.ts ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Rules engine for v2 bounty sync.
2
+ //
3
+ // Both exported functions are pure: they take current state and return the
4
+ // minimal Patch needed to reconcile it with the desired state.
5
+ // No I/O, no side effects — fully unit-testable without mocks.
6
+
7
+ import type { IssueState, PrState, Patch, LabelOp, Issue } from "./types.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Label name constants
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export const BOUNTY_GENERIC = "bounty";
14
+ export const BOUNTY_CLAIMED = "bounty: claimed";
15
+ export const BOUNTY_REWARDED = "bounty: rewarded";
16
+
17
+ /// Matches bounty value labels in both formats:
18
+ /// - new: "bounty: 💰 $100", "bounty: 💰 $5500"
19
+ /// - legacy: "bounty: $300", "bounty: $500"
20
+ export const VALUE_LABEL_RE = /^bounty: (?:💰 )?\$\d/;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /// Returns true when the label name is a bounty value label.
27
+ export function isBountyValue(label: string): boolean {
28
+ return VALUE_LABEL_RE.test(label);
29
+ }
30
+
31
+ /// Extract all issue numbers referenced by closing keywords in a PR body.
32
+ /// A new RegExp is created on each call to avoid stale `lastIndex` state.
33
+ export function linkedIssueNumbers(body: string | null): number[] {
34
+ const numbers: number[] = [];
35
+ const text = body ?? "";
36
+ const re = /(?:closes?|fix(?:es)?|resolves?)\s+#(\d+)/gi;
37
+ let m: RegExpExecArray | null;
38
+ while ((m = re.exec(text)) !== null) {
39
+ numbers.push(parseInt(m[1]!, 10));
40
+ }
41
+ return numbers;
42
+ }
43
+
44
+ /// Build a LabelOp that only includes labels that actually need to change.
45
+ function diff(
46
+ target: number,
47
+ current: Set<string>,
48
+ desired: Set<string>,
49
+ comment?: string,
50
+ meta?: { title?: string; url?: string }
51
+ ): LabelOp | null {
52
+ const add = [...desired].filter((l) => !current.has(l));
53
+ const remove = [...current].filter((l) => !desired.has(l));
54
+ if (add.length === 0 && remove.length === 0 && !comment) return null;
55
+ return { target, title: meta?.title, url: meta?.url, add, remove, comment };
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Issue rules
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /// Compute the desired label set for an issue given its full current state.
63
+ ///
64
+ /// Rules (all applied simultaneously, not sequentially):
65
+ ///
66
+ /// 1. **Generic bounty** — `bounty` must be present iff any value label is present.
67
+ /// 2. **Claimed** — `bounty: claimed` must be present iff the issue has a value
68
+ /// label AND at least one assignee. It must be absent otherwise.
69
+ /// 3. **Value labels and `bounty: rewarded`** — these are set by maintainers or
70
+ /// the PR merge job and are left untouched by the issue sync.
71
+ /// 4. All non-bounty labels are preserved as-is.
72
+ export function computeIssuePatch({ issue, currentLabels }: IssueState): Patch {
73
+ const desired = new Set(currentLabels);
74
+
75
+ const hasValueLabel = [...currentLabels].some(isBountyValue);
76
+ const isAssigned = issue.assignees.length > 0;
77
+
78
+ // Rule 1 — generic label mirrors value label presence.
79
+ if (hasValueLabel) {
80
+ desired.add(BOUNTY_GENERIC);
81
+ } else {
82
+ desired.delete(BOUNTY_GENERIC);
83
+ }
84
+
85
+ // Rule 2 — claimed mirrors assignment presence (only when bounty exists).
86
+ if (hasValueLabel && isAssigned) {
87
+ desired.add(BOUNTY_CLAIMED);
88
+ } else {
89
+ desired.delete(BOUNTY_CLAIMED);
90
+ }
91
+
92
+ const op = diff(issue.number, currentLabels, desired, undefined, {
93
+ title: issue.title,
94
+ url: issue.html_url,
95
+ });
96
+ return { ops: op ? [op] : [] };
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // PR rules
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /// Compute the desired state for a PR and its linked issues.
104
+ ///
105
+ /// Rules:
106
+ ///
107
+ /// 1. **Label propagation** — for each linked issue that has a value label,
108
+ /// copy that value label to the PR (if the PR doesn't already have it).
109
+ /// 2. **Rewarded on merge** — if the PR is merged and has a value label:
110
+ /// a. Add `bounty: rewarded` to the PR.
111
+ /// b. Add `bounty: rewarded` to each linked issue.
112
+ /// c. Remove `bounty: claimed` from each linked issue.
113
+ /// 3. **Comment on propagation** — when a value label is first added to the PR
114
+ /// via rule 1, post a comment on the source issue.
115
+ export function computePrPatch({ pr, currentLabels, linkedIssues }: PrState): Patch {
116
+ const ops: LabelOp[] = [];
117
+
118
+ const prDesired = new Set(currentLabels);
119
+
120
+ // Collect value labels to propagate from linked issues.
121
+ const labelsToPropagate: string[] = [];
122
+ for (const issue of linkedIssues) {
123
+ const issueValueLabels = issue.labels.map((l) => l.name).filter(isBountyValue);
124
+ for (const label of issueValueLabels) {
125
+ if (!prDesired.has(label)) {
126
+ labelsToPropagate.push(label);
127
+ prDesired.add(label);
128
+ }
129
+ }
130
+ }
131
+
132
+ const prHasValueLabel = [...prDesired].some(isBountyValue);
133
+
134
+ if (pr.merged && prHasValueLabel) {
135
+ // Rule 2 — rewarded lifecycle.
136
+ prDesired.add(BOUNTY_REWARDED);
137
+
138
+ const prOp = diff(pr.number, currentLabels, prDesired, undefined, {
139
+ title: pr.title,
140
+ url: pr.html_url,
141
+ });
142
+ if (prOp) ops.push(prOp);
143
+
144
+ // Update linked issues — only those that already have a bounty value label.
145
+ for (const issue of linkedIssues) {
146
+ const issueCurrent = new Set(issue.labels.map((l) => l.name));
147
+ const issueHasValue = [...issueCurrent].some(isBountyValue);
148
+ if (!issueHasValue) continue;
149
+
150
+ const issueDesired = new Set(issueCurrent);
151
+ issueDesired.add(BOUNTY_REWARDED);
152
+ issueDesired.delete(BOUNTY_CLAIMED);
153
+
154
+ const op = diff(issue.number, issueCurrent, issueDesired, undefined, {
155
+ title: issue.title,
156
+ url: issue.html_url,
157
+ });
158
+ if (op) ops.push(op);
159
+ }
160
+ } else {
161
+ // Rule 1 — label propagation (pre-merge).
162
+ const prOp = diff(pr.number, currentLabels, prDesired, undefined, {
163
+ title: pr.title,
164
+ url: pr.html_url,
165
+ });
166
+ if (prOp) ops.push(prOp);
167
+
168
+ // Rule 3 — comment on each issue whose value label was newly propagated.
169
+ if (labelsToPropagate.length > 0) {
170
+ for (const issue of linkedIssues) {
171
+ const hadValueLabel = issue.labels.map((l) => l.name).some(isBountyValue);
172
+ if (hadValueLabel) {
173
+ ops.push({
174
+ target: issue.number,
175
+ title: issue.title,
176
+ url: issue.html_url,
177
+ add: [],
178
+ remove: [],
179
+ comment: `PR [#${pr.number}](${pr.html_url}) has been opened for this bounty by @${pr.user.login}.`,
180
+ });
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ return { ops };
187
+ }
.github/scripts/bounty/src/sync-all-issues.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env tsx
2
+ // Syncs bounty labels across ALL open issues that carry any "bounty" label.
3
+ //
4
+ // Without --execute: fetches all matching issues, computes the combined patch,
5
+ // and prints a plan showing exactly what would change. No writes are made.
6
+ //
7
+ // With --execute: fetches, computes, and applies the patch for every issue.
8
+ //
9
+ // Usage:
10
+ // tsx sync-all-issues.ts --repo <owner/repo> --token <token> [--execute]
11
+
12
+ import * as url from "url";
13
+ import yargs from "yargs";
14
+ import { hideBin } from "yargs/helpers";
15
+ import { GitHubRestApi, type GitHubApi } from "./api.js";
16
+ import { computeIssuePatch } from "./rules.js";
17
+ import { applyPatch, printPlan, resolveToken } from "./sync-issue.js";
18
+ import type { Patch } from "./types.js";
19
+
20
+ const BOUNTY_LABEL_PREFIX = "bounty";
21
+
22
+ export interface PlanAllIssuesInput {
23
+ api: GitHubApi;
24
+ }
25
+
26
+ /// Fetches all open issues with any bounty label and computes the combined patch.
27
+ /// Makes no writes — safe to call at any time.
28
+ export async function planAllIssues({ api }: PlanAllIssuesInput): Promise<Patch> {
29
+ const issues = await api.listIssuesWithLabelPrefix(BOUNTY_LABEL_PREFIX);
30
+ const ops = issues.flatMap((issue) => {
31
+ const currentLabels = new Set(issue.labels.map((l) => l.name));
32
+ return computeIssuePatch({ issue, currentLabels }).ops;
33
+ });
34
+ return { ops };
35
+ }
36
+
37
+ /// Fetches, computes, and applies the label patch for all bounty issues.
38
+ /// Returns the patch that was applied.
39
+ export async function syncAllIssues({ api }: PlanAllIssuesInput): Promise<Patch> {
40
+ const patch = await planAllIssues({ api });
41
+ await applyPatch(patch, api);
42
+ return patch;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // CLI entrypoint
47
+ // ---------------------------------------------------------------------------
48
+
49
+ if (process.argv[1] === url.fileURLToPath(import.meta.url)) {
50
+ const argv = await yargs(hideBin(process.argv))
51
+ .option("repo", { type: "string", demandOption: true, description: "owner/repo" })
52
+ .option("token", {
53
+ type: "string",
54
+ description: "GitHub token (falls back to GITHUB_TOKEN env var or `gh auth token`)",
55
+ })
56
+ .option("execute", {
57
+ type: "boolean",
58
+ default: false,
59
+ description: "Apply the patch. Without this flag only the plan is printed.",
60
+ })
61
+ .strict()
62
+ .parseAsync();
63
+
64
+ const [owner, repo] = argv.repo.split("/") as [string, string];
65
+ const token = resolveToken(argv.token);
66
+ const api = new GitHubRestApi(owner, repo, token);
67
+
68
+ if (argv.execute) {
69
+ const patch = await syncAllIssues({ api });
70
+ if (patch.ops.length === 0) {
71
+ console.log("All issues already in sync — no changes needed.");
72
+ }
73
+ } else {
74
+ const patch = await planAllIssues({ api });
75
+ printPlan(patch, "All bounty issues");
76
+ }
77
+ }
.github/scripts/bounty/src/sync-issue.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env tsx
2
+ // Syncs all bounty labels on a single issue.
3
+ //
4
+ // Without --execute: fetches current state, computes the patch, and prints
5
+ // a plan showing exactly what would change. No writes are made.
6
+ //
7
+ // With --execute: fetches, computes, and applies the patch.
8
+ //
9
+ // Usage:
10
+ // tsx sync-issue.ts --issue <number> --repo <owner/repo> --token <token> [--execute]
11
+
12
+ import * as url from "url";
13
+ import { execSync } from "child_process";
14
+ import chalk from "chalk";
15
+ import yargs from "yargs";
16
+ import { hideBin } from "yargs/helpers";
17
+ import { GitHubRestApi, type GitHubApi } from "./api.js";
18
+ import { computeIssuePatch } from "./rules.js";
19
+ import type { Patch } from "./types.js";
20
+
21
+ /// Resolves a GitHub token from the provided value, the GITHUB_TOKEN env var,
22
+ /// or by invoking `gh auth token` as a fallback. Exits with an error if none
23
+ /// can be found.
24
+ export function resolveToken(flag: string | undefined): string {
25
+ const token = flag || process.env["GITHUB_TOKEN"] || (() => {
26
+ try {
27
+ return execSync("gh auth token", { encoding: "utf8" }).trim();
28
+ } catch {
29
+ return "";
30
+ }
31
+ })();
32
+ if (!token) {
33
+ console.error(
34
+ "Error: no GitHub token found. Pass --token, set GITHUB_TOKEN, or run `gh auth login`."
35
+ );
36
+ process.exit(1);
37
+ }
38
+ return token;
39
+ }
40
+
41
+ export interface PlanIssueInput {
42
+ issueNumber: number;
43
+ api: GitHubApi;
44
+ }
45
+
46
+ /// Fetches the current issue state and computes the minimal label patch.
47
+ /// Makes no writes — safe to call at any time.
48
+ export async function planIssue({ issueNumber, api }: PlanIssueInput): Promise<Patch> {
49
+ const issue = await api.getIssue(issueNumber);
50
+ const currentLabels = new Set(issue.labels.map((l) => l.name));
51
+ return computeIssuePatch({ issue, currentLabels });
52
+ }
53
+
54
+ /// Fetches, computes, and applies the label patch for a single issue.
55
+ /// Returns the patch that was applied (useful for tests and logging).
56
+ export async function syncIssue({ issueNumber, api }: PlanIssueInput): Promise<Patch> {
57
+ const patch = await planIssue({ issueNumber, api });
58
+ await applyPatch(patch, api);
59
+ return patch;
60
+ }
61
+
62
+ /// Applies a Patch against the GitHub API.
63
+ /// Each target gets one batched addLabels call; each removal is individual.
64
+ export async function applyPatch(patch: Patch, api: GitHubApi): Promise<void> {
65
+ for (const op of patch.ops) {
66
+ const ref = chalk.cyan(`#${op.target}`);
67
+ if (op.add.length > 0) {
68
+ await api.addLabels(op.target, op.add);
69
+ console.log(`${chalk.green("✔")} ${ref}: added [${chalk.green(op.add.join(", "))}]`);
70
+ }
71
+ for (const label of op.remove) {
72
+ await api.removeLabel(op.target, label);
73
+ console.log(`${chalk.red("✖")} ${ref}: removed "${chalk.red(label)}"`);
74
+ }
75
+ if (op.comment) {
76
+ await api.addComment(op.target, op.comment);
77
+ console.log(`${chalk.yellow("✉")} ${ref}: posted comment`);
78
+ }
79
+ }
80
+ }
81
+
82
+ /// Prints a human-readable plan to stdout without making any API calls.
83
+ export function printPlan(patch: Patch, subject: string): void {
84
+ if (patch.ops.length === 0) {
85
+ console.log(`${chalk.green("✔")} ${chalk.bold(subject)}: already in sync — no changes needed.`);
86
+ return;
87
+ }
88
+ console.log(`${chalk.yellow("●")} ${chalk.bold(subject)}: plan (${chalk.bold(String(patch.ops.length))} target(s) to update)\n`);
89
+ for (const op of patch.ops) {
90
+ const title = op.title ? chalk.bold(` ${op.title}`) : "";
91
+ const href = op.url ? `\n ${chalk.dim(chalk.blue(op.url))}` : "";
92
+ console.log(` ${chalk.cyan(`#${op.target}`)}${title}${href}`);
93
+ if (op.add.length > 0)
94
+ console.log(` ${chalk.green("+")} add: ${chalk.green(op.add.join(", "))}`);
95
+ if (op.remove.length > 0)
96
+ console.log(` ${chalk.red("-")} remove: ${chalk.red(op.remove.join(", "))}`);
97
+ if (op.comment)
98
+ console.log(` ${chalk.yellow("~")} comment: ${chalk.dim(op.comment.slice(0, 80))}…`);
99
+ }
100
+ console.log(`\n${chalk.dim("Run with --execute to apply.")}`);
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // CLI entrypoint
105
+ // ---------------------------------------------------------------------------
106
+
107
+ if (process.argv[1] === url.fileURLToPath(import.meta.url)) {
108
+ const argv = await yargs(hideBin(process.argv))
109
+ .option("issue", { type: "number", demandOption: true, description: "Issue number" })
110
+ .option("repo", { type: "string", demandOption: true, description: "owner/repo" })
111
+ .option("token", { type: "string", description: "GitHub token (falls back to GITHUB_TOKEN env var or `gh auth token`)" })
112
+ .option("execute", {
113
+ type: "boolean",
114
+ default: false,
115
+ description: "Apply the patch. Without this flag only the plan is printed.",
116
+ })
117
+ .strict()
118
+ .parseAsync();
119
+
120
+ const [owner, repo] = argv.repo.split("/") as [string, string];
121
+ const token = resolveToken(argv.token);
122
+ const api = new GitHubRestApi(owner, repo, token);
123
+
124
+ if (argv.execute) {
125
+ const patch = await syncIssue({ issueNumber: argv.issue, api });
126
+ if (patch.ops.length === 0) {
127
+ console.log(`Issue #${argv.issue}: already in sync, no changes needed.`);
128
+ }
129
+ } else {
130
+ const patch = await planIssue({ issueNumber: argv.issue, api });
131
+ printPlan(patch, `Issue #${argv.issue}`);
132
+ }
133
+ }
.github/scripts/bounty/src/sync-pr.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env tsx
2
+ // Syncs bounty labels on a PR (and its linked issues).
3
+ //
4
+ // Without --execute: fetches current state, computes the patch, and prints
5
+ // a plan showing exactly what would change. No writes are made.
6
+ //
7
+ // With --execute: fetches, computes, and applies the patch.
8
+ //
9
+ // Usage:
10
+ // tsx sync-pr.ts --pr <number> --repo <owner/repo> --token <token> [--execute]
11
+
12
+ import * as url from "url";
13
+ import yargs from "yargs";
14
+ import { hideBin } from "yargs/helpers";
15
+ import { GitHubRestApi, type GitHubApi } from "./api.js";
16
+ import { computePrPatch, linkedIssueNumbers } from "./rules.js";
17
+ import { applyPatch, printPlan, resolveToken } from "./sync-issue.js";
18
+ import type { Patch } from "./types.js";
19
+
20
+ export interface PlanPrInput {
21
+ prNumber: number;
22
+ api: GitHubApi;
23
+ }
24
+
25
+ /// Fetches the current PR + linked issue state and computes the minimal label patch.
26
+ /// Makes no writes — safe to call at any time.
27
+ export async function planPr({ prNumber, api }: PlanPrInput): Promise<Patch> {
28
+ const pr = await api.getPullRequest(prNumber);
29
+ const currentLabels = new Set(pr.labels.map((l) => l.name));
30
+ const issueNumbers = linkedIssueNumbers(pr.body);
31
+
32
+ const linkedIssues = await Promise.all(
33
+ issueNumbers.map((n) =>
34
+ api.getIssue(n).catch((err) => {
35
+ console.warn(`Could not fetch linked issue #${n}: ${String(err)}, skipping.`);
36
+ return null;
37
+ })
38
+ )
39
+ ).then((results) => results.filter((i): i is NonNullable<typeof i> => i !== null));
40
+
41
+ return computePrPatch({ pr, currentLabels, linkedIssues });
42
+ }
43
+
44
+ /// Fetches, computes, and applies the label patch for a PR and its linked issues.
45
+ /// Returns the patch that was applied (useful for tests and logging).
46
+ export async function syncPr({ prNumber, api }: PlanPrInput): Promise<Patch> {
47
+ const patch = await planPr({ prNumber, api });
48
+ await applyPatch(patch, api);
49
+ return patch;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // CLI entrypoint
54
+ // ---------------------------------------------------------------------------
55
+
56
+ if (process.argv[1] === url.fileURLToPath(import.meta.url)) {
57
+ const argv = await yargs(hideBin(process.argv))
58
+ .option("pr", { type: "number", demandOption: true, description: "PR number" })
59
+ .option("repo", { type: "string", demandOption: true, description: "owner/repo" })
60
+ .option("token", { type: "string", description: "GitHub token (falls back to GITHUB_TOKEN env var or `gh auth token`)" })
61
+ .option("execute", {
62
+ type: "boolean",
63
+ default: false,
64
+ description: "Apply the patch. Without this flag only the plan is printed.",
65
+ })
66
+ .strict()
67
+ .parseAsync();
68
+
69
+ const [owner, repo] = argv.repo.split("/") as [string, string];
70
+ const token = resolveToken(argv.token);
71
+ const api = new GitHubRestApi(owner, repo, token);
72
+
73
+ if (argv.execute) {
74
+ const patch = await syncPr({ prNumber: argv.pr, api });
75
+ if (patch.ops.length === 0) {
76
+ console.log(`PR #${argv.pr}: already in sync, no changes needed.`);
77
+ }
78
+ } else {
79
+ const patch = await planPr({ prNumber: argv.pr, api });
80
+ printPlan(patch, `PR #${argv.pr}`);
81
+ }
82
+ }
.github/scripts/bounty/src/types.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Core domain types for v2 bounty sync.
2
+ //
3
+ // Design: both sync commands work by fetching the complete current state of an
4
+ // issue or PR from GitHub, deriving the *desired* label set from the rules, and
5
+ // computing a minimal patch (add / remove) to reconcile the two.
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // GitHub resource types (as returned by the REST API)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface Label {
12
+ name: string;
13
+ }
14
+
15
+ export interface User {
16
+ login: string;
17
+ }
18
+
19
+ /// Full issue as returned by GET /repos/:owner/:repo/issues/:number
20
+ export interface Issue {
21
+ number: number;
22
+ title: string;
23
+ html_url: string;
24
+ state: "open" | "closed";
25
+ labels: Label[];
26
+ assignees: User[];
27
+ pull_request?: unknown; // present when the issue is actually a PR
28
+ }
29
+
30
+ /// Full pull request as returned by GET /repos/:owner/:repo/pulls/:number
31
+ export interface PullRequest {
32
+ number: number;
33
+ title: string;
34
+ state: "open" | "closed";
35
+ merged: boolean;
36
+ body: string | null;
37
+ labels: Label[];
38
+ user: User;
39
+ html_url: string;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Derived state used by the rules engine
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /// Everything the rules engine needs to know about an issue.
47
+ export interface IssueState {
48
+ issue: Issue;
49
+ /// Current label names on the issue.
50
+ currentLabels: Set<string>;
51
+ }
52
+
53
+ /// Everything the rules engine needs to know about a PR and its linked issues.
54
+ export interface PrState {
55
+ pr: PullRequest;
56
+ /// Current label names on the PR.
57
+ currentLabels: Set<string>;
58
+ /// Full state of each issue linked via "closes/fixes/resolves #N" in the PR body.
59
+ linkedIssues: Issue[];
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Patch types — the minimal diff to apply
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /// A label operation on a single target (issue or PR number).
67
+ export interface LabelOp {
68
+ target: number;
69
+ /// Human-readable title of the issue or PR, for display in plan output.
70
+ title?: string;
71
+ /// URL of the issue or PR, for display in plan output.
72
+ url?: string;
73
+ add: string[];
74
+ remove: string[];
75
+ /// Optional comment to post on the target after label ops.
76
+ comment?: string;
77
+ }
78
+
79
+ /// The complete set of operations to apply.
80
+ export interface Patch {
81
+ ops: LabelOp[];
82
+ }
.github/scripts/bounty/tests/rules.test.ts ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ computeIssuePatch,
5
+ computePrPatch,
6
+ linkedIssueNumbers,
7
+ BOUNTY_GENERIC,
8
+ BOUNTY_CLAIMED,
9
+ BOUNTY_REWARDED,
10
+ } from "../src/rules.js";
11
+ import type { Issue, PullRequest, IssueState, PrState } from "../src/types.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Fixtures
15
+ // ---------------------------------------------------------------------------
16
+
17
+ function makeIssue(overrides: Partial<Issue> & { number: number }): Issue {
18
+ return {
19
+ title: "Test issue",
20
+ html_url: `https://github.com/owner/repo/issues/${overrides.number}`,
21
+ state: "open",
22
+ labels: [],
23
+ assignees: [],
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ function makePr(overrides: Partial<PullRequest> & { number: number }): PullRequest {
29
+ return {
30
+ title: "Test PR",
31
+ state: "open",
32
+ merged: false,
33
+ body: null,
34
+ labels: [],
35
+ user: { login: "contributor" },
36
+ html_url: `https://github.com/owner/repo/pull/${overrides.number}`,
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ function labelNames(...names: string[]): { name: string }[] {
42
+ return names.map((name) => ({ name }));
43
+ }
44
+
45
+ function issueState(issue: Issue): IssueState {
46
+ return { issue, currentLabels: new Set(issue.labels.map((l) => l.name)) };
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // computeIssuePatch — issue rules
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe("computeIssuePatch", () => {
54
+ describe("no bounty label", () => {
55
+ it("produces no patch when issue has no bounty labels at all", () => {
56
+ const issue = makeIssue({ number: 1, labels: labelNames("type: bug") });
57
+ const patch = computeIssuePatch(issueState(issue));
58
+ assert.deepEqual(patch.ops, []);
59
+ });
60
+
61
+ it("removes generic bounty label when no value label present", () => {
62
+ const issue = makeIssue({ number: 1, labels: labelNames(BOUNTY_GENERIC) });
63
+ const patch = computeIssuePatch(issueState(issue));
64
+ assert.equal(patch.ops.length, 1);
65
+ assert.deepEqual(patch.ops[0]!.remove, [BOUNTY_GENERIC]);
66
+ assert.deepEqual(patch.ops[0]!.add, []);
67
+ });
68
+
69
+ it("removes claimed label when no value label present", () => {
70
+ const issue = makeIssue({
71
+ number: 1,
72
+ labels: labelNames(BOUNTY_CLAIMED),
73
+ assignees: [{ login: "alice" }],
74
+ });
75
+ const patch = computeIssuePatch(issueState(issue));
76
+ assert.equal(patch.ops.length, 1);
77
+ assert.deepEqual(patch.ops[0]!.remove, [BOUNTY_CLAIMED]);
78
+ });
79
+ });
80
+
81
+ describe("has value label, no assignees", () => {
82
+ it("adds generic label when missing", () => {
83
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
84
+ const patch = computeIssuePatch(issueState(issue));
85
+ assert.equal(patch.ops.length, 1);
86
+ assert.deepEqual(patch.ops[0]!.add, [BOUNTY_GENERIC]);
87
+ assert.deepEqual(patch.ops[0]!.remove, []);
88
+ });
89
+
90
+ it("recognises legacy label format (no emoji) as a value label", () => {
91
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: $300") });
92
+ const patch = computeIssuePatch(issueState(issue));
93
+ assert.equal(patch.ops.length, 1);
94
+ assert.deepEqual(patch.ops[0]!.add, [BOUNTY_GENERIC]);
95
+ assert.deepEqual(patch.ops[0]!.remove, []);
96
+ });
97
+
98
+ it("produces no patch when generic already present and no assignees", () => {
99
+ const issue = makeIssue({
100
+ number: 1,
101
+ labels: labelNames("bounty: 💰 $500", BOUNTY_GENERIC),
102
+ });
103
+ const patch = computeIssuePatch(issueState(issue));
104
+ assert.deepEqual(patch.ops, []);
105
+ });
106
+
107
+ it("removes claimed when no assignees", () => {
108
+ const issue = makeIssue({
109
+ number: 1,
110
+ labels: labelNames("bounty: 💰 $500", BOUNTY_GENERIC, BOUNTY_CLAIMED),
111
+ assignees: [],
112
+ });
113
+ const patch = computeIssuePatch(issueState(issue));
114
+ assert.equal(patch.ops.length, 1);
115
+ assert.deepEqual(patch.ops[0]!.remove, [BOUNTY_CLAIMED]);
116
+ assert.deepEqual(patch.ops[0]!.add, []);
117
+ });
118
+ });
119
+
120
+ describe("has value label and assignees", () => {
121
+ it("adds both generic and claimed when both missing", () => {
122
+ const issue = makeIssue({
123
+ number: 1,
124
+ labels: labelNames("bounty: 💰 $300"),
125
+ assignees: [{ login: "alice" }],
126
+ });
127
+ const patch = computeIssuePatch(issueState(issue));
128
+ assert.equal(patch.ops.length, 1);
129
+ assert.deepEqual(patch.ops[0]!.add.sort(), [BOUNTY_CLAIMED, BOUNTY_GENERIC].sort());
130
+ assert.deepEqual(patch.ops[0]!.remove, []);
131
+ });
132
+
133
+ it("produces no patch when generic and claimed both already present", () => {
134
+ const issue = makeIssue({
135
+ number: 1,
136
+ labels: labelNames("bounty: 💰 $300", BOUNTY_GENERIC, BOUNTY_CLAIMED),
137
+ assignees: [{ login: "alice" }],
138
+ });
139
+ const patch = computeIssuePatch(issueState(issue));
140
+ assert.deepEqual(patch.ops, []);
141
+ });
142
+
143
+ it("preserves non-bounty labels untouched", () => {
144
+ const issue = makeIssue({
145
+ number: 1,
146
+ labels: labelNames("type: bug", "bounty: 💰 $100", BOUNTY_GENERIC, BOUNTY_CLAIMED),
147
+ assignees: [{ login: "alice" }],
148
+ });
149
+ const patch = computeIssuePatch(issueState(issue));
150
+ assert.deepEqual(patch.ops, []);
151
+ });
152
+ });
153
+
154
+ describe("rewarded state", () => {
155
+ it("leaves rewarded label alone (owned by PR sync)", () => {
156
+ const issue = makeIssue({
157
+ number: 1,
158
+ labels: labelNames("bounty: 💰 $500", BOUNTY_GENERIC, BOUNTY_REWARDED),
159
+ assignees: [],
160
+ });
161
+ const patch = computeIssuePatch(issueState(issue));
162
+ // rewarded is not touched; only generic should be kept since value label present
163
+ assert.deepEqual(patch.ops, []);
164
+ });
165
+ });
166
+ });
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // linkedIssueNumbers
170
+ // ---------------------------------------------------------------------------
171
+
172
+ describe("linkedIssueNumbers", () => {
173
+ it("returns empty array for null body", () => {
174
+ assert.deepEqual(linkedIssueNumbers(null), []);
175
+ });
176
+
177
+ it("parses closes/fixes/resolves keywords", () => {
178
+ const body = "Closes #10\nFixes #20\nResolves #30";
179
+ assert.deepEqual(linkedIssueNumbers(body), [10, 20, 30]);
180
+ });
181
+
182
+ it("is case-insensitive", () => {
183
+ const body = "CLOSES #5 fixes #6 RESOLVES #7";
184
+ assert.deepEqual(linkedIssueNumbers(body), [5, 6, 7]);
185
+ });
186
+
187
+ it("handles plural forms: close/fix/resolve", () => {
188
+ const body = "close #1 fix #2 resolve #3";
189
+ assert.deepEqual(linkedIssueNumbers(body), [1, 2, 3]);
190
+ });
191
+
192
+ it("returns empty when no keywords present", () => {
193
+ assert.deepEqual(linkedIssueNumbers("See issue #99 for context."), []);
194
+ });
195
+ });
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // computePrPatch — PR rules
199
+ // ---------------------------------------------------------------------------
200
+
201
+ describe("computePrPatch", () => {
202
+ describe("label propagation (pre-merge)", () => {
203
+ it("produces no patch when PR body has no linked issues", () => {
204
+ const pr = makePr({ number: 10, body: "No references here." });
205
+ const patch = computePrPatch({ pr, currentLabels: new Set(), linkedIssues: [] });
206
+ assert.deepEqual(patch.ops, []);
207
+ });
208
+
209
+ it("produces no patch when linked issues have no bounty labels", () => {
210
+ const pr = makePr({ number: 10, body: "Closes #1" });
211
+ const issue = makeIssue({ number: 1, labels: labelNames("type: bug") });
212
+ const patch = computePrPatch({ pr, currentLabels: new Set(), linkedIssues: [issue] });
213
+ assert.deepEqual(patch.ops, []);
214
+ });
215
+
216
+ it("copies bounty value label from linked issue to PR", () => {
217
+ const pr = makePr({ number: 10, body: "Closes #1" });
218
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
219
+ const patch = computePrPatch({ pr, currentLabels: new Set(), linkedIssues: [issue] });
220
+
221
+ const prOp = patch.ops.find((o) => o.target === 10);
222
+ assert.ok(prOp);
223
+ assert.deepEqual(prOp.add, ["bounty: 💰 $500"]);
224
+ assert.deepEqual(prOp.remove, []);
225
+ });
226
+
227
+ it("does not duplicate a value label already on the PR", () => {
228
+ const pr = makePr({ number: 10, body: "Closes #1" });
229
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
230
+ const patch = computePrPatch({
231
+ pr,
232
+ currentLabels: new Set(["bounty: 💰 $500"]),
233
+ linkedIssues: [issue],
234
+ });
235
+ assert.deepEqual(patch.ops, []);
236
+ });
237
+
238
+ it("posts a comment on the linked issue when label is propagated", () => {
239
+ const pr = makePr({ number: 10, body: "Closes #1" });
240
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $200") });
241
+ const patch = computePrPatch({ pr, currentLabels: new Set(), linkedIssues: [issue] });
242
+
243
+ const issueOp = patch.ops.find((o) => o.target === 1);
244
+ assert.ok(issueOp);
245
+ assert.ok(issueOp.comment?.includes("PR"));
246
+ assert.ok(issueOp.comment?.includes("#10"));
247
+ assert.ok(issueOp.comment?.includes("contributor"));
248
+ });
249
+
250
+ it("does not post a comment when label was already on the PR", () => {
251
+ const pr = makePr({ number: 10, body: "Closes #1" });
252
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $200") });
253
+ const patch = computePrPatch({
254
+ pr,
255
+ currentLabels: new Set(["bounty: 💰 $200"]),
256
+ linkedIssues: [issue],
257
+ });
258
+ const issueOp = patch.ops.find((o) => o.target === 1);
259
+ assert.ok(!issueOp);
260
+ });
261
+
262
+ it("merges value labels from multiple linked issues onto the PR", () => {
263
+ const pr = makePr({ number: 10, body: "Closes #1\nCloses #2" });
264
+ const issue1 = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $100") });
265
+ const issue2 = makeIssue({ number: 2, labels: labelNames("bounty: 💰 $200") });
266
+ const patch = computePrPatch({
267
+ pr,
268
+ currentLabels: new Set(),
269
+ linkedIssues: [issue1, issue2],
270
+ });
271
+
272
+ const prOp = patch.ops.find((o) => o.target === 10);
273
+ assert.ok(prOp);
274
+ assert.deepEqual(prOp.add.sort(), ["bounty: 💰 $100", "bounty: 💰 $200"].sort());
275
+ });
276
+ });
277
+
278
+ describe("rewarded on merge", () => {
279
+ it("produces no patch when PR was closed but not merged", () => {
280
+ const pr = makePr({ number: 10, state: "closed", merged: false, body: "Closes #1" });
281
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
282
+ const patch = computePrPatch({
283
+ pr,
284
+ currentLabels: new Set(["bounty: 💰 $500"]),
285
+ linkedIssues: [issue],
286
+ });
287
+ const prOp = patch.ops.find((o) => o.target === 10);
288
+ assert.ok(!prOp?.add.includes(BOUNTY_REWARDED));
289
+ });
290
+
291
+ it("adds rewarded to PR on merge", () => {
292
+ const pr = makePr({
293
+ number: 10,
294
+ merged: true,
295
+ body: "Closes #1",
296
+ labels: labelNames("bounty: 💰 $500"),
297
+ });
298
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
299
+ const patch = computePrPatch({
300
+ pr,
301
+ currentLabels: new Set(["bounty: 💰 $500"]),
302
+ linkedIssues: [issue],
303
+ });
304
+
305
+ const prOp = patch.ops.find((o) => o.target === 10);
306
+ assert.ok(prOp);
307
+ assert.ok(prOp.add.includes(BOUNTY_REWARDED));
308
+ });
309
+
310
+ it("adds rewarded to linked issue on merge", () => {
311
+ const pr = makePr({
312
+ number: 10,
313
+ merged: true,
314
+ body: "Closes #1",
315
+ labels: labelNames("bounty: 💰 $500"),
316
+ });
317
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500", BOUNTY_CLAIMED) });
318
+ const patch = computePrPatch({
319
+ pr,
320
+ currentLabels: new Set(["bounty: 💰 $500"]),
321
+ linkedIssues: [issue],
322
+ });
323
+
324
+ const issueOp = patch.ops.find((o) => o.target === 1);
325
+ assert.ok(issueOp);
326
+ assert.ok(issueOp.add.includes(BOUNTY_REWARDED));
327
+ assert.ok(issueOp.remove.includes(BOUNTY_CLAIMED));
328
+ });
329
+
330
+ it("does not add rewarded again when already present", () => {
331
+ const pr = makePr({
332
+ number: 10,
333
+ merged: true,
334
+ body: "Closes #1",
335
+ labels: labelNames("bounty: 💰 $500", BOUNTY_REWARDED),
336
+ });
337
+ const issue = makeIssue({
338
+ number: 1,
339
+ labels: labelNames("bounty: 💰 $500", BOUNTY_REWARDED),
340
+ });
341
+ const patch = computePrPatch({
342
+ pr,
343
+ currentLabels: new Set(["bounty: 💰 $500", BOUNTY_REWARDED]),
344
+ linkedIssues: [issue],
345
+ });
346
+ assert.deepEqual(patch.ops, []);
347
+ });
348
+
349
+ it("skips issues with no bounty label when merged PR has a bounty", () => {
350
+ const pr = makePr({
351
+ number: 10,
352
+ merged: true,
353
+ body: "Closes #1\nCloses #2",
354
+ labels: labelNames("bounty: 💰 $500"),
355
+ });
356
+ const issue1 = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
357
+ const issue2 = makeIssue({ number: 2, labels: labelNames("type: bug") });
358
+ const patch = computePrPatch({
359
+ pr,
360
+ currentLabels: new Set(["bounty: 💰 $500"]),
361
+ linkedIssues: [issue1, issue2],
362
+ });
363
+
364
+ // issue2 has no bounty so its desired state equals current: no op
365
+ const issue2Op = patch.ops.find((o) => o.target === 2);
366
+ assert.ok(!issue2Op);
367
+ });
368
+ });
369
+ });
.github/scripts/bounty/tests/sync-all-issues.test.ts ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { planAllIssues, syncAllIssues } from "../src/sync-all-issues.js";
4
+ import type { GitHubApi } from "../src/api.js";
5
+ import type { Issue, PullRequest } from "../src/types.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mock API
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function makeMockApi(issues: Issue[]): GitHubApi & {
12
+ added: Map<number, string[][]>;
13
+ removed: Map<number, string[]>;
14
+ } {
15
+ const added = new Map<number, string[][]>();
16
+ const removed = new Map<number, string[]>();
17
+
18
+ return {
19
+ added,
20
+ removed,
21
+ async getIssue(number) {
22
+ const found = issues.find((i) => i.number === number);
23
+ if (!found) throw new Error(`Issue #${number} not found in mock`);
24
+ return found;
25
+ },
26
+ async getPullRequest(): Promise<PullRequest> {
27
+ throw new Error("not used");
28
+ },
29
+ async listIssuesWithLabelPrefix(_prefix) {
30
+ // Return only real issues (not PRs — pull_request field absent)
31
+ return issues.filter((i) => i.pull_request === undefined);
32
+ },
33
+ async addLabels(target, labels) {
34
+ if (!added.has(target)) added.set(target, []);
35
+ added.get(target)!.push(labels);
36
+ },
37
+ async removeLabel(target, label) {
38
+ if (!removed.has(target)) removed.set(target, []);
39
+ removed.get(target)!.push(label);
40
+ },
41
+ async addComment() {},
42
+ };
43
+ }
44
+
45
+ function makeIssue(overrides: Partial<Issue> & { number: number }): Issue {
46
+ return { title: "Test issue", html_url: `https://github.com/owner/repo/issues/${overrides.number}`, state: "open", labels: [], assignees: [], ...overrides };
47
+ }
48
+
49
+ function labelNames(...names: string[]): { name: string }[] {
50
+ return names.map((name) => ({ name }));
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Tests
55
+ // ---------------------------------------------------------------------------
56
+
57
+ describe("planAllIssues", () => {
58
+ it("returns empty patch when no issues need changes", async () => {
59
+ const issues = [
60
+ makeIssue({ number: 1, labels: labelNames("bounty: $100", "bounty") }),
61
+ makeIssue({ number: 2, labels: labelNames("type: bug") }),
62
+ ];
63
+ const api = makeMockApi(issues);
64
+ const patch = await planAllIssues({ api });
65
+ assert.deepEqual(patch.ops, []);
66
+ });
67
+
68
+ it("plans adds for issues missing the generic bounty label", async () => {
69
+ const issues = [
70
+ makeIssue({ number: 1, labels: labelNames("bounty: $100") }),
71
+ makeIssue({ number: 2, labels: labelNames("bounty: $200") }),
72
+ ];
73
+ const api = makeMockApi(issues);
74
+ const patch = await planAllIssues({ api });
75
+ assert.equal(patch.ops.length, 2);
76
+ assert.ok(patch.ops.every((op) => op.add.includes("bounty")));
77
+ });
78
+
79
+ it("plans removal of stale claimed label when issue is unassigned", async () => {
80
+ const issues = [
81
+ makeIssue({
82
+ number: 3,
83
+ labels: labelNames("bounty: $500", "bounty", "bounty: claimed"),
84
+ assignees: [],
85
+ }),
86
+ ];
87
+ const api = makeMockApi(issues);
88
+ const patch = await planAllIssues({ api });
89
+ assert.equal(patch.ops.length, 1);
90
+ assert.deepEqual(patch.ops[0]!.remove, ["bounty: claimed"]);
91
+ });
92
+
93
+ it("collects ops for multiple issues with different needs", async () => {
94
+ const issues = [
95
+ makeIssue({ number: 1, labels: labelNames("bounty: $100") }), // missing generic
96
+ makeIssue({ number: 2, labels: labelNames("bounty: $200", "bounty") }), // already in sync
97
+ makeIssue({
98
+ number: 3,
99
+ labels: labelNames("bounty: $300", "bounty", "bounty: claimed"),
100
+ assignees: [],
101
+ }), // stale claimed
102
+ ];
103
+ const api = makeMockApi(issues);
104
+ const patch = await planAllIssues({ api });
105
+ assert.equal(patch.ops.length, 2);
106
+ const targets = patch.ops.map((op) => op.target).sort();
107
+ assert.deepEqual(targets, [1, 3]);
108
+ });
109
+ });
110
+
111
+ describe("syncAllIssues", () => {
112
+ it("applies adds and removals across all issues", async () => {
113
+ const issues = [
114
+ makeIssue({ number: 1, labels: labelNames("bounty: $100") }),
115
+ makeIssue({
116
+ number: 2,
117
+ labels: labelNames("bounty: $200", "bounty", "bounty: claimed"),
118
+ assignees: [],
119
+ }),
120
+ ];
121
+ const api = makeMockApi(issues);
122
+ await syncAllIssues({ api });
123
+
124
+ assert.ok(api.added.get(1)?.[0]?.includes("bounty"));
125
+ assert.deepEqual(api.removed.get(2), ["bounty: claimed"]);
126
+ });
127
+
128
+ it("returns empty patch when all issues are already in sync", async () => {
129
+ const issues = [
130
+ makeIssue({ number: 1, labels: labelNames("bounty: $100", "bounty") }),
131
+ ];
132
+ const api = makeMockApi(issues);
133
+ const patch = await syncAllIssues({ api });
134
+ assert.deepEqual(patch.ops, []);
135
+ assert.equal(api.added.size, 0);
136
+ assert.equal(api.removed.size, 0);
137
+ });
138
+ });
.github/scripts/bounty/tests/sync-issue.test.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { syncIssue } from "../src/sync-issue.js";
4
+ import type { GitHubApi } from "../src/api.js";
5
+ import type { Issue, PullRequest } from "../src/types.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mock API
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function makeMockApi(issue: Issue): GitHubApi & {
12
+ added: string[][];
13
+ removed: string[];
14
+ comments: string[];
15
+ } {
16
+ const added: string[][] = [];
17
+ const removed: string[] = [];
18
+ const comments: string[] = [];
19
+
20
+ return {
21
+ added,
22
+ removed,
23
+ comments,
24
+ async getIssue() {
25
+ return issue;
26
+ },
27
+ async getPullRequest(): Promise<PullRequest> {
28
+ throw new Error("not used");
29
+ },
30
+ async listIssuesWithLabelPrefix(): Promise<Issue[]> {
31
+ throw new Error("not used");
32
+ },
33
+ async addLabels(_target, labels) {
34
+ added.push(labels);
35
+ },
36
+ async removeLabel(_target, label) {
37
+ removed.push(label);
38
+ },
39
+ async addComment(_target, body) {
40
+ comments.push(body);
41
+ },
42
+ };
43
+ }
44
+
45
+ function makeIssue(overrides: Partial<Issue> & { number: number }): Issue {
46
+ return {
47
+ title: "Test issue",
48
+ html_url: `https://github.com/owner/repo/issues/${overrides.number}`,
49
+ state: "open",
50
+ labels: [],
51
+ assignees: [],
52
+ ...overrides,
53
+ };
54
+ }
55
+
56
+ function labelNames(...names: string[]): { name: string }[] {
57
+ return names.map((name) => ({ name }));
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Tests
62
+ // ---------------------------------------------------------------------------
63
+
64
+ describe("syncIssue", () => {
65
+ it("returns empty patch and makes no API calls when issue is already in sync", async () => {
66
+ const issue = makeIssue({
67
+ number: 42,
68
+ labels: labelNames("bounty: 💰 $500", "bounty", "bounty: claimed"),
69
+ assignees: [{ login: "alice" }],
70
+ });
71
+ const api = makeMockApi(issue);
72
+ const patch = await syncIssue({ issueNumber: 42, api });
73
+
74
+ assert.deepEqual(patch.ops, []);
75
+ assert.deepEqual(api.added, []);
76
+ assert.deepEqual(api.removed, []);
77
+ });
78
+
79
+ it("adds generic bounty label when value label is present but generic is missing", async () => {
80
+ const issue = makeIssue({
81
+ number: 42,
82
+ labels: labelNames("bounty: 💰 $300"),
83
+ });
84
+ const api = makeMockApi(issue);
85
+ const patch = await syncIssue({ issueNumber: 42, api });
86
+
87
+ assert.equal(patch.ops.length, 1);
88
+ assert.deepEqual(api.added, [["bounty"]]);
89
+ assert.deepEqual(api.removed, []);
90
+ });
91
+
92
+ it("adds both generic and claimed when value label present and issue assigned", async () => {
93
+ const issue = makeIssue({
94
+ number: 42,
95
+ labels: labelNames("bounty: 💰 $800"),
96
+ assignees: [{ login: "bob" }],
97
+ });
98
+ const api = makeMockApi(issue);
99
+ await syncIssue({ issueNumber: 42, api });
100
+
101
+ assert.equal(api.added.length, 1);
102
+ assert.deepEqual(api.added[0]!.sort(), ["bounty", "bounty: claimed"].sort());
103
+ });
104
+
105
+ it("removes claimed when last assignee is removed", async () => {
106
+ const issue = makeIssue({
107
+ number: 42,
108
+ labels: labelNames("bounty: 💰 $500", "bounty", "bounty: claimed"),
109
+ assignees: [],
110
+ });
111
+ const api = makeMockApi(issue);
112
+ await syncIssue({ issueNumber: 42, api });
113
+
114
+ assert.deepEqual(api.removed, ["bounty: claimed"]);
115
+ assert.deepEqual(api.added, []);
116
+ });
117
+
118
+ it("removes both generic and claimed when value label is removed", async () => {
119
+ const issue = makeIssue({
120
+ number: 42,
121
+ labels: labelNames("bounty", "bounty: claimed"),
122
+ assignees: [{ login: "alice" }],
123
+ });
124
+ const api = makeMockApi(issue);
125
+ await syncIssue({ issueNumber: 42, api });
126
+
127
+ assert.deepEqual(api.removed.sort(), ["bounty", "bounty: claimed"].sort());
128
+ assert.deepEqual(api.added, []);
129
+ });
130
+
131
+ it("does a single batched addLabels call for multiple adds", async () => {
132
+ const issue = makeIssue({
133
+ number: 42,
134
+ labels: labelNames("bounty: 💰 $100"),
135
+ assignees: [{ login: "carol" }],
136
+ });
137
+ const api = makeMockApi(issue);
138
+ await syncIssue({ issueNumber: 42, api });
139
+
140
+ // One call, two labels
141
+ assert.equal(api.added.length, 1);
142
+ assert.equal(api.added[0]!.length, 2);
143
+ });
144
+ });
.github/scripts/bounty/tests/sync-pr.test.ts ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { syncPr } from "../src/sync-pr.js";
4
+ import type { GitHubApi } from "../src/api.js";
5
+ import type { Issue, PullRequest } from "../src/types.js";
6
+ import { BOUNTY_CLAIMED, BOUNTY_REWARDED } from "../src/rules.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Mock API
10
+ // ---------------------------------------------------------------------------
11
+
12
+ interface MockApi extends GitHubApi {
13
+ addedByTarget: Map<number, string[][]>;
14
+ removedByTarget: Map<number, string[]>;
15
+ commentsByTarget: Map<number, string[]>;
16
+ }
17
+
18
+ function makeMockApi(pr: PullRequest, issues: Issue[]): MockApi {
19
+ const addedByTarget = new Map<number, string[][]>();
20
+ const removedByTarget = new Map<number, string[]>();
21
+ const commentsByTarget = new Map<number, string[]>();
22
+
23
+ function recordAdd(target: number, labels: string[]) {
24
+ if (!addedByTarget.has(target)) addedByTarget.set(target, []);
25
+ addedByTarget.get(target)!.push(labels);
26
+ }
27
+ function recordRemove(target: number, label: string) {
28
+ if (!removedByTarget.has(target)) removedByTarget.set(target, []);
29
+ removedByTarget.get(target)!.push(label);
30
+ }
31
+ function recordComment(target: number, body: string) {
32
+ if (!commentsByTarget.has(target)) commentsByTarget.set(target, []);
33
+ commentsByTarget.get(target)!.push(body);
34
+ }
35
+
36
+ return {
37
+ addedByTarget,
38
+ removedByTarget,
39
+ commentsByTarget,
40
+ async getIssue(number) {
41
+ const found = issues.find((i) => i.number === number);
42
+ if (!found) throw new Error(`Issue #${number} not found in mock`);
43
+ return found;
44
+ },
45
+ async getPullRequest() {
46
+ return pr;
47
+ },
48
+ async listIssuesWithLabelPrefix(): Promise<Issue[]> {
49
+ throw new Error("not used");
50
+ },
51
+ async addLabels(target, labels) {
52
+ recordAdd(target, labels);
53
+ },
54
+ async removeLabel(target, label) {
55
+ recordRemove(target, label);
56
+ },
57
+ async addComment(target, body) {
58
+ recordComment(target, body);
59
+ },
60
+ };
61
+ }
62
+
63
+ function makeIssue(overrides: Partial<Issue> & { number: number }): Issue {
64
+ return { title: "issue", html_url: `https://github.com/owner/repo/issues/${overrides.number}`, state: "open", labels: [], assignees: [], ...overrides };
65
+ }
66
+
67
+ function makePr(overrides: Partial<PullRequest> & { number: number }): PullRequest {
68
+ return {
69
+ title: "Test PR",
70
+ state: "open",
71
+ merged: false,
72
+ body: null,
73
+ labels: [],
74
+ user: { login: "dev" },
75
+ html_url: `https://github.com/owner/repo/pull/${overrides.number}`,
76
+ ...overrides,
77
+ };
78
+ }
79
+
80
+ function labelNames(...names: string[]): { name: string }[] {
81
+ return names.map((name) => ({ name }));
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Tests
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe("syncPr", () => {
89
+ describe("label propagation", () => {
90
+ it("returns empty patch when PR has no linked issues", async () => {
91
+ const pr = makePr({ number: 10, body: "No links" });
92
+ const api = makeMockApi(pr, []);
93
+ const patch = await syncPr({ prNumber: 10, api });
94
+ assert.deepEqual(patch.ops, []);
95
+ });
96
+
97
+ it("returns empty patch when linked issue has no bounty label", async () => {
98
+ const pr = makePr({ number: 10, body: "Closes #1" });
99
+ const issue = makeIssue({ number: 1, labels: labelNames("type: bug") });
100
+ const api = makeMockApi(pr, [issue]);
101
+ const patch = await syncPr({ prNumber: 10, api });
102
+ assert.deepEqual(patch.ops, []);
103
+ });
104
+
105
+ it("copies bounty label from linked issue to PR", async () => {
106
+ const pr = makePr({ number: 10, body: "Closes #1" });
107
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
108
+ const api = makeMockApi(pr, [issue]);
109
+ await syncPr({ prNumber: 10, api });
110
+
111
+ assert.ok(api.addedByTarget.get(10)?.[0]?.includes("bounty: 💰 $500"));
112
+ });
113
+
114
+ it("skips label already on the PR", async () => {
115
+ const pr = makePr({
116
+ number: 10,
117
+ body: "Closes #1",
118
+ labels: labelNames("bounty: 💰 $500"),
119
+ });
120
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
121
+ const api = makeMockApi(pr, [issue]);
122
+ const patch = await syncPr({ prNumber: 10, api });
123
+ assert.deepEqual(patch.ops, []);
124
+ });
125
+
126
+ it("posts comment on linked issue when label is propagated", async () => {
127
+ const pr = makePr({ number: 10, body: "Closes #1" });
128
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $200") });
129
+ const api = makeMockApi(pr, [issue]);
130
+ await syncPr({ prNumber: 10, api });
131
+
132
+ const comments = api.commentsByTarget.get(1);
133
+ assert.ok(comments && comments.length === 1);
134
+ assert.ok(comments[0]!.includes("#10"));
135
+ assert.ok(comments[0]!.includes("dev"));
136
+ });
137
+
138
+ it("does not post comment when label was already on the PR", async () => {
139
+ const pr = makePr({
140
+ number: 10,
141
+ body: "Closes #1",
142
+ labels: labelNames("bounty: 💰 $200"),
143
+ });
144
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $200") });
145
+ const api = makeMockApi(pr, [issue]);
146
+ await syncPr({ prNumber: 10, api });
147
+
148
+ assert.ok(!api.commentsByTarget.has(1));
149
+ });
150
+
151
+ it("fetches multiple linked issues in parallel and merges labels", async () => {
152
+ const pr = makePr({ number: 10, body: "Closes #1\nCloses #2" });
153
+ const issue1 = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $100") });
154
+ const issue2 = makeIssue({ number: 2, labels: labelNames("bounty: 💰 $200") });
155
+ const api = makeMockApi(pr, [issue1, issue2]);
156
+ await syncPr({ prNumber: 10, api });
157
+
158
+ const added = api.addedByTarget.get(10)?.[0] ?? [];
159
+ assert.ok(added.includes("bounty: 💰 $100"));
160
+ assert.ok(added.includes("bounty: 💰 $200"));
161
+ });
162
+
163
+ it("skips a linked issue that fails to fetch without aborting", async () => {
164
+ const pr = makePr({ number: 10, body: "Closes #1\nCloses #999" });
165
+ const issue1 = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $300") });
166
+ // issue #999 will throw (not in mock store)
167
+ const api = makeMockApi(pr, [issue1]);
168
+ const patch = await syncPr({ prNumber: 10, api });
169
+
170
+ // Issue #1 label still propagated
171
+ assert.ok(api.addedByTarget.get(10)?.[0]?.includes("bounty: 💰 $300"));
172
+ });
173
+ });
174
+
175
+ describe("rewarded on merge", () => {
176
+ it("adds rewarded to PR and linked issue on merge", async () => {
177
+ const pr = makePr({
178
+ number: 10,
179
+ merged: true,
180
+ body: "Closes #1",
181
+ labels: labelNames("bounty: 💰 $500"),
182
+ });
183
+ const issue = makeIssue({
184
+ number: 1,
185
+ labels: labelNames("bounty: 💰 $500", BOUNTY_CLAIMED),
186
+ });
187
+ const api = makeMockApi(pr, [issue]);
188
+ await syncPr({ prNumber: 10, api });
189
+
190
+ assert.ok(api.addedByTarget.get(10)?.[0]?.includes(BOUNTY_REWARDED));
191
+ assert.ok(api.addedByTarget.get(1)?.[0]?.includes(BOUNTY_REWARDED));
192
+ assert.ok(api.removedByTarget.get(1)?.includes(BOUNTY_CLAIMED));
193
+ });
194
+
195
+ it("does not add rewarded again when already present on PR and issue", async () => {
196
+ const pr = makePr({
197
+ number: 10,
198
+ merged: true,
199
+ body: "Closes #1",
200
+ labels: labelNames("bounty: 💰 $500", BOUNTY_REWARDED),
201
+ });
202
+ const issue = makeIssue({
203
+ number: 1,
204
+ labels: labelNames("bounty: 💰 $500", BOUNTY_REWARDED),
205
+ });
206
+ const api = makeMockApi(pr, [issue]);
207
+ const patch = await syncPr({ prNumber: 10, api });
208
+ assert.deepEqual(patch.ops, []);
209
+ });
210
+
211
+ it("does not add rewarded when PR was closed but not merged", async () => {
212
+ const pr = makePr({
213
+ number: 10,
214
+ state: "closed",
215
+ merged: false,
216
+ body: "Closes #1",
217
+ labels: labelNames("bounty: 💰 $500"),
218
+ });
219
+ const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
220
+ const api = makeMockApi(pr, [issue]);
221
+ await syncPr({ prNumber: 10, api });
222
+
223
+ assert.ok(!api.addedByTarget.get(10)?.[0]?.includes(BOUNTY_REWARDED));
224
+ });
225
+
226
+ it("handles merged PR with no linked issues gracefully", async () => {
227
+ const pr = makePr({
228
+ number: 10,
229
+ merged: true,
230
+ body: "No issue links",
231
+ labels: labelNames("bounty: 💰 $500"),
232
+ });
233
+ const api = makeMockApi(pr, []);
234
+ const patch = await syncPr({ prNumber: 10, api });
235
+
236
+ // Only the PR op
237
+ assert.ok(api.addedByTarget.get(10)?.[0]?.includes(BOUNTY_REWARDED));
238
+ assert.equal(patch.ops.length, 1);
239
+ });
240
+ });
241
+ });
.github/workflows/autofix.yml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------------
2
+ # ------------------------------- WARNING ---------------------------
3
+ # -------------------------------------------------------------------
4
+ #
5
+ # This file was automatically generated by gh-workflows using the
6
+ # gh-workflow-gen bin. You should add and commit this file to your
7
+ # git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
8
+ # will be lost if the file is regenerated.
9
+ #
10
+ # To make modifications, update your `build.rs` configuration to adjust
11
+ # the workflow description as needed, then regenerate this file to apply
12
+ # those changes.
13
+ #
14
+ # -------------------------------------------------------------------
15
+ # ----------------------------- END WARNING -------------------------
16
+ # -------------------------------------------------------------------
17
+
18
+ name: autofix.ci
19
+ env:
20
+ RUSTFLAGS: '-Dwarnings'
21
+ 'on':
22
+ pull_request:
23
+ types:
24
+ - opened
25
+ - synchronize
26
+ - reopened
27
+ branches:
28
+ - main
29
+ push:
30
+ branches:
31
+ - main
32
+ jobs:
33
+ lint:
34
+ name: Lint Fix
35
+ runs-on: ubuntu-latest
36
+ permissions:
37
+ contents: read
38
+ steps:
39
+ - name: Checkout Code
40
+ uses: actions/checkout@v6
41
+ - name: Install SQLite
42
+ run: sudo apt-get install -y libsqlite3-dev
43
+ - name: Setup Protobuf Compiler
44
+ uses: arduino/setup-protoc@v3
45
+ with:
46
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
47
+ - name: Setup Rust Toolchain
48
+ uses: actions-rust-lang/setup-rust-toolchain@v1
49
+ with:
50
+ toolchain: nightly
51
+ components: clippy, rustfmt
52
+ - name: Cargo Fmt
53
+ run: cargo +nightly fmt --all
54
+ - name: Cargo Clippy
55
+ run: cargo +nightly clippy --all-features --workspace --all-targets --fix --allow-dirty -- -D warnings
56
+ - name: Cargo Clippy String Safety
57
+ run: cargo +nightly clippy --all-features --workspace -- -D clippy::string_slice -D clippy::indexing_slicing -D clippy::disallowed_methods
58
+ - name: Autofix
59
+ uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8
60
+ concurrency:
61
+ group: autofix-${{github.ref}}
62
+ cancel-in-progress: false
.github/workflows/bounty.yml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------------
2
+ # ------------------------------- WARNING ---------------------------
3
+ # -------------------------------------------------------------------
4
+ #
5
+ # This file was automatically generated by gh-workflows using the
6
+ # gh-workflow-gen bin. You should add and commit this file to your
7
+ # git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
8
+ # will be lost if the file is regenerated.
9
+ #
10
+ # To make modifications, update your `build.rs` configuration to adjust
11
+ # the workflow description as needed, then regenerate this file to apply
12
+ # those changes.
13
+ #
14
+ # -------------------------------------------------------------------
15
+ # ----------------------------- END WARNING -------------------------
16
+ # -------------------------------------------------------------------
17
+
18
+ name: Bounty Management
19
+ 'on':
20
+ issues:
21
+ types:
22
+ - assigned
23
+ - unassigned
24
+ - labeled
25
+ - unlabeled
26
+ pull_request:
27
+ types:
28
+ - opened
29
+ - edited
30
+ - reopened
31
+ pull_request_target:
32
+ types:
33
+ - closed
34
+ schedule:
35
+ - cron: '0 2 * * *'
36
+ permissions:
37
+ issues: write
38
+ pull-requests: write
39
+ jobs:
40
+ sync-all-issues:
41
+ name: Sync all bounty issues
42
+ runs-on: ubuntu-latest
43
+ permissions:
44
+ issues: write
45
+ steps:
46
+ - name: Checkout
47
+ uses: actions/checkout@v6
48
+ - name: Install npm packages
49
+ run: npm install
50
+ - name: Sync all bounty labels
51
+ run: npx tsx .github/scripts/bounty/src/sync-all-issues.ts --repo ${{ github.repository }} --token ${{ secrets.GITHUB_TOKEN }} --execute
52
+ sync-pr:
53
+ name: Sync PR bounty labels
54
+ runs-on: ubuntu-latest
55
+ permissions:
56
+ issues: write
57
+ pull-requests: write
58
+ steps:
59
+ - name: Checkout
60
+ uses: actions/checkout@v6
61
+ - name: Install npm packages
62
+ run: npm install
63
+ - name: Sync bounty labels
64
+ run: npx tsx .github/scripts/bounty/src/sync-pr.ts --pr ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --token ${{ secrets.GITHUB_TOKEN }}
.github/workflows/ci.yml ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------------
2
+ # ------------------------------- WARNING ---------------------------
3
+ # -------------------------------------------------------------------
4
+ #
5
+ # This file was automatically generated by gh-workflows using the
6
+ # gh-workflow-gen bin. You should add and commit this file to your
7
+ # git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
8
+ # will be lost if the file is regenerated.
9
+ #
10
+ # To make modifications, update your `build.rs` configuration to adjust
11
+ # the workflow description as needed, then regenerate this file to apply
12
+ # those changes.
13
+ #
14
+ # -------------------------------------------------------------------
15
+ # ----------------------------- END WARNING -------------------------
16
+ # -------------------------------------------------------------------
17
+
18
+ name: ci
19
+ env:
20
+ RUSTFLAGS: '-Dwarnings'
21
+ OPENROUTER_API_KEY: ${{secrets.OPENROUTER_API_KEY}}
22
+ 'on':
23
+ pull_request:
24
+ types:
25
+ - opened
26
+ - synchronize
27
+ - reopened
28
+ - labeled
29
+ branches:
30
+ - main
31
+ push:
32
+ branches:
33
+ - main
34
+ tags:
35
+ - v*
36
+ jobs:
37
+ build:
38
+ name: Build and Test
39
+ runs-on: ubuntu-latest
40
+ permissions:
41
+ contents: read
42
+ steps:
43
+ - name: Checkout Code
44
+ uses: actions/checkout@v6
45
+ - name: Setup Protobuf Compiler
46
+ uses: arduino/setup-protoc@v3
47
+ with:
48
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
49
+ - name: Setup Rust Toolchain
50
+ uses: actions-rust-lang/setup-rust-toolchain@v1
51
+ with:
52
+ toolchain: stable
53
+ - name: Install cargo-llvm-cov
54
+ run: cargo install cargo-llvm-cov
55
+ - name: Generate coverage
56
+ run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
57
+ zsh_rprompt_perf:
58
+ name: 'Performance: zsh rprompt'
59
+ runs-on: ubuntu-latest
60
+ permissions:
61
+ contents: read
62
+ steps:
63
+ - name: Checkout Code
64
+ uses: actions/checkout@v6
65
+ - name: Setup Protobuf Compiler
66
+ uses: arduino/setup-protoc@v3
67
+ with:
68
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
69
+ - name: Setup Rust Toolchain
70
+ uses: actions-rust-lang/setup-rust-toolchain@v1
71
+ with:
72
+ toolchain: stable
73
+ - name: Run performance benchmark
74
+ run: './scripts/benchmark.sh --threshold 60 zsh rprompt'
75
+ draft_release:
76
+ needs:
77
+ - build
78
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
79
+ name: Draft Release
80
+ runs-on: ubuntu-latest
81
+ permissions:
82
+ contents: write
83
+ pull-requests: write
84
+ outputs:
85
+ crate_release_name: ${{ steps.set_output.outputs.crate_release_name }}
86
+ crate_release_id: ${{ steps.set_output.outputs.crate_release_id }}
87
+ steps:
88
+ - name: Checkout Code
89
+ uses: actions/checkout@v6
90
+ - id: create_release
91
+ name: Draft Release
92
+ uses: release-drafter/release-drafter@v7
93
+ with:
94
+ config-name: release-drafter.yml
95
+ env:
96
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
97
+ - id: set_output
98
+ name: Export Outputs
99
+ run: echo "crate_release_id=${{ steps.create_release.outputs.id }}" >> $GITHUB_OUTPUT && echo "crate_release_name=${{ steps.create_release.outputs.tag_name }}" >> $GITHUB_OUTPUT
100
+ draft_release_pr:
101
+ if: 'github.event_name == ''pull_request'' && contains(github.event.pull_request.labels.*.name, ''ci: build all targets'')'
102
+ name: Draft Release for PR
103
+ runs-on: ubuntu-latest
104
+ outputs:
105
+ crate_release_name: ${{ steps.set_output.outputs.crate_release_name }}
106
+ crate_release_id: ${{ steps.set_output.outputs.crate_release_id }}
107
+ steps:
108
+ - name: Checkout Code
109
+ uses: actions/checkout@v6
110
+ - id: set_output
111
+ name: Set Release Version
112
+ run: echo "crate_release_name=pr-build-${{ github.event.number }}" >> $GITHUB_OUTPUT && echo "crate_release_id=pr-build-${{ github.event.number }}" >> $GITHUB_OUTPUT
113
+ build_release:
114
+ needs:
115
+ - draft_release
116
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
117
+ name: build-release
118
+ runs-on: ${{ matrix.os }}
119
+ permissions:
120
+ contents: write
121
+ pull-requests: write
122
+ strategy:
123
+ matrix:
124
+ include:
125
+ - binary_name: forge-x86_64-unknown-linux-musl
126
+ binary_path: target/x86_64-unknown-linux-musl/release/forge
127
+ cross: 'true'
128
+ os: ubuntu-latest
129
+ target: x86_64-unknown-linux-musl
130
+ - binary_name: forge-aarch64-unknown-linux-musl
131
+ binary_path: target/aarch64-unknown-linux-musl/release/forge
132
+ cross: 'true'
133
+ os: ubuntu-latest
134
+ target: aarch64-unknown-linux-musl
135
+ - binary_name: forge-x86_64-unknown-linux-gnu
136
+ binary_path: target/x86_64-unknown-linux-gnu/release/forge
137
+ cross: 'false'
138
+ os: ubuntu-latest
139
+ target: x86_64-unknown-linux-gnu
140
+ - binary_name: forge-aarch64-unknown-linux-gnu
141
+ binary_path: target/aarch64-unknown-linux-gnu/release/forge
142
+ cross: 'true'
143
+ os: ubuntu-latest
144
+ target: aarch64-unknown-linux-gnu
145
+ - binary_name: forge-x86_64-apple-darwin
146
+ binary_path: target/x86_64-apple-darwin/release/forge
147
+ cross: 'false'
148
+ os: macos-latest
149
+ target: x86_64-apple-darwin
150
+ - binary_name: forge-aarch64-apple-darwin
151
+ binary_path: target/aarch64-apple-darwin/release/forge
152
+ cross: 'false'
153
+ os: macos-latest
154
+ target: aarch64-apple-darwin
155
+ - binary_name: forge-x86_64-pc-windows-msvc.exe
156
+ binary_path: target/x86_64-pc-windows-msvc/release/forge.exe
157
+ cross: 'false'
158
+ os: windows-latest
159
+ target: x86_64-pc-windows-msvc
160
+ - binary_name: forge-aarch64-pc-windows-msvc.exe
161
+ binary_path: target/aarch64-pc-windows-msvc/release/forge.exe
162
+ cross: 'false'
163
+ os: windows-latest
164
+ target: aarch64-pc-windows-msvc
165
+ - binary_name: forge-aarch64-linux-android
166
+ binary_path: target/aarch64-linux-android/release/forge
167
+ cross: 'true'
168
+ os: ubuntu-latest
169
+ target: aarch64-linux-android
170
+ steps:
171
+ - name: Checkout Code
172
+ uses: actions/checkout@v6
173
+ - name: Setup Protobuf Compiler
174
+ if: ${{ matrix.cross == 'false' }}
175
+ uses: arduino/setup-protoc@v3
176
+ with:
177
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
178
+ - name: Setup Cross Toolchain
179
+ if: ${{ matrix.cross == 'false' }}
180
+ uses: taiki-e/setup-cross-toolchain-action@v1
181
+ with:
182
+ target: ${{ matrix.target }}
183
+ - name: Add Rust target
184
+ if: ${{ matrix.cross == 'false' }}
185
+ run: rustup target add ${{ matrix.target }}
186
+ - name: Set Rust Flags
187
+ if: '!(contains(matrix.target, ''-unknown-linux-'') || contains(matrix.target, ''-android''))'
188
+ run: echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
189
+ - name: Build Binary
190
+ uses: ClementTsang/cargo-action@v0.0.7
191
+ with:
192
+ command: build --release
193
+ args: '--target ${{ matrix.target }}'
194
+ use-cross: ${{ matrix.cross }}
195
+ cross-version: '0.2.5'
196
+ env:
197
+ RUSTFLAGS: ${{ env.RUSTFLAGS }}
198
+ POSTHOG_API_SECRET: ${{secrets.POSTHOG_API_SECRET}}
199
+ APP_VERSION: ${{ needs.draft_release.outputs.crate_release_name }}
200
+ - name: Copy Binary
201
+ run: cp ${{ matrix.binary_path }} ${{ matrix.binary_name }}
202
+ - name: Upload to Release
203
+ uses: xresloader/upload-to-github-release@v1
204
+ with:
205
+ release_id: ${{ needs.draft_release.outputs.crate_release_id }}
206
+ file: ${{ matrix.binary_name }}
207
+ overwrite: 'true'
208
+ build_release_pr:
209
+ needs:
210
+ - draft_release_pr
211
+ if: 'github.event_name == ''pull_request'' && contains(github.event.pull_request.labels.*.name, ''ci: build all targets'')'
212
+ name: build-release
213
+ runs-on: ${{ matrix.os }}
214
+ permissions:
215
+ contents: write
216
+ pull-requests: write
217
+ strategy:
218
+ matrix:
219
+ include:
220
+ - binary_name: forge-x86_64-unknown-linux-musl
221
+ binary_path: target/x86_64-unknown-linux-musl/release/forge
222
+ cross: 'true'
223
+ os: ubuntu-latest
224
+ target: x86_64-unknown-linux-musl
225
+ - binary_name: forge-aarch64-unknown-linux-musl
226
+ binary_path: target/aarch64-unknown-linux-musl/release/forge
227
+ cross: 'true'
228
+ os: ubuntu-latest
229
+ target: aarch64-unknown-linux-musl
230
+ - binary_name: forge-x86_64-unknown-linux-gnu
231
+ binary_path: target/x86_64-unknown-linux-gnu/release/forge
232
+ cross: 'false'
233
+ os: ubuntu-latest
234
+ target: x86_64-unknown-linux-gnu
235
+ - binary_name: forge-aarch64-unknown-linux-gnu
236
+ binary_path: target/aarch64-unknown-linux-gnu/release/forge
237
+ cross: 'true'
238
+ os: ubuntu-latest
239
+ target: aarch64-unknown-linux-gnu
240
+ - binary_name: forge-x86_64-apple-darwin
241
+ binary_path: target/x86_64-apple-darwin/release/forge
242
+ cross: 'false'
243
+ os: macos-latest
244
+ target: x86_64-apple-darwin
245
+ - binary_name: forge-aarch64-apple-darwin
246
+ binary_path: target/aarch64-apple-darwin/release/forge
247
+ cross: 'false'
248
+ os: macos-latest
249
+ target: aarch64-apple-darwin
250
+ - binary_name: forge-x86_64-pc-windows-msvc.exe
251
+ binary_path: target/x86_64-pc-windows-msvc/release/forge.exe
252
+ cross: 'false'
253
+ os: windows-latest
254
+ target: x86_64-pc-windows-msvc
255
+ - binary_name: forge-aarch64-pc-windows-msvc.exe
256
+ binary_path: target/aarch64-pc-windows-msvc/release/forge.exe
257
+ cross: 'false'
258
+ os: windows-latest
259
+ target: aarch64-pc-windows-msvc
260
+ - binary_name: forge-aarch64-linux-android
261
+ binary_path: target/aarch64-linux-android/release/forge
262
+ cross: 'true'
263
+ os: ubuntu-latest
264
+ target: aarch64-linux-android
265
+ steps:
266
+ - name: Checkout Code
267
+ uses: actions/checkout@v6
268
+ - name: Setup Protobuf Compiler
269
+ if: ${{ matrix.cross == 'false' }}
270
+ uses: arduino/setup-protoc@v3
271
+ with:
272
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
273
+ - name: Setup Cross Toolchain
274
+ if: ${{ matrix.cross == 'false' }}
275
+ uses: taiki-e/setup-cross-toolchain-action@v1
276
+ with:
277
+ target: ${{ matrix.target }}
278
+ - name: Add Rust target
279
+ if: ${{ matrix.cross == 'false' }}
280
+ run: rustup target add ${{ matrix.target }}
281
+ - name: Set Rust Flags
282
+ if: '!(contains(matrix.target, ''-unknown-linux-'') || contains(matrix.target, ''-android''))'
283
+ run: echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
284
+ - name: Build Binary
285
+ uses: ClementTsang/cargo-action@v0.0.7
286
+ with:
287
+ command: build --release
288
+ args: '--target ${{ matrix.target }}'
289
+ use-cross: ${{ matrix.cross }}
290
+ cross-version: '0.2.5'
291
+ env:
292
+ RUSTFLAGS: ${{ env.RUSTFLAGS }}
293
+ POSTHOG_API_SECRET: ${{secrets.POSTHOG_API_SECRET}}
294
+ APP_VERSION: ${{ needs.draft_release_pr.outputs.crate_release_name }}
295
+ concurrency:
296
+ group: ${{ github.workflow }}-${{ github.ref }}
.github/workflows/labels.yml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------------
2
+ # ------------------------------- WARNING ---------------------------
3
+ # -------------------------------------------------------------------
4
+ #
5
+ # This file was automatically generated by gh-workflows using the
6
+ # gh-workflow-gen bin. You should add and commit this file to your
7
+ # git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
8
+ # will be lost if the file is regenerated.
9
+ #
10
+ # To make modifications, update your `build.rs` configuration to adjust
11
+ # the workflow description as needed, then regenerate this file to apply
12
+ # those changes.
13
+ #
14
+ # -------------------------------------------------------------------
15
+ # ----------------------------- END WARNING -------------------------
16
+ # -------------------------------------------------------------------
17
+
18
+ name: Github Label Sync
19
+ 'on':
20
+ push:
21
+ branches:
22
+ - main
23
+ permissions:
24
+ contents: write
25
+ jobs:
26
+ label-sync:
27
+ name: label-sync
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ issues: write
31
+ steps:
32
+ - name: Checkout
33
+ uses: actions/checkout@v6
34
+ - name: Sync labels
35
+ run: |-
36
+ npx github-label-sync \
37
+ --access-token ${{ secrets.GITHUB_TOKEN }} \
38
+ --labels ".github/labels.json" \
39
+ ${{ github.repository }}
.github/workflows/release-drafter.yml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------------
2
+ # ------------------------------- WARNING ---------------------------
3
+ # -------------------------------------------------------------------
4
+ #
5
+ # This file was automatically generated by gh-workflows using the
6
+ # gh-workflow-gen bin. You should add and commit this file to your
7
+ # git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
8
+ # will be lost if the file is regenerated.
9
+ #
10
+ # To make modifications, update your `build.rs` configuration to adjust
11
+ # the workflow description as needed, then regenerate this file to apply
12
+ # those changes.
13
+ #
14
+ # -------------------------------------------------------------------
15
+ # ----------------------------- END WARNING -------------------------
16
+ # -------------------------------------------------------------------
17
+
18
+ name: Release Drafter
19
+ 'on':
20
+ pull_request_target:
21
+ types:
22
+ - opened
23
+ - reopened
24
+ - synchronize
25
+ - labeled
26
+ - unlabeled
27
+ - closed
28
+ branches:
29
+ - main
30
+ push:
31
+ branches:
32
+ - main
33
+ permissions:
34
+ contents: write
35
+ pull-requests: write
36
+ jobs:
37
+ update_release_draft:
38
+ name: update_release_draft
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - name: Auto Labeler
42
+ if: github.event_name == 'pull_request_target'
43
+ uses: release-drafter/release-drafter/autolabeler@v7
44
+ with:
45
+ config-name: release-drafter.yml
46
+ env:
47
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48
+ - name: Release Drafter
49
+ uses: release-drafter/release-drafter@v7
50
+ with:
51
+ config-name: release-drafter.yml
52
+ env:
53
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
.github/workflows/release.yml ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------------
2
+ # ------------------------------- WARNING ---------------------------
3
+ # -------------------------------------------------------------------
4
+ #
5
+ # This file was automatically generated by gh-workflows using the
6
+ # gh-workflow-gen bin. You should add and commit this file to your
7
+ # git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
8
+ # will be lost if the file is regenerated.
9
+ #
10
+ # To make modifications, update your `build.rs` configuration to adjust
11
+ # the workflow description as needed, then regenerate this file to apply
12
+ # those changes.
13
+ #
14
+ # -------------------------------------------------------------------
15
+ # ----------------------------- END WARNING -------------------------
16
+ # -------------------------------------------------------------------
17
+
18
+ name: Multi Channel Release
19
+ 'on':
20
+ release:
21
+ types:
22
+ - published
23
+ permissions:
24
+ contents: write
25
+ pull-requests: write
26
+ jobs:
27
+ build_release:
28
+ name: build-release
29
+ runs-on: ${{ matrix.os }}
30
+ permissions:
31
+ contents: write
32
+ pull-requests: write
33
+ strategy:
34
+ matrix:
35
+ include:
36
+ - binary_name: forge-x86_64-unknown-linux-musl
37
+ binary_path: target/x86_64-unknown-linux-musl/release/forge
38
+ cross: 'true'
39
+ os: ubuntu-latest
40
+ target: x86_64-unknown-linux-musl
41
+ - binary_name: forge-aarch64-unknown-linux-musl
42
+ binary_path: target/aarch64-unknown-linux-musl/release/forge
43
+ cross: 'true'
44
+ os: ubuntu-latest
45
+ target: aarch64-unknown-linux-musl
46
+ - binary_name: forge-x86_64-unknown-linux-gnu
47
+ binary_path: target/x86_64-unknown-linux-gnu/release/forge
48
+ cross: 'false'
49
+ os: ubuntu-latest
50
+ target: x86_64-unknown-linux-gnu
51
+ - binary_name: forge-aarch64-unknown-linux-gnu
52
+ binary_path: target/aarch64-unknown-linux-gnu/release/forge
53
+ cross: 'true'
54
+ os: ubuntu-latest
55
+ target: aarch64-unknown-linux-gnu
56
+ - binary_name: forge-x86_64-apple-darwin
57
+ binary_path: target/x86_64-apple-darwin/release/forge
58
+ cross: 'false'
59
+ os: macos-latest
60
+ target: x86_64-apple-darwin
61
+ - binary_name: forge-aarch64-apple-darwin
62
+ binary_path: target/aarch64-apple-darwin/release/forge
63
+ cross: 'false'
64
+ os: macos-latest
65
+ target: aarch64-apple-darwin
66
+ - binary_name: forge-x86_64-pc-windows-msvc.exe
67
+ binary_path: target/x86_64-pc-windows-msvc/release/forge.exe
68
+ cross: 'false'
69
+ os: windows-latest
70
+ target: x86_64-pc-windows-msvc
71
+ - binary_name: forge-aarch64-pc-windows-msvc.exe
72
+ binary_path: target/aarch64-pc-windows-msvc/release/forge.exe
73
+ cross: 'false'
74
+ os: windows-latest
75
+ target: aarch64-pc-windows-msvc
76
+ - binary_name: forge-aarch64-linux-android
77
+ binary_path: target/aarch64-linux-android/release/forge
78
+ cross: 'true'
79
+ os: ubuntu-latest
80
+ target: aarch64-linux-android
81
+ steps:
82
+ - name: Checkout Code
83
+ uses: actions/checkout@v6
84
+ - name: Setup Protobuf Compiler
85
+ if: ${{ matrix.cross == 'false' }}
86
+ uses: arduino/setup-protoc@v3
87
+ with:
88
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
89
+ - name: Setup Cross Toolchain
90
+ if: ${{ matrix.cross == 'false' }}
91
+ uses: taiki-e/setup-cross-toolchain-action@v1
92
+ with:
93
+ target: ${{ matrix.target }}
94
+ - name: Add Rust target
95
+ if: ${{ matrix.cross == 'false' }}
96
+ run: rustup target add ${{ matrix.target }}
97
+ - name: Set Rust Flags
98
+ if: '!(contains(matrix.target, ''-unknown-linux-'') || contains(matrix.target, ''-android''))'
99
+ run: echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
100
+ - name: Build Binary
101
+ uses: ClementTsang/cargo-action@v0.0.7
102
+ with:
103
+ command: build --release
104
+ args: '--target ${{ matrix.target }}'
105
+ use-cross: ${{ matrix.cross }}
106
+ cross-version: '0.2.5'
107
+ env:
108
+ RUSTFLAGS: ${{ env.RUSTFLAGS }}
109
+ POSTHOG_API_SECRET: ${{secrets.POSTHOG_API_SECRET}}
110
+ APP_VERSION: ${{ github.event.release.tag_name }}
111
+ - name: Copy Binary
112
+ run: cp ${{ matrix.binary_path }} ${{ matrix.binary_name }}
113
+ - name: Upload to Release
114
+ uses: xresloader/upload-to-github-release@v1
115
+ with:
116
+ release_id: ${{ github.event.release.id }}
117
+ file: ${{ matrix.binary_name }}
118
+ overwrite: 'true'
119
+ npm_release:
120
+ needs:
121
+ - build_release
122
+ name: npm_release
123
+ runs-on: ubuntu-latest
124
+ strategy:
125
+ matrix:
126
+ repository:
127
+ - antinomyhq/npm-code-forge
128
+ - antinomyhq/npm-forgecode
129
+ steps:
130
+ - name: Checkout Code
131
+ uses: actions/checkout@v6
132
+ with:
133
+ repository: ${{ matrix.repository }}
134
+ ref: main
135
+ token: ${{ secrets.NPM_ACCESS }}
136
+ - name: Update NPM Package
137
+ run: './update-package.sh ${{ github.event.release.tag_name }}'
138
+ env:
139
+ AUTO_PUSH: 'true'
140
+ CI: 'true'
141
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
142
+ homebrew_release:
143
+ needs:
144
+ - build_release
145
+ name: homebrew_release
146
+ runs-on: ubuntu-latest
147
+ steps:
148
+ - name: Checkout Code
149
+ uses: actions/checkout@v6
150
+ with:
151
+ repository: antinomyhq/homebrew-code-forge
152
+ ref: main
153
+ token: ${{ secrets.HOMEBREW_ACCESS }}
154
+ - name: Update Homebrew Formula
155
+ run: GITHUB_TOKEN="${{ secrets.HOMEBREW_ACCESS }}" ./update-formula.sh ${{ github.event.release.tag_name }}
.github/workflows/stale.yml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------------
2
+ # ------------------------------- WARNING ---------------------------
3
+ # -------------------------------------------------------------------
4
+ #
5
+ # This file was automatically generated by gh-workflows using the
6
+ # gh-workflow-gen bin. You should add and commit this file to your
7
+ # git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
8
+ # will be lost if the file is regenerated.
9
+ #
10
+ # To make modifications, update your `build.rs` configuration to adjust
11
+ # the workflow description as needed, then regenerate this file to apply
12
+ # those changes.
13
+ #
14
+ # -------------------------------------------------------------------
15
+ # ----------------------------- END WARNING -------------------------
16
+ # -------------------------------------------------------------------
17
+
18
+ name: Close Stale Issues and PR
19
+ env:
20
+ DAYS_BEFORE_ISSUE_STALE: '30'
21
+ DAYS_BEFORE_ISSUE_CLOSE: '7'
22
+ DAYS_BEFORE_PR_STALE: '5'
23
+ DAYS_BEFORE_PR_CLOSE: '10'
24
+ 'on':
25
+ schedule:
26
+ - cron: '0 * * * *'
27
+ permissions:
28
+ issues: write
29
+ pull-requests: write
30
+ jobs:
31
+ stale:
32
+ name: Stale Issues
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - name: Mark Stale Issues
36
+ uses: actions/stale@v10
37
+ with:
38
+ stale-issue-label: 'state: inactive'
39
+ stale-pr-label: 'state: inactive'
40
+ stale-issue-message: |-
41
+ **Action required:** Issue inactive for ${{ env.DAYS_BEFORE_ISSUE_STALE }} days.
42
+ Status update or closure in ${{ env.DAYS_BEFORE_ISSUE_CLOSE }} days.
43
+ close-issue-message: Issue closed after ${{ env.DAYS_BEFORE_ISSUE_CLOSE }} days of inactivity.
44
+ stale-pr-message: |-
45
+ **Action required:** PR inactive for ${{ env.DAYS_BEFORE_PR_STALE }} days.
46
+ Status update or closure in ${{ env.DAYS_BEFORE_PR_CLOSE }} days.
47
+ close-pr-message: PR closed after ${{ env.DAYS_BEFORE_PR_CLOSE }} days of inactivity.
48
+ days-before-issue-stale: ${{ env.DAYS_BEFORE_ISSUE_STALE }}
49
+ days-before-issue-close: ${{ env.DAYS_BEFORE_ISSUE_CLOSE }}
50
+ days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }}
51
+ days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}
benchmarks/README.md ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Forge Code Evaluations
2
+
3
+ A flexible evaluation framework for running automated tests and benchmarks against Forge Code commands.
4
+
5
+ ## Quick Start
6
+
7
+ ### Setup
8
+
9
+ Before running evaluations, create a `forgee` symlink to the debug binary:
10
+
11
+ ```bash
12
+ # Create symlink in your PATH (e.g., ~/bin or /usr/local/bin)
13
+ ln -sf /path/to/code-forge/target/debug/forge ~/forgee
14
+
15
+ # Or if ~/bin is in your PATH
16
+ ln -sf $(pwd)/target/debug/forge ~/bin/forgee
17
+ ```
18
+
19
+ **Why is this needed?** Tasks execute in temporary directories, so relative paths like `../../target/debug/forge` won't work. The `forgee` symlink provides a stable absolute path that works from any directory.
20
+
21
+ ### Running Evaluations
22
+
23
+ ```bash
24
+ # Run an evaluation
25
+ npm run eval ./evals/create_skill/task.yml
26
+
27
+ # Set custom log level
28
+ LOG_LEVEL=debug npm run eval ./evals/create_skill/task.yml
29
+ ```
30
+
31
+ ## How It Works
32
+
33
+ The evaluation system executes commands based on task definitions and validates their output. It supports:
34
+
35
+ - **Parallel execution** with configurable concurrency
36
+ - **Timeout handling** for long-running tasks
37
+ - **Data-driven testing** using CSV files
38
+ - **Output validation** using regex patterns
39
+ - **Debug artifacts** stored with timestamps
40
+
41
+ ## Project Structure
42
+
43
+ ```
44
+ benchmarks/
45
+ ├── cli.ts # Main CLI entry point
46
+ ├── command-generator.ts # Command template rendering
47
+ ├── task-executor.ts # Task execution with timeout support
48
+ ├── model.ts # TypeScript types for tasks
49
+ ├── parse.ts # CLI argument parsing
50
+ └── evals/ # Evaluation definitions
51
+ └── create_skill/
52
+ ├── task.yml # Task definition
53
+ ├── create_skill_tasks.csv # Test data
54
+ └── debug/ # Debug outputs (timestamped)
55
+ ```
56
+
57
+ ## Creating an Evaluation
58
+
59
+ ### 1. Create an Evaluation Directory
60
+
61
+ ```bash
62
+ mkdir -p evals/my_eval
63
+ ```
64
+
65
+ ### 2. Create a `task.yml` File
66
+
67
+ The task file defines how to run your evaluation:
68
+
69
+ ```yaml
70
+ # Optional: Commands to run before evaluation starts
71
+ before_run:
72
+ - cargo build
73
+ - npm install
74
+
75
+ # Required: Command(s) to execute for each test case
76
+ # Single command
77
+ run: forgee -p '{{prompt}}'
78
+
79
+ # Or multiple commands (executed sequentially)
80
+ run:
81
+ - echo "Step 1: {{task}}"
82
+ - forgee -p '{{prompt}}'
83
+ - echo "Step 2: Complete"
84
+
85
+ # Execution configuration
86
+ parallelism: 10 # Number of tasks to run in parallel (default: 1)
87
+ timeout: 60 # Timeout in seconds (optional)
88
+ early_exit: true # Stop execution when validations pass (optional)
89
+
90
+ # Optional: Validations to run on output
91
+ validations:
92
+ - name: "Check success message"
93
+ type: regex
94
+ regex: \[[0-9:]*\] Skill create-skill
95
+
96
+ # Required: Data sources for test cases
97
+ sources:
98
+ - csv: my_tasks.csv
99
+ ```
100
+
101
+ #### Task File Schema
102
+
103
+ **`before_run`** (optional): Array of shell commands to execute before running tasks
104
+ - Runs sequentially before the main command execution
105
+ - Executes in a temporary directory created for the evaluation run
106
+ - Useful for building binaries or setting up dependencies
107
+
108
+ **`run`** (required): Command(s) to execute for each test case
109
+ - Can be a single string or an array of strings
110
+ - Commands support template placeholders (e.g., `{{variable}}`)
111
+ - Multiple commands are executed sequentially
112
+ - If any command fails, subsequent commands are skipped
113
+
114
+ **`parallelism`** (optional): Number of tasks to run concurrently (default: 1)
115
+
116
+ **`timeout`** (optional): Maximum execution time in seconds per task
117
+
118
+ **`early_exit`** (optional): Stop command execution when all validations pass
119
+
120
+ **`validations`** (optional): Array of validation rules
121
+ - `name`: Human-readable description
122
+ - `type`: Validation type. Supported values:
123
+ - `regex`: Match output against a regular expression pattern
124
+ - `shell`: Execute a shell command with output as stdin
125
+ - For `regex` type:
126
+ - `regex`: Regular expression pattern to match in output
127
+ - For `shell` type:
128
+ - `command`: Shell command to execute (receives task output via stdin)
129
+ - `exit_code`: Expected exit code (default: 0)
130
+
131
+ **`sources`** (required): Array of data sources
132
+ - Currently supports CSV files: `- csv: filename.csv`
133
+ - Future: Command output: `- cmd: command`
134
+
135
+ ### 3. Create Test Data (CSV)
136
+
137
+ Create a CSV file with columns matching your template variables:
138
+
139
+ ```csv
140
+ prompt,expected_output
141
+ "Create a backup script","backup.sh"
142
+ "Generate API client","api_client.py"
143
+ ```
144
+
145
+ The column names (e.g., `prompt`, `expected_output`) become template variables you can use in your command with `{{column_name}}`.
146
+
147
+ ### 4. Run the Evaluation
148
+
149
+ ```bash
150
+ npm run eval ./evals/my_eval/task.yml
151
+ ```
152
+
153
+ ## Template Variables
154
+
155
+ Commands support Handlebars template syntax. Variables are populated from CSV columns:
156
+
157
+ ```yaml
158
+ run:
159
+ command: ./tool --input '{{input_file}}' --format {{format}} --verbose
160
+ ```
161
+
162
+ With CSV:
163
+ ```csv
164
+ input_file,format
165
+ data1.txt,json
166
+ data2.txt,yaml
167
+ ```
168
+
169
+ ## Output and Debugging
170
+
171
+ ### Debug Artifacts
172
+
173
+ Each run creates a timestamped debug directory:
174
+
175
+ ```
176
+ evals/my_eval/debug/2025-11-23T10-30-45-123Z/
177
+ ├── task-1.log
178
+ ├── task-2.log
179
+ └── task-3.log
180
+ ```
181
+
182
+ Each log file contains the full output (stdout + stderr) from the command execution.
183
+
184
+ ### Task Status
185
+
186
+ Tasks can have four statuses:
187
+
188
+ - **`passed`**: Task completed successfully and passed all validations
189
+ - **`validation_failed`**: Task completed but failed one or more validations
190
+ - **`timeout`**: Task exceeded the timeout limit
191
+ - **`failed`**: Task execution failed (non-zero exit code)
192
+
193
+ ### Logging
194
+
195
+ **Human-readable output (default):**
196
+ ```bash
197
+ npm run eval ./evals/my_eval/task.yml
198
+ ```
199
+
200
+ **Machine-readable JSON output:**
201
+ ```bash
202
+ LOG_JSON=1 npm run eval ./evals/my_eval/task.yml | jq .
203
+ ```
204
+
205
+ **Debug logging:**
206
+ ```bash
207
+ LOG_LEVEL=debug npm run eval ./evals/my_eval/task.yml
208
+ ```
209
+
210
+ ## Examples
211
+
212
+ ### Example 1: Simple Sequential Execution
213
+
214
+ ```yaml
215
+ run: echo "Processing {{name}}"
216
+ sources:
217
+ - csv: names.csv
218
+ ```
219
+
220
+ ```csv
221
+ name
222
+ Alice
223
+ Bob
224
+ Charlie
225
+ ```
226
+
227
+ ### Example 2: Parallel Execution with Timeout
228
+
229
+ ```yaml
230
+ run: ./slow_task --id {{task_id}}
231
+ parallelism: 5
232
+ timeout: 30
233
+ sources:
234
+ - csv: tasks.csv
235
+ ```
236
+
237
+ ### Example 3: Multiple Commands
238
+
239
+ ```yaml
240
+ run:
241
+ - echo "Starting task {{id}}"
242
+ - ./process --input {{file}}
243
+ - echo "Task {{id}} complete"
244
+ parallelism: 3
245
+ timeout: 120
246
+ sources:
247
+ - csv: tasks.csv
248
+ ```
249
+
250
+ ### Example 4: Shell Command Validation
251
+
252
+ ```yaml
253
+ run: echo "{{message}}"
254
+ parallelism: 3
255
+ validations:
256
+ # Using grep to check if output contains specific text
257
+ - name: "Contains 'test' word"
258
+ type: shell
259
+ command: grep -q "test"
260
+ exit_code: 0
261
+
262
+ # Count words and ensure it's greater than 2
263
+ - name: "More than 2 words"
264
+ type: shell
265
+ command: test $(wc -w | awk '{print $1}') -gt 2
266
+ exit_code: 0
267
+
268
+ # Traditional regex validation (for comparison)
269
+ - name: "Contains test or validation"
270
+ type: regex
271
+ regex: "test|validation"
272
+ sources:
273
+ - csv: messages.csv
274
+ ```
275
+
276
+ ### Example 5: Regex Validation
277
+
278
+ ```yaml
279
+ run: cargo test {{test_name}}
280
+ validations:
281
+ - name: "All tests passed"
282
+ type: regex
283
+ regex: test result:\s+ok
284
+ sources:
285
+ - csv: tests.csv
286
+ ```
287
+
288
+ ## Tips
289
+
290
+ 1. **Use quotes in commands**: When passing CSV values with spaces, wrap them in quotes:
291
+ ```yaml
292
+ command: forge -p '{{prompt}}'
293
+ ```
294
+
295
+ 2. **Build before running**: Use `before_run` to ensure binaries are up-to-date:
296
+ ```yaml
297
+ before_run:
298
+ - cargo build
299
+ ```
300
+
301
+ 3. **Start with low parallelism**: Test with `parallelism: 1` first, then increase:
302
+ ```yaml
303
+ parallelism: 1 # Start here
304
+ ```
305
+
306
+ 4. **Set appropriate timeouts**: Add timeouts to prevent hanging:
307
+ ```yaml
308
+ timeout: 60 # seconds
309
+ ```
310
+
311
+ 5. **Check debug logs**: When tasks fail, check the debug directory for full output:
312
+ ```bash
313
+ cat evals/my_eval/debug/*/task-1.log
314
+ ```
315
+
316
+ ## Exit Codes
317
+
318
+ The CLI exits with:
319
+ - **0**: All tasks passed
320
+ - **1**: One or more tasks failed (excluding timeouts and validation failures)
benchmarks/cli.ts ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ // Handle EPIPE errors gracefully (e.g., when piping to `head` or `jq` that closes early)
4
+ process.stdout.on("error", (error: NodeJS.ErrnoException) => {
5
+ if (error.code === "EPIPE") {
6
+ process.exit(0);
7
+ }
8
+ throw error;
9
+ });
10
+
11
+ import * as fs from "fs/promises";
12
+ import * as path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { parse as parseYaml } from "yaml";
15
+ import { exec } from "child_process";
16
+ import { promisify } from "util";
17
+ import pLimit from "p-limit";
18
+ import pino from "pino";
19
+ import { TaskStatus, type Task } from "./model.js";
20
+ import {
21
+ getContextsFromSources,
22
+ generateCommand,
23
+ } from "./command-generator.js";
24
+ import { parseCliArgs } from "./parse.js";
25
+ import { executeTask, type TaskExecutionResult } from "./task-executor.js";
26
+ import { processValidations, type ValidationResult } from "./verification.js";
27
+ import { createTempDir, parseCsvAsync } from "./utils.js";
28
+
29
+ const execAsync = promisify(exec);
30
+
31
+ export type TaskResult = {
32
+ index: number;
33
+ status: TaskStatus;
34
+ command: string;
35
+ duration: number;
36
+ validationResults: ValidationResult[];
37
+ };
38
+
39
+ // ESM compatibility for __dirname
40
+ const __filename = fileURLToPath(import.meta.url);
41
+ const __dirname = path.dirname(__filename);
42
+
43
+ /**
44
+ * Create logger instance
45
+ * - Human-readable CLI output by default
46
+ * - Set LOG_JSON=1 for machine-readable JSON output (for piping to jq, log aggregators, etc.)
47
+ */
48
+ const logger =
49
+ process.env.LOG_JSON === "1"
50
+ ? pino({
51
+ level: process.env.LOG_LEVEL || "info",
52
+ formatters: {
53
+ level: (label) => ({ level: label }),
54
+ },
55
+ timestamp: pino.stdTimeFunctions.isoTime,
56
+ })
57
+ : pino({
58
+ level: process.env.LOG_LEVEL || "info",
59
+ transport: {
60
+ target: "pino-pretty",
61
+ options: {
62
+ colorize: true,
63
+ translateTime: "HH:MM:ss",
64
+ ignore: "pid,hostname",
65
+ messageFormat: "{msg}",
66
+ },
67
+ },
68
+ formatters: {
69
+ level: (label) => ({ level: label }),
70
+ },
71
+ timestamp: pino.stdTimeFunctions.isoTime,
72
+ });
73
+
74
+ async function main() {
75
+ // Parse command line arguments
76
+ let args;
77
+ try {
78
+ args = await parseCliArgs(__dirname);
79
+ } catch (error) {
80
+ const message = error instanceof Error ? error.message : "Unknown error";
81
+ logger.error({ error: message }, "Failed to parse CLI arguments");
82
+ process.exit(1);
83
+ }
84
+
85
+ const { evalName, evalDir, taskFile } = args;
86
+
87
+ // Check if eval directory and task file exist
88
+ try {
89
+ await fs.access(evalDir);
90
+ } catch {
91
+ logger.error({ evalDir }, "Eval directory not found");
92
+ process.exit(1);
93
+ }
94
+
95
+ try {
96
+ await fs.access(taskFile);
97
+ } catch {
98
+ logger.error({ evalDir }, "task.yml not found");
99
+ process.exit(1);
100
+ }
101
+
102
+ // Read and parse task.yml
103
+ const taskContent = await fs.readFile(taskFile, "utf-8");
104
+ const task: Task = parseYaml(taskContent);
105
+
106
+ // Display header
107
+ const displayName = path.relative(__dirname, evalDir) || evalName;
108
+
109
+ // Create debug directory with timestamp
110
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
111
+ const debugDir = path.join(evalDir, "debug", timestamp);
112
+ await fs.mkdir(debugDir, { recursive: true });
113
+
114
+ // Create a temp directory for setup commands
115
+ const setupTmpDir = await createTempDir("forge-setup-");
116
+
117
+ // Execute before_run commands
118
+ if (task.before_run && task.before_run.length > 0) {
119
+ for (const cmd of task.before_run) {
120
+ try {
121
+ logger.info(
122
+ { dir: setupTmpDir.name, command: cmd },
123
+ "Running setup command",
124
+ );
125
+ // Small delay to allow logger to flush before command output
126
+ await new Promise((resolve) => setTimeout(resolve, 0));
127
+ await execAsync(cmd, {
128
+ cwd: setupTmpDir.name,
129
+ });
130
+ } catch (error) {
131
+ logger.error({ command: cmd }, "Setup command failed");
132
+ process.exit(1);
133
+ }
134
+ }
135
+ }
136
+
137
+ // Load data from sources and create cross product
138
+ const sourcesData: Record<string, string>[][] = [];
139
+
140
+ for (const source of task.sources) {
141
+ if ("csv" in source) {
142
+ const csvPath = path.join(evalDir, source.csv);
143
+ try {
144
+ await fs.access(csvPath);
145
+ } catch {
146
+ logger.error({ csvPath }, "CSV file not found");
147
+ process.exit(1);
148
+ }
149
+
150
+ const csvContent = await fs.readFile(csvPath, "utf-8");
151
+ const csvData = await parseCsvAsync(csvContent, {
152
+ columns: true,
153
+ skip_empty_lines: true,
154
+ });
155
+ sourcesData.push(csvData);
156
+ } else if ("cmd" in source) {
157
+ logger.error("cmd source type not yet implemented");
158
+ process.exit(1);
159
+ } else if ("value" in source) {
160
+ sourcesData.push(source.value);
161
+ }
162
+ }
163
+
164
+ // Create cross product of all sources
165
+ if (sourcesData.length === 0) {
166
+ logger.error("No sources configured");
167
+ process.exit(1);
168
+ }
169
+
170
+ // Get contexts from sources using pure function
171
+ const data = getContextsFromSources(sourcesData);
172
+
173
+ const results: TaskResult[] = [];
174
+
175
+ // Get parallelism setting (default to 1 for sequential execution)
176
+ const parallelism = task.parallelism ?? 1;
177
+ const limit = pLimit(parallelism);
178
+
179
+ // Execute run command for each data row
180
+ // Create promises for all tasks
181
+ const taskPromises = data.map((row, i) => {
182
+ return limit(async () => {
183
+ // Create a unique temp directory for this task
184
+ const taskTmpDir = await createTempDir(`forge-task-${i + 1}-`);
185
+
186
+ // Create a 'task' subdirectory for running commands
187
+ const taskWorkDir = path.join(taskTmpDir.name, 'task');
188
+ await fs.mkdir(taskWorkDir, { recursive: true });
189
+
190
+ const logFile = path.join(taskTmpDir.name, `task.log`);
191
+
192
+ // Context for command interpolation and validations
193
+ const context = { ...row, dir: taskTmpDir.name };
194
+
195
+ // Support both single command and multiple commands
196
+ const commands = Array.isArray(task.run) ? task.run : [task.run];
197
+
198
+ // Filter out empty or non-string commands
199
+ const validCommands = commands.filter(cmd => typeof cmd === 'string' && cmd.trim().length > 0);
200
+
201
+ // If no valid commands, skip this task
202
+ if (validCommands.length === 0) {
203
+ logger.warn({ task_id: i + 1 }, "No valid commands found, skipping task");
204
+ return {
205
+ index: i + 1,
206
+ status: TaskStatus.Failed,
207
+ command: "No valid commands",
208
+ duration: 0,
209
+ validationResults: [],
210
+ };
211
+ }
212
+
213
+ let combinedOutput = "";
214
+ let totalDuration = 0;
215
+ let lastError: string | undefined;
216
+ let hasTimeout = false;
217
+ let hasEarlyExit = false;
218
+
219
+ // Log task launch once before executing commands
220
+ logger.info(
221
+ {
222
+ task_id: i + 1,
223
+ total_commands: validCommands.length,
224
+ log: logFile,
225
+ dir: taskTmpDir.name,
226
+ work_dir: taskWorkDir,
227
+ parameters: context,
228
+ },
229
+ "Launching task",
230
+ );
231
+
232
+ // Execute commands sequentially
233
+ for (let cmdIdx = 0; cmdIdx < validCommands.length; cmdIdx++) {
234
+ const commandTemplate = validCommands[cmdIdx]!; // Non-null assertion safe after filter
235
+
236
+ const command = generateCommand(commandTemplate, context);
237
+
238
+ logger.info(
239
+ {
240
+ command,
241
+ task_id: i + 1,
242
+ command_id: cmdIdx + 1,
243
+ total_commands: validCommands.length,
244
+ },
245
+ "Executing command",
246
+ );
247
+
248
+ const executionResult = await executeTask(
249
+ command,
250
+ i + 1,
251
+ logFile,
252
+ taskWorkDir, // Use taskWorkDir instead of taskTmpDir.name
253
+ task,
254
+ context,
255
+ cmdIdx > 0, // append if this is not the first command
256
+ );
257
+
258
+ totalDuration += executionResult.duration;
259
+
260
+ if (executionResult.output) {
261
+ combinedOutput += executionResult.output;
262
+ }
263
+
264
+ if (executionResult.earlyExit) {
265
+ hasEarlyExit = true;
266
+ }
267
+
268
+ // If execution failed or timed out, stop executing remaining commands
269
+ if (executionResult.error) {
270
+ lastError = executionResult.error;
271
+ hasTimeout = executionResult.isTimeout;
272
+
273
+ logger.warn(
274
+ {
275
+ task_id: executionResult.index,
276
+ command: executionResult.command,
277
+ command_id: cmdIdx + 1,
278
+ duration: executionResult.duration,
279
+ error: executionResult.error,
280
+ is_timeout: executionResult.isTimeout,
281
+ },
282
+ executionResult.isTimeout ? "Task timed out" : "Task failed",
283
+ );
284
+ break;
285
+ }
286
+ }
287
+
288
+ // If any command failed, return failure result
289
+ if (lastError) {
290
+ const { validationResults } = await processValidations(
291
+ combinedOutput,
292
+ task,
293
+ logger,
294
+ i + 1,
295
+ totalDuration,
296
+ logFile,
297
+ context,
298
+ );
299
+
300
+ return {
301
+ index: i + 1,
302
+ status: hasTimeout ? TaskStatus.Timeout : TaskStatus.Failed,
303
+ command: validCommands.length === 1 ? validCommands[0]! : `${validCommands.length} commands`,
304
+ duration: totalDuration,
305
+ validationResults,
306
+ };
307
+ }
308
+
309
+ // Run validations on the combined output
310
+ const { validationResults, status: validationStatus } =
311
+ await processValidations(
312
+ combinedOutput,
313
+ task,
314
+ logger,
315
+ i + 1,
316
+ totalDuration,
317
+ logFile,
318
+ context,
319
+ );
320
+
321
+ return {
322
+ index: i + 1,
323
+ status:
324
+ validationStatus === "passed"
325
+ ? TaskStatus.Passed
326
+ : TaskStatus.ValidationFailed,
327
+ command: validCommands.length === 1 ? validCommands[0]! : `${validCommands.length} commands`,
328
+ duration: totalDuration,
329
+ validationResults,
330
+ };
331
+ });
332
+ });
333
+
334
+ // Wait for all tasks to complete
335
+ const taskResults = await Promise.all(taskPromises);
336
+ results.push(...taskResults);
337
+
338
+ // Calculate summary statistics
339
+ const successCount = results.filter(
340
+ (r) => r.status === TaskStatus.Passed,
341
+ ).length;
342
+ const warningCount = results.filter(
343
+ (r) => r.status === TaskStatus.ValidationFailed,
344
+ ).length;
345
+ const timeoutCount = results.filter(
346
+ (r) => r.status === TaskStatus.Timeout,
347
+ ).length;
348
+ const failCount = results.filter(
349
+ (r) => r.status === TaskStatus.Failed,
350
+ ).length;
351
+ const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
352
+
353
+ // Calculate validation statistics
354
+ const totalValidations = results.reduce(
355
+ (sum, r) => sum + r.validationResults.length,
356
+ 0,
357
+ );
358
+ const passedValidations = results.reduce(
359
+ (sum, r) => sum + r.validationResults.filter((v) => v.passed).length,
360
+ 0,
361
+ );
362
+
363
+ // Print summary
364
+ logger.info(
365
+ {
366
+ total: results.length,
367
+ passed: successCount,
368
+ validation_failed: warningCount,
369
+ timeout: timeoutCount,
370
+ failed: failCount,
371
+ total_duration: totalDuration,
372
+ validations: {
373
+ total: totalValidations,
374
+ passed: passedValidations,
375
+ failed: totalValidations - passedValidations,
376
+ },
377
+ dir: setupTmpDir.name,
378
+ },
379
+ "Evaluation completed",
380
+ );
381
+
382
+ // Exit with error code if any task failed (excluding timeouts and validation failures)
383
+ if (failCount > 0) {
384
+ process.exit(1);
385
+ }
386
+
387
+ // Exit successfully - ensures process terminates even with open handles
388
+ process.exit(0);
389
+ }
390
+
391
+ main().catch((error) => {
392
+ logger.error({ error: error.message }, "Fatal error");
393
+ process.exit(1);
394
+ });
benchmarks/command-generator.ts ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Handlebars from "handlebars";
2
+
3
+ /**
4
+ * Loads data from CSV content string (internal helper)
5
+ */
6
+ function loadCsvData(csvContent: string): Record<string, string>[] {
7
+ const lines = csvContent.trim().split("\n");
8
+ if (lines.length === 0) {
9
+ return [];
10
+ }
11
+
12
+ const firstLine = lines[0];
13
+ if (!firstLine) {
14
+ return [];
15
+ }
16
+
17
+ const headers = firstLine.split(",").map((h) => h.trim());
18
+ const data: Record<string, string>[] = [];
19
+
20
+ for (let i = 1; i < lines.length; i++) {
21
+ const line = lines[i];
22
+ if (!line || !line.trim()) continue;
23
+
24
+ const values = line.split(",").map((v) => v.trim());
25
+ const row: Record<string, string> = {};
26
+
27
+ for (let j = 0; j < headers.length; j++) {
28
+ const header = headers[j];
29
+ if (header) {
30
+ row[header] = values[j] || "";
31
+ }
32
+ }
33
+
34
+ data.push(row);
35
+ }
36
+
37
+ return data;
38
+ }
39
+
40
+ /**
41
+ * Creates a cross product of multiple data arrays (internal helper)
42
+ */
43
+ function createCrossProduct(
44
+ sourcesData: Record<string, string>[][]
45
+ ): Record<string, string>[] {
46
+ if (sourcesData.length === 0) {
47
+ return [];
48
+ }
49
+
50
+ return sourcesData.reduce((acc, sourceData) => {
51
+ if (acc.length === 0) {
52
+ return sourceData;
53
+ }
54
+
55
+ const result: Record<string, string>[] = [];
56
+ for (const accItem of acc) {
57
+ for (const sourceItem of sourceData) {
58
+ result.push({ ...accItem, ...sourceItem });
59
+ }
60
+ }
61
+ return result;
62
+ }, [] as Record<string, string>[]);
63
+ }
64
+
65
+ /**
66
+ * Generates a command from a template and data context
67
+ */
68
+ export function generateCommand(
69
+ commandTemplate: string,
70
+ context: Record<string, string>
71
+ ): string {
72
+ const template = Handlebars.compile(commandTemplate, {
73
+ strict: true,
74
+ });
75
+ return template(context);
76
+ }
77
+
78
+ /**
79
+ * Generates all commands from a template and cross product data (internal helper)
80
+ */
81
+ function generateCommands(
82
+ commandTemplate: string,
83
+ sourcesData: Record<string, string>[][]
84
+ ): string[] {
85
+ const crossProduct = createCrossProduct(sourcesData);
86
+ return crossProduct.map((context) =>
87
+ generateCommand(commandTemplate, context)
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Pure function to get contexts from sources data
93
+ */
94
+ export function getContextsFromSources(
95
+ sourcesData: Record<string, string>[][]
96
+ ): Record<string, string>[] {
97
+ return createCrossProduct(sourcesData);
98
+ }
benchmarks/evals/echo/task.yml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ run:
2
+ - git clone --depth=1 --branch main https://github.com/antinomyhq/forge
3
+ - ls -la
4
+ - echo "Forge deployment: $(readlink $(which forgee))"
5
+ - echo '{{message}}'
6
+ parallelism: 5
7
+ timeout: 10
8
+ validations:
9
+ - name: "Output contains Hello"
10
+ type: regex
11
+ regex: "Hello"
12
+ - name: "Contains Hello using grep"
13
+ type: shell
14
+ command: 'grep -q "Hello"'
15
+ exit_code: 0
16
+ - name: "Output is not empty"
17
+ type: shell
18
+ command: 'grep -q "."'
19
+ exit_code: 0
20
+ sources:
21
+ - value:
22
+ - message: "Hello, World!"
23
+ - message: "Hello from test message 123"
24
+ - message: "Hello with special chars: !@#$%"
25
+ - message: "Hello multi word message with spaces"
26
+ - message: "Hello with numbers: 42 and 3.14"
benchmarks/evals/multi_file_patch/multi_file_patch_tasks.csv ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ task
2
+ "Add a comment // TODO: Refactor this module at the top of these three files: crates/forge_domain/src/lib.rs, crates/forge_app/src/lib.rs, and crates/forge_infra/src/lib.rs"
3
+ "Add the line edition = ""2021"" to the [package] section in these three Cargo.toml files: crates/forge_domain/Cargo.toml, crates/forge_app/Cargo.toml, and crates/forge_infra/Cargo.toml"
4
+ "Add ## License as a new section at the end of these markdown files: README.md, AGENTS.md, and docs/tool-guidelines.md"
5
+ "Add the import use anyhow::Result; at the top of these files (after existing imports): crates/forge_domain/src/lib.rs, crates/forge_app/src/lib.rs, and crates/forge_repo/src/lib.rs"
benchmarks/evals/multi_file_patch/task.yml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ run:
2
+ # Clone into `tmp/task` dir
3
+ - git clone --depth=1 --branch main https://github.com/tailcallhq/forgecode .
4
+ - FORGE_DEBUG_REQUESTS='{{dir}}/context.json' forgee --provider open_router --model {{model}} -p '{{task}}'
5
+ parallelism: 50
6
+ timeout: 120
7
+ early_exit: true
8
+ validations:
9
+ - name: "Should use patch tool (not git apply)"
10
+ type: shell
11
+ command: "jq -e '[.messages[]?.tool_calls[]? | select(.function.name == \"patch\")] | length > 0' {{dir}}/context.json"
12
+ - name: "Should NOT use shell with git apply"
13
+ type: shell
14
+ command: "jq -e '[.messages[]?.tool_calls[]? | select(.function.name == \"shell\" and (.function.arguments | contains(\"git apply\") or contains(\"git patch\")))] | length == 0' {{dir}}/context.json"
15
+ - name: "Should call patch tool multiple times for multi-file changes"
16
+ type: shell
17
+ command: "jq -e '[.messages[]?.tool_calls[]? | select(.function.name == \"patch\")] | length >= 3' {{dir}}/context.json"
18
+ sources:
19
+ - value:
20
+ - model: "openai/gpt-5.2"
21
+ - csv: multi_file_patch_tasks.csv
benchmarks/evals/patch_exact_match/patch_exact_match_tasks.csv ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ task
2
+ "Remove all backticks from test_files/plan.md. Make multiple separate patches to remove backticks from different sections."
3
+ "Update test_files/plan.md to remove markdown code formatting. Apply patches to remove backticks from each task rationale."
4
+ "Fix test_files/plan.md by converting backtick-wrapped identifiers. Use patch for each occurrence."
5
+ "Clean up test_files/plan.md by removing backticks. Patch each task description separately."
6
+ "In test_files/plan.md, convert backtick-enclosed terms. Use patch operations for different lines."
7
+ "Remove markdown code formatting (backticks) from test_files/plan.md. Apply multiple patches."
benchmarks/evals/patch_exact_match/task.yml ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ run:
2
+ - mkdir -p test_files
3
+ - |
4
+ cat > test_files/plan.md << 'EOF'
5
+ # Support for Remote Tools (Provider-Executed Tools)
6
+
7
+ ## Objective
8
+
9
+ Enable support for remote tools (also known as provider-executed tools) such as websearch, which are executed by the AI provider's infrastructure rather than by the client application. This allows providers like OpenAI to offer specialized capabilities like web search, code execution, and file retrieval that run on their servers and return results directly to the application.
10
+
11
+ ## Implementation Plan
12
+
13
+ - [ ] 1. Extend the ToolDefinition structure to support provider-executed tools
14
+ Add a new field to the `ToolDefinition` struct to distinguish between client-executed tools (current behavior) and provider-executed tools (new capability). This field will indicate that the tool should be passed directly to the provider without local execution logic.
15
+
16
+ Rationale: The current `ToolDefinition` at `crates/forge_domain/src/tools/definition/tool_definition.rs:12` only contains name, description, and input_schema. To support provider-executed tools, we need a way to mark which tools are handled by the provider infrastructure. This is similar to how Vercel AI SDK distinguishes between `function` type tools and `provider` type tools.
17
+
18
+ Affected files:
19
+ - `crates/forge_domain/src/tools/definition/tool_definition.rs` - Add field to ToolDefinition
20
+ - `crates/forge_repo/src/conversation/conversation_record.rs` - Update serialization/deserialization
21
+
22
+ - [ ] 2. Create provider-specific tool definitions for OpenAI Responses API
23
+ Define a set of provider-executed tools for OpenAI's Responses API, including `web_search`, `code_interpreter`, `file_search`, and other provider-native capabilities. These will be represented as `ToolDefinition` instances marked as provider-executed.
24
+
25
+ Rationale: OpenAI's Responses API provides built-in tools like `web_search` that are executed by OpenAI's infrastructure. These tools need to be defined with their specific schemas and parameters (e.g., searchContextSize, userLocation for web_search) similar to how Vercel AI SDK's `prepareResponsesTools` function handles these tools. This allows the application to offer these capabilities without implementing the execution logic.
26
+
27
+ Affected files:
28
+ - `crates/forge_app/src/dto/openai/provider_tools.rs` - New file for provider tool definitions
29
+ - `crates/forge_app/src/dto/openai/mod.rs` - Export new module
30
+
31
+ - [ ] 3. Implement tool conversion logic for provider-executed tools
32
+ Create a conversion function that transforms provider-executed `ToolDefinition` instances into the provider-specific format required by the API. For OpenAI, this means converting provider tools to the appropriate structure (e.g., `{type: "web_search", ...}`) instead of the standard function tool format.
33
+
34
+ Rationale: The current `From<ToolDefinition> for Tool` implementation at `crates/forge_app/src/dto/openai/request.rs:297` converts all tools to the standard function format. Provider-executed tools need special handling to be converted to their provider-specific formats. For OpenAI, this means converting provider tools to the appropriate structure with a type field indicating the specific tool type instead of using the standard function tool format. This mirrors Vercel AI SDK's `prepareResponsesTools` approach where provider tools are handled differently from function tools.
35
+
36
+ Affected files:
37
+ - `crates/forge_app/src/dto/openai/request.rs` - Modify Tool conversion logic
38
+ - `crates/forge_app/src/dto/openai/provider_tools.rs` - Implement conversion functions
39
+
40
+ - [ ] 4. Extend the Context to Tool request conversion
41
+ Update the `From<Context> for Request` implementation at `crates/forge_app/src/dto/openai/request.rs:323` to handle provider-executed tools. This involves filtering and converting provider tools separately from standard function tools when building the request.
42
+
43
+ Rationale: The current implementation treats all tools uniformly and converts them using the same logic. Provider-executed tools need to be identified and converted using the new provider-specific conversion logic before being included in the API request. This ensures that tools like `web_search` are sent in the correct format to OpenAI.
44
+
45
+ Affected files:
46
+ - `crates/forge_app/src/dto/openai/request.rs` - Update Context to Request conversion
47
+
48
+ - [ ] 5. Update the tool registry to handle provider-executed tool results
49
+ Modify the `ToolRegistry` at `crates/forge_app/src/tool_registry.rs:21` to recognize that provider-executed tools do not require local execution. When a provider tool is invoked, the tool registry should pass the result directly from the provider response without attempting local execution.
50
+
51
+ Rationale: Currently, the tool registry attempts to execute all tools locally through `ToolExecutor`, `AgentExecutor`, or `McpExecutor`. Provider-executed tools are different - the provider executes them and returns the result. The tool registry needs to distinguish between these cases and handle provider tool results appropriately.
52
+
53
+ Affected files:
54
+ - `crates/forge_app/src/tool_registry.rs` - Add logic to skip execution for provider tools
55
+ - `crates/forge_app/src/tool_registry.rs` - Handle provider tool results from responses
56
+
57
+ - [ ] 6. Add provider tool configuration to agent definitions
58
+ Extend the agent definition to include optional provider tool configurations. This allows agents to be configured with specific provider-executed tools (e.g., `web_search` enabled with specific parameters) while maintaining compatibility with existing agent configurations.
59
+
60
+ Rationale: Different agents may require different provider tools. For example, a research agent might need `web_search` enabled, while a coding agent might need `code_interpreter`. Adding this configuration to agent definitions provides fine-grained control over which provider tools are available to each agent.
61
+
62
+ Affected files:
63
+ - `crates/forge_domain/src/agent_definition.rs` - Add provider tools configuration
64
+ - `crates/forge_app/src/dto/tools_overview.rs` - Include provider tools in overview
65
+
66
+ - [ ] 7. Create provider tool factory and registration
67
+ Implement a factory pattern for registering provider tools with the system. This factory will create provider tool definitions for different providers (OpenAI, Anthropic, etc.) and make them available to agents that request them.
68
+
69
+ Rationale: Different providers support different provider-executed tools. A factory pattern allows the system to create the appropriate tool definitions based on the provider being used. This is extensible - new providers can be added by implementing their tool factory without modifying core logic.
70
+
71
+ Affected files:
72
+ - `crates/forge_app/src/provider_tools/factory.rs` - New file for tool factory
73
+ - `crates/forge_app/src/provider_tools/mod.rs` - New module
74
+ - `crates/forge_app/src/dto/tools_overview.rs` - Integrate with factory
75
+
76
+ - [ ] 8. Update the response parsing to handle provider tool results
77
+ Modify the response parsing logic in `crates/forge_app/src/dto/openai/response.rs` to correctly parse and handle tool results from provider-executed tools. This includes recognizing when a tool result comes from a provider tool and ensuring it's properly formatted.
78
+
79
+ Rationale: Provider tool results may have different structures than standard tool results. The response parser needs to handle these differences and convert them to the internal `ToolResult` format consistently. This ensures that tool results are processed correctly regardless of whether they came from a client-executed or provider-executed tool.
80
+
81
+ Affected files:
82
+ - `crates/forge_app/src/dto/openai/response.rs` - Update tool result parsing
83
+ - `crates/forge_app/src/dto/openai/response.rs` - Handle provider-specific result formats
84
+
85
+ - [ ] 9. Add provider tool support to the tool overview API
86
+ Update the `ToolsOverview` structure and related API endpoints to include provider-executed tools alongside system tools, agent tools, and MCP tools. This allows clients to discover which provider tools are available.
87
+
88
+ Rationale: The current `ToolsOverview` at `crates/forge_app/src/dto/tools_overview.rs:9` includes system tools, agents, and MCP tools. Provider tools should be included in this overview so clients can see all available tools, including those executed by providers. This maintains consistency with the existing tool discovery mechanism.
89
+
90
+ Affected files:
91
+ - `crates/forge_app/src/dto/tools_overview.rs` - Add provider tools field
92
+ - `crates/forge_app/src/tool_registry.rs` - Update tools_overview method
93
+
94
+ - [ ] 10. Implement Anthropic provider tools support
95
+ Create provider tool definitions for Anthropic's provider-executed tools (e.g., `computer_20241022`, `textEditor_20241022`, `bash_20241022`, `webSearch_20250305`, `codeExecution_20250522`). This demonstrates the extensibility of the provider tool system to multiple providers.
96
+
97
+ Rationale: Anthropic offers provider-executed tools similar to OpenAI but with different names and capabilities. Implementing support for Anthropic provider tools validates that the architecture is provider-agnostic and extensible. It follows the same pattern established for OpenAI tools.
98
+
99
+ Affected files:
100
+ - `crates/forge_app/src/dto/anthropic/provider_tools.rs` - New file for Anthropic provider tools
101
+ - `crates/forge_app/src/dto/anthropic/request.rs` - Update conversion logic
102
+ - `crates/forge_app/src/provider_tools/factory.rs` - Add Anthropic tool factory
103
+
104
+ - [ ] 11. Add tests for provider tool conversion and execution
105
+ Create comprehensive tests covering provider tool definition, conversion to provider formats, integration with Context to Request conversion, and handling of provider tool results. Tests should verify that provider tools are correctly formatted and that client-executed tools remain unaffected.
106
+
107
+ Rationale: Provider tools introduce new code paths and conversion logic that need thorough testing. Tests should ensure that provider tools are correctly converted to their provider-specific formats, that they don't interfere with standard tool execution, and that the system handles mixed scenarios (both provider and client tools) correctly.
108
+
109
+ Affected files:
110
+ - `crates/forge_app/src/dto/openai/request.rs` - Add conversion tests
111
+ - `crates/forge_app/src/dto/anthropic/request.rs` - Add conversion tests
112
+ - `crates/forge_app/src/tool_registry.rs` - Add execution handling tests
113
+
114
+ - [ ] 12. Update documentation and examples
115
+ Document the provider tool system, including how to define provider tools, how they are converted to provider formats, and how agents can be configured to use them. Include examples of using `web_search` and other provider tools.
116
+
117
+ Rationale: Provider tools are a new concept in the system. Documentation is essential for developers to understand when and how to use provider-executed tools versus client-executed tools. Examples should demonstrate practical usage patterns and configuration options.
118
+
119
+ Affected files:
120
+ - `docs/provider-tools.md` - New documentation file
121
+ - `examples/` - Add example configurations
122
+
123
+ ## Verification Criteria
124
+
125
+ - [ ] Provider tool definitions can be created and marked as provider-executed
126
+ - [ ] Provider tools are correctly converted to OpenAI-specific format (for example, using the type field to specify web search instead of function format)
127
+ - [ ] Provider tools are correctly converted to Anthropic-specific format
128
+ - [ ] Context to Request conversion includes provider tools in the correct format
129
+ - [ ] Tool registry recognizes provider tools and skips local execution
130
+ - [ ] Provider tool results from API responses are correctly parsed and handled
131
+ - [ ] Agent definitions can be configured with provider tools
132
+ - [ ] Tool overview API includes provider tools alongside other tool types
133
+ - [ ] All existing tests pass without modification
134
+ - [ ] New tests cover provider tool conversion, execution, and result handling
135
+ - [ ] Documentation describes the provider tool system with examples
136
+
137
+ ## Potential Risks and Mitigations
138
+
139
+ 1. **Breaking changes to existing tool definitions**
140
+ Risk: Adding a new field to `ToolDefinition` could break existing serialization/deserialization or require migrations.
141
+
142
+ Mitigation: Make the new field optional with a default value of `false` (client-executed). This ensures backward compatibility with existing tool definitions. Run all existing tests to verify no regressions.
143
+
144
+ 2. **Provider tool format incompatibility**
145
+ Risk: Different providers may have incompatible tool formats, making it difficult to create a unified abstraction.
146
+
147
+ Mitigation: Use provider-specific conversion modules (e.g., `openai_provider_tools`, `anthropic_provider_tools`) that handle format conversion independently. The common `ToolDefinition` serves as an abstraction, but each provider has its own conversion logic.
148
+
149
+ 3. **Tool result handling complexity**
150
+ Risk: Provider tool results may have different structures that are difficult to normalize into a common `ToolResult` format.
151
+
152
+ Mitigation: Create provider-specific result parsers that normalize results to the internal format. Handle unknown or unexpected result formats gracefully with appropriate error messages.
153
+
154
+ 4. **Execution model confusion**
155
+ Risk: Developers may be confused about which tools are executed locally versus by the provider, leading to unexpected behavior.
156
+
157
+ Mitigation: Clearly document the distinction in the `ToolDefinition` structure and provide tool overview information that indicates execution location (client vs. provider). Use descriptive naming conventions.
158
+
159
+ 5. **Testing complexity**
160
+ Risk: Testing provider tools requires mocking provider responses, which can be complex and brittle.
161
+
162
+ Mitigation: Create integration tests that use real provider APIs for critical paths, supplemented by unit tests with mocked responses. Use snapshot testing to verify correct request formatting.
163
+
164
+ ## Alternative Approaches
165
+
166
+ 1. **Separate tool type enum instead of boolean flag**
167
+ Instead of adding a boolean field to `ToolDefinition`, create an enum `ToolExecutionType` with variants `Client` and `Provider`. This is more explicit and extensible, allowing future execution types (e.g., `Hybrid`, `Delegated`).
168
+
169
+ Trade-offs: More explicit and type-safe, but adds complexity. The boolean flag is simpler for the current use case and can be extended to an enum later if needed.
170
+
171
+ 2. **Provider-specific tool registries**
172
+ Create separate tool registries for each provider (OpenAI, Anthropic, etc.) rather than a unified registry. Each provider would manage its own tools, including provider-executed tools.
173
+
174
+ Trade-offs: Cleaner separation of concerns and easier provider-specific logic, but increases code duplication and makes it harder to share tools across providers.
175
+
176
+ 3. **Dynamic tool discovery from provider APIs**
177
+ Instead of hard-coding provider tool definitions, query provider APIs to discover available tools and their schemas dynamically. This would automatically support new provider tools as they are added.
178
+
179
+ Trade-offs: More flexible and future-proof, but introduces runtime dependencies on provider APIs and may be slower. Requires robust error handling for API failures.
180
+
181
+ 4. **Tool execution delegation pattern**
182
+ Implement a delegation pattern where tools specify an executor (client, provider, MCP, etc.) through a trait. The tool registry would delegate execution based on the executor specified by the tool.
183
+
184
+ Trade-offs: More flexible and extensible architecture, but adds abstraction layers and complexity. The current approach of checking tool type is simpler for the immediate use case.
185
+ EOF
186
+ - |
187
+ FORGE_DEBUG_REQUESTS='{{dir}}/context.json' forgee --provider open_router --model {{model}} -p '{{task}}'
188
+ parallelism: 2
189
+ timeout: 180
190
+ early_exit: true
191
+ validations:
192
+ - name: "Should use patch tool"
193
+ type: shell
194
+ command: "jq -e '[.messages[]?.tool_calls[]? | select(.function.name == \"patch\")] | length > 0' {{dir}}/context.json"
195
+
196
+ - name: "Should NOT have patch failures due to missing operation field"
197
+ type: shell
198
+ command: "! jq -e '[.messages[]? | select(.role == \"tool\" and .name == \"patch\" and (.content | contains(\"missing field\") and contains(\"operation\")))] | length > 0' {{dir}}/context.json"
199
+
200
+ - name: "Should NOT have patch failures due to text mismatch"
201
+ type: shell
202
+ command: "! jq -e '[.messages[]? | select(.role == \"tool\" and .name == \"patch\" and (.content | contains(\"Could not find match for search text\")))] | length > 0' {{dir}}/context.json"
203
+
204
+ - name: "Patch search text should match file content exactly"
205
+ type: shell
206
+ command: |
207
+ # Extract all patch tool calls and verify search text exists in the file
208
+ # This is a heuristic check - we look for patch calls that should have succeeded
209
+ jq -e '[.messages[]?.tool_calls[]? | select(.function.name == "patch")] | length > 0' {{dir}}/context.json
210
+
211
+ sources:
212
+ - value:
213
+ - model: "anthropic/claude-sonnet-4.5"
214
+ - model: "z-ai/glm-4.6:exacto"
215
+ - model: "minimax/minimax-m2.1"
216
+
217
+ - csv: patch_exact_match_tasks.csv
benchmarks/evals/redundant_cd_with_cwd/task.yml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ run:
2
+ - git clone --depth=1 --branch main https://github.com/tailcallhq/forgecode .
3
+ - mkdir ../test-dir && cd ../test-dir
4
+ - FORGE_DEBUG_REQUESTS='{{dir}}/context.json' forgee --provider open_router --model {{model}} -p '{{task}}'
5
+ parallelism: 5
6
+ timeout: 120
7
+ early_exit: false # We want to see all cases, not just the first success
8
+
9
+ validations:
10
+ # Check if any shell tool call contains "cd" command when cwd is also provided
11
+ - name: "No redundant cd with cwd"
12
+ type: shell
13
+ command: |
14
+ jq -e '
15
+ [.messages[]?.tool_calls[]? |
16
+ select(.function.name == "shell") |
17
+ .function.arguments | fromjson |
18
+ select((.command | contains("cd "))) |
19
+ {command: .command, cwd: .cwd}
20
+ ] | length == 0
21
+ ' {{dir}}/context.json
22
+
23
+ # Detect specific pattern: "cd <path> && <command>" with cwd set
24
+ - name: "No cd && pattern with cwd"
25
+ type: shell
26
+ command: |
27
+ jq -e '
28
+ [.messages[]?.tool_calls[]? |
29
+ select(.function.name == "shell") |
30
+ .function.arguments | fromjson |
31
+ select((.command | test("cd\\s+[^;]+\\s*&&"))) |
32
+ {command: .command, cwd: .cwd, issue: "redundant_cd_with_cwd"}
33
+ ] | length == 0
34
+ ' {{dir}}/context.json
35
+
36
+ sources:
37
+ - value:
38
+ - model: "anthropic/claude-sonnet-4.5"
39
+ - model: "z-ai/glm-4.6:exacto"
40
+ - value:
41
+ - task: "clean ~/tailcall"
42
+ - task: "format code in ~/task/crates/forge_domain"
43
+ - task: "install dependencies in ~/task/benchmarks"
44
+ - task: "cd to ~/task/crates and run cargo test"
45
+ - task: "go to ~/task/docs and list files"
benchmarks/evals/search_over_find/task.yml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ run:
2
+ - git clone --depth=1 --branch main https://github.com/tailcallhq/forgecode .
3
+ - FORGE_DEBUG_REQUESTS='{{dir}}/context.json' forgee --provider open_router --model {{model}} -p '{{task}}'
4
+ parallelism: 10
5
+ timeout: 180
6
+ early_exit: true
7
+
8
+ validations:
9
+ # Primary: Model should NOT use shell find command for searching code
10
+ - name: "No shell find command usage"
11
+ type: shell
12
+ command: |
13
+ jq -e '
14
+ [.messages[]?.tool_calls[]? |
15
+ select(.function.name == "shell") |
16
+ .function.arguments | fromjson |
17
+ select(.command | test("\\bfind\\s+"))
18
+ ] | length == 0
19
+ ' {{dir}}/context.json
20
+
21
+ # Model SHOULD use builtin search tools instead
22
+ - name: "Uses builtin search tools"
23
+ type: shell
24
+ command: |
25
+ jq -e '
26
+ [.messages[]?.tool_calls[]? |
27
+ select(.function.name == "search" or
28
+ .function.name == "fs_search" or
29
+ .function.name == "sem_search")
30
+ ] | length > 0
31
+ ' {{dir}}/context.json
32
+
33
+ # Detect specific anti-pattern: find with grep pipeline
34
+ - name: "No find + grep anti-pattern"
35
+ type: shell
36
+ command: |
37
+ jq -e '
38
+ [.messages[]?.tool_calls[]? |
39
+ select(.function.name == "shell") |
40
+ .function.arguments | fromjson |
41
+ select(.command | test("find.*\\|.*grep"))
42
+ ] | length == 0
43
+ ' {{dir}}/context.json
44
+
45
+ sources:
46
+ - value:
47
+ - model: "anthropic/claude-sonnet-4.5"
48
+ - model: "z-ai/glm-4.6:exacto"
49
+ - value:
50
+ # File discovery tasks - should use fs_search, NOT find command
51
+ - task: "I need to merge service and app crates. Tell me what files exist in the crates/ directory."
52
+ - task: "Find all Rust source files in the crates/service directory."
53
+ - task: "List all Rust files in crates/app to understand the structure."
54
+ - task: "Show me all .rs files in the project so I can plan refactoring."
55
+
56
+ # Structure understanding - should use fs_search or sem_search
57
+ - task: "Find all source files under crates/service and crates/app for analysis."
58
+ - task: "List all Rust source files in the workspace for documentation purposes."
benchmarks/evals/sem_search/README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Semantic Search Tool Selection Evaluation
2
+
3
+ This evaluation validates that the Forge agent correctly identifies and uses the `sem_search` (Codebase Search) tool when presented with conceptual, functionality-based code location queries.
4
+
5
+ ## What We're Testing
6
+
7
+ The agent's ability to recognize when a user query requires semantic understanding rather than exact pattern matching. Specifically, we test whether the agent:
8
+
9
+ 1. **Correctly invokes `sem_search`** for conceptual queries about code functionality
10
+ 2. **Understands the distinction** between semantic search (conceptual) vs regex search (exact patterns)
11
+ 3. **Recognizes appropriate use cases** such as:
12
+ - Finding code by its purpose/behavior (e.g., "retry logic with exponential backoff")
13
+ - Locating implementation patterns (e.g., "authentication token validation")
14
+ - Discovering architectural components (e.g., "message transformation between AI providers")
15
+ - Identifying system behaviors (e.g., "rate limiting for API requests")
16
+
17
+ ## Test Scenarios
18
+
19
+ The evaluation uses real-world queries that describe:
20
+ - **Message transformation** between different AI provider formats
21
+ - **Retry mechanisms** with exponential backoff patterns
22
+ - **Authentication flows** including token validation and refresh
23
+ - **Error handling** for network failures
24
+ - **Caching strategies** for API responses
25
+ - **Streaming response** handling from LLM APIs
26
+ - **File validation** including upload and size checks
27
+ - **Rate limiting** implementations for API requests
28
+ - **Tool registration** and availability management
29
+ - **Context management** including conversation history truncation
30
+
31
+ ## Expected Behavior
32
+
33
+ For all test queries, the agent should invoke the `sem_search` tool (displayed as "Codebase Search" in logs) rather than falling back to regex-based `search` tool, demonstrating understanding that these are conceptual queries requiring semantic code comprehension.
benchmarks/evals/sem_search/task.yml ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ run:
2
+ # Clone into `tmp/task` dir
3
+ - git clone --depth=1 --branch main https://github.com/tailcallhq/forgecode .
4
+ - forge workspace init --yes
5
+ - forge workspace sync
6
+ - FORGE_DEBUG_REQUESTS='{{dir}}/context.jsonl' FORGE_SESSION__PROVIDER_ID=open_router FORGE_SESSION__MODEL_ID={{model}} forge -p '{{task}}'
7
+ parallelism: 8
8
+ timeout: 60
9
+ early_exit: true
10
+ validations:
11
+ - name: "Uses codebase search tool"
12
+ type: shell
13
+ command: grep -q "Codebase Search" '{{dir}}/task.log'
14
+ - name: "Does not call task tool before sem_search"
15
+ type: shell
16
+ command: "python3 -c \"\nimport sys\nlog = open('{{dir}}/task.log').read().splitlines()\ntask_line = next((i for i, l in enumerate(log) if '[Agent]' in l), None)\nsearch_line = next((i for i, l in enumerate(log) if 'Codebase Search' in l), None)\nif task_line is None: sys.exit(0)\nif search_line is None: sys.exit(1)\nsys.exit(0 if search_line < task_line else 1)\n\""
17
+ sources:
18
+ - value:
19
+ # - model: "x-ai/grok-code-fast-1"
20
+ - model: "anthropic/claude-sonnet-4.6"
21
+ - value:
22
+ # # Location questions - "where is"
23
+ # - task: "Where is the code that transforms messages between different AI provider formats?"
24
+ # - task: "Where is the error handling code for network failures?"
25
+ # - task: "Where is file upload validation and size checking implemented?"
26
+ # - task: "Where are environment variables parsed and configuration loaded?"
27
+ # - task: "Where is the logic for selecting which AI model to use for a task?"
28
+
29
+ # # Discovery questions - "find"
30
+ # - task: "Find the code that caches API responses to reduce costs"
31
+ # - task: "Find where tool definitions are registered and made available to the AI"
32
+ # - task: "Find the code that manages API keys, tokens, and credentials securely"
33
+ # - task: "Find where Git operations like commit, diff, and status are implemented"
34
+
35
+ # # Understanding questions - "how does"
36
+ # - task: "How does the system handle differences between OpenAI and Anthropic APIs?"
37
+ # - task: "How does the tool execution pipeline process and validate tool calls?"
38
+ # - task: "How are streaming responses from LLMs processed?"
39
+
40
+ # # Need-based questions - "I need to"
41
+ # - task: "I need to find retry logic with exponential backoff for API calls"
42
+ # - task: "I need to understand how streaming responses from LLM APIs work"
43
+ # - task: "I need to see how conversation history is truncated when context is too large"
44
+ # - task: "I need to find examples of rate limiting implementations"
45
+
46
+ # # Show me questions
47
+ # - task: "Show me where authentication tokens are validated and refreshed"
48
+ # - task: "Show me the code that handles rate limiting for API requests"
49
+ # - task: "Show me the code that compacts conversation context when tokens exceed limits"
50
+ # - task: "Show me how the system handles request timeouts and cancellation"
51
+ # - task: "Show me the permission checking and security policy enforcement code"
52
+
53
+ # # Strategy/pattern questions
54
+ # - task: "I want to understand the caching strategy for semantic search embeddings"
55
+ # - task: "What's the strategy for managing concurrent API requests?"
56
+ # - task: "What's the approach for handling API errors and retries?"
57
+
58
+ # # Troubleshooting questions
59
+ # - task: "What could cause API requests to fail?"
60
+ # - task: "How are timeout errors handled in the system?"
61
+
62
+ # # Architecture questions
63
+ # - task: "How do the different AI provider adapters integrate with the main system?"
64
+ # - task: "What's the flow of a user request from input to response?"
65
+ # - task: "How are tools registered and executed?"
66
+
67
+ # # Capability questions
68
+ # - task: "How does the system track token usage?"
69
+ # - task: "What mechanism prevents infinite loops in tool execution?"
70
+
71
+ # Human review tasks - integrated from human-review-sem-search-tasks.json
72
+ - task: "Now that we support piping, how can I use an external editor to type a large command and pipe it to the forge cli?"
73
+ - task: "Is there a type-safe version of commander that can make sure the command is parsed into a type-safe object"
74
+ - task: "Search for ZAI-related model fetching code in the codebase"
75
+ - task: "Look for API endpoints and authentication patterns used for ZAI"
76
+ - task: "Starting a new session always loads the older node version"
77
+ - task: "Examine any existing curl examples or HTTP client implementations"
78
+ - task: "Before sorting the tools we will dedupe all the tools"
79
+ - task: "Identify the current embedding implementation and how it interfaces with OpenRouter"
80
+ - task: "Is there a place in the code base where we are subscribing to events?"
81
+ - task: "There is a bug, where when I paste text the syntax highlighting is lost for a moment until some key is pressed again."
82
+ - task: "Search for any existing validation patterns in the forge_domain crate, particularly looking for validation methods, validate functions, or error types used for domain validation"
83
+ - task: "Show me the temperature type"
84
+ - task: "Search for cancellation mechanisms including cancellation tokens, abort signals, task cancellation, and how the system handles user interrupts or cancellation of in-flight requests across all crates"
85
+ - task: "In the workspace info, we should return the creation time of the workspace too."
86
+ - task: "token cost is being sent but we are not able to show it on the ui. Our cost conversation in some place is getting missed."
87
+ - task: "Look at workflow-related files to understand how model should be obtained from API"
88
+ - task: "Lets use the newtype crate for all the type in domain that are newtypes so that we don't need to implement all this boilerplate"
89
+ - task: "Look for any existing patterns in the codebase for ensuring migrations run exactly once across all tests, such as global mutexes or initialization guards"
90
+ - task: "The clear conversation doesn't happen on CMD+K."
91
+ - task: "Use plimit and prompt.all to run it concurrently"
92
+ - task: "Like infra I need another crate called - test_infra that looks similar and has a ForgeInfra struct that has test implementations of stuff."
93
+ - task: "Infra query nodes should take Query<Embedding> instead of each param as a separate arg."
94
+ - task: "Add health check endpoints for monitoring"
95
+ - task: "Examine conversation ID generation and management in domain layer"
96
+ - task: "Lets recursively derive Dummy from fake, on Node and its children."
97
+ - task: "Add a new infra component called Reranker. It takes a list of nodes and reranks them."
98
+ - task: "Update the implementations of Cache based on the new trait implementation."
99
+ - task: "In skill lets add description also"
100
+ - task: "Investigate signal handling and graceful shutdown mechanisms in /Volumes/Bran/code-forge-workspace/code-forge, particularly looking for SIGINT, SIGTERM handling and how they affect ongoing operations"
101
+ - task: "Lets add a new range.rs under arbitrary/ and implement Arbitrary for all the std Range types eg: Range, RangeFrom etc."
102
+ - task: "Make `-v` optional. It should not be required."
103
+ - task: "in ProviderRegistry I need a few more methods - get_active_provider, set_active_provider and get_all_providers"
104
+ - task: "Lets use derive_setters on Config. Drop the handwritten setters"
105
+ - task: "Lets drop path and env from the `info` command. Its a bit too noisy."
106
+ - task: "The Secret type should print the last 4 chars of the secret in debug."
107
+ - task: "What's the purpose of ModelId?"
108
+ - task: "Where is the Agent struct defined?"
109
+ - task: "**Questions**\n\n1. Switching agent happens via CLI `forge config --set-agent muse` or `forge config -i` opens up an interactive configuration manager (RATATUI)\n2. How do I know the active agent and model? `forge config --get-agent` show in via a ZSH theme\n3. Additional CLI commands to be added\n 1. `forge conversation --set-active <cid>`\n 2. `forge info` Show current info including agent, model etc."
110
+ - task: "Find all places where event names are constructed or used, particularly in relation to agent IDs. Look for patterns like 'forge', 'muse', 'sage' in event names and how they map to agents."
111
+ - task: "Change the structure to be a simple - Vec of Vec of Option of String."
112
+ - task: "Where are the fixtures located?"
113
+ - task: "Move McpToolCache to forge_app dto"
114
+ - task: "based on your understanding of the project, where are the tables created in supbase?"
115
+ - task: "Examine how the truncation functions are called and where the results are used. Identify the call sites and understand the data flow from truncation to final output."
116
+ - task: "Like ShowCommands, I need ShowModels and ShowConversations"
117
+ - task: "Lets implement a function on SummaryMessage that can merge consecutive calls together. If the nth can be merged with nth-1, then we should remove the nth-1 and insert the new value."
118
+ - task: "Lets have upload take a glob pattern for upload"
119
+ - task: "Check this for security issues: eval(user_input)"
120
+ - task: "Update the vector dimension to be 4096"
121
+ - task: "Fix this - 'docker run tailcallhq/forgecode'"
122
+ - task: "Document the adapter registration and discovery mechanism"
123
+ - task: "There are multiple places where we throw - \"Conversation not found\" how do u suggest we consolidate and reuse."
124
+ - task: "Is there a crate for humanizing date time format that we are using?"
125
+ - task: "Lets move the domain compaction transformers into app/compact/transformers. Make sure you move them in bulk."
126
+ - task: "Identify the main entry point (main.rs or equivalent) and understand how the application initializes and starts the server."
127
+ - task: "Lets use chrono-humanize instead of humantime in forge_main everywhere."
128
+ - task: "Look at all the docs in .rs files and remove the args and return info from the docs."
129
+ - task: "Investigate why each approach (1-4) has failed - what are the technical limitations we're hitting with ZLE widgets, terminal control sequences, and shell execution contexts"
130
+ - task: "as per rust best practices, what would you recommend should we call arbit? and how should the file be structured. Show me the best examples - purest form"
131
+ - task: "Explain the flow of authentication in the project. I need to understand it deeply, going from high level to micro details."
132
+ - task: "Research xml.rs in forge_domain and create a plan to add helper functions for common XML operations"
133
+ - task: "Add JWT-based authentication system"
134
+ - task: "How does FinishReason work?"
135
+ - task: "Check what changes were made to api_key.rs conversions and identify why the key field might be getting the wrong value"
136
+ - task: "Trace the flow from skill listing to system prompt generation to identify where the integration is missing or broken"
137
+ - task: "What could cause memory leaks in long-running sessions?"
138
+ - task: "I am not able to make send of these name - DefinitionBatchGenerator DefinitionBatch. I need something much better,"
benchmarks/evals/semantic_search_quality/README.md ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Semantic Search Quality Evaluation
2
+
3
+ This evaluation measures the quality of semantic search queries generated by Forge using an LLM-as-judge approach.
4
+
5
+ ## Quick Start
6
+
7
+ ### Test Your Queries Manually
8
+
9
+ Before running the full eval, test your queries to get immediate feedback:
10
+
11
+ ```bash
12
+ # Interactive mode
13
+ npx tsx benchmarks/evals/semantic_search_quality/test_queries.ts
14
+
15
+ # Non-interactive mode
16
+ npx tsx benchmarks/evals/semantic_search_quality/test_queries.ts \
17
+ "retry mechanism with exponential backoff" \
18
+ "I need to find the implementation code for retry logic"
19
+ ```
20
+
21
+ The query tester provides:
22
+ - Detailed scores for embedding and reranking queries
23
+ - Specific strengths and weaknesses
24
+ - Actionable suggestions for improvement
25
+ - Differentiation analysis
26
+
27
+ ### Run the Full Evaluation
28
+
29
+ ```bash
30
+ # Run the entire semantic search quality eval
31
+ npm run eval semantic_search_quality
32
+
33
+ # Run with specific parallelism
34
+ npm run eval semantic_search_quality -- --parallelism 10
35
+
36
+ # Run with specific model
37
+ npm run eval semantic_search_quality -- --model "anthropic/claude-sonnet-4.5"
38
+ ```
39
+
40
+ ## Purpose
41
+
42
+ The semantic search feature uses two types of queries:
43
+ 1. **Embedding Query (`query`)**: Converted to vector embeddings for semantic similarity search
44
+ 2. **Reranking Query (`use_case`)**: Used by the reranker to filter results based on intent
45
+
46
+ This eval verifies that:
47
+ - Queries are well-formed and contain sufficient context
48
+ - The two queries are different and serve distinct purposes
49
+ - Results match the user's intent (implementation vs docs vs tests, etc.)
50
+ - Correct file types are returned based on intent
51
+ - Unwanted file types are avoided
52
+
53
+ ## How It Works
54
+
55
+ ### 1. Test Execution
56
+ The eval runs various semantic search tasks covering different intents:
57
+ - **Implementation**: Find actual code, not documentation
58
+ - **Flow Understanding**: Understand how systems work
59
+ - **Architecture**: Understand structural design
60
+ - **Tests**: Find test examples and fixtures
61
+ - **Documentation**: Find docs and guides
62
+ - **Debugging**: Investigate issues
63
+ - **Modification**: Find code to modify
64
+ - **Configuration**: Find config files and schemas
65
+
66
+ ### 2. LLM Judge Evaluation
67
+ After each task, an LLM judge (Gemini 3 Pro) evaluates:
68
+
69
+ #### Query Quality (40 points)
70
+ - **Embedding Query (15pts)**: Domain terms, technical context, behavior description
71
+ - **Reranking Query (15pts)**: Intent clarity, context about WHY, specificity
72
+ - **Query Differentiation (10pts)**: Queries serve different purposes
73
+
74
+ #### Result Relevance (60 points)
75
+ - **Intent Matching (25pts)**: Results match the stated intent
76
+ - **File Type Accuracy (20pts)**: Correct file types returned
77
+ - **Avoidance Compliance (15pts)**: Unwanted file types avoided
78
+
79
+ #### Pass Criteria
80
+ - Total score >= 70/100
81
+ - No critical issues (e.g., identical queries, severe intent mismatch)
82
+
83
+ ### 3. Structured Output
84
+ The judge uses structured output (Zod schema) to provide:
85
+ - Detailed scores for each dimension
86
+ - Specific feedback on what's good/bad
87
+ - List of critical issues
88
+ - Overall pass/fail determination
89
+
90
+ ## Environment Variables
91
+
92
+ Required:
93
+ - `GOOGLE_APPLICATION_CREDENTIALS`: Path to Google Cloud service account JSON credentials file
94
+
95
+ Or authenticate using `gcloud auth application-default login`.
96
+
97
+ The LLM judge uses Vertex AI to run Gemini 3 Pro. It supports Google's Application Default Credentials through the `google-auth-library`. The most common authentication method is to set the path to a JSON credentials file in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.
98
+
99
+ ## Test Cases
100
+
101
+ The eval includes diverse test cases:
102
+
103
+ ### Implementation Queries
104
+ ```yaml
105
+ - task: "Find the retry mechanism with exponential backoff implementation"
106
+ intent: "implementation"
107
+ expected_file_types: "rust,typescript,javascript"
108
+ should_avoid: "markdown,txt,documentation"
109
+ ```
110
+
111
+ ### Flow Understanding
112
+ ```yaml
113
+ - task: "How does the authentication flow work from request to response?"
114
+ intent: "flow_understanding"
115
+ expected_file_types: "rust,typescript"
116
+ should_avoid: "test,markdown"
117
+ ```
118
+
119
+ ### Documentation
120
+ ```yaml
121
+ - task: "Find documentation on how to configure semantic search"
122
+ intent: "documentation"
123
+ expected_file_types: "markdown,txt"
124
+ should_avoid: "rust,typescript,javascript"
125
+ ```
126
+
127
+ ## Interpreting Results
128
+
129
+ ### High Scores (>80)
130
+ Queries are excellent - domain-specific, well-differentiated, and would return highly relevant results.
131
+
132
+ ### Medium Scores (60-80)
133
+ Queries are functional but have room for improvement. Check feedback for specific suggestions.
134
+
135
+ ### Low Scores (<60)
136
+ Queries have significant issues:
137
+ - Too generic or vague
138
+ - Embedding and reranking queries too similar
139
+ - Intent mismatch
140
+ - Would return wrong file types
141
+
142
+ ## Common Issues
143
+
144
+ 1. **Identical Queries**: Embedding and reranking queries are the same or too similar
145
+ - **Fix**: Make reranking query focus on intent, not just rephrasing
146
+
147
+ 2. **Generic Queries**: Lack domain-specific context
148
+ - **Fix**: Add technical terms, describe behavior, include framework names
149
+
150
+ 3. **Intent Mismatch**: Queries would return docs when implementation is needed
151
+ - **Fix**: Be explicit in reranking query about what type of code is needed
152
+
153
+ 4. **Poor Differentiation**: Both queries try to do the same thing
154
+ - **Fix**: Embedding = WHAT code does, Reranking = WHY you need it
155
+
156
+ ## Architecture
157
+
158
+ ```
159
+ task.yml
160
+ ├── Runs Forge with semantic search task
161
+ ├── Captures debug output (context.json)
162
+ └── Validates with llm_judge.ts
163
+ ├── Extracts sem_search calls
164
+ ├── Calls Gemini 3 Pro with structured output
165
+ └── Returns evaluation with scores
166
+ ```
167
+
168
+ ## Extending the Eval
169
+
170
+ To add new test cases:
171
+
172
+ 1. Add to `task.yml` under `sources`:
173
+ ```yaml
174
+ - task: "Your task description"
175
+ intent: "implementation|flow_understanding|tests|documentation|debugging|modification|architecture|configuration"
176
+ expected_file_types: "comma,separated,extensions"
177
+ should_avoid: "comma,separated,extensions"
178
+ ```
179
+
180
+ 2. Choose appropriate intent:
181
+ - `implementation`: Actual code implementation
182
+ - `flow_understanding`: Understanding how things work
183
+ - `architecture`: Structural/design understanding
184
+ - `tests`: Test files and fixtures
185
+ - `documentation`: Docs, README, guides
186
+ - `debugging`: Finding bugs/issues
187
+ - `modification`: Code to modify/extend
188
+ - `configuration`: Config files, schemas
189
+
190
+ ## Future Improvements
191
+
192
+ - [ ] Add actual result verification (not just query quality)
193
+ - [ ] Measure embedding quality directly
194
+ - [ ] Test reranking effectiveness with real results
195
+ - [ ] Add precision/recall metrics
196
+ - [ ] Compare against human-labeled ground truth
197
+ - [ ] Test edge cases (very short queries, multi-lingual code, etc.)
benchmarks/evals/semantic_search_quality/llm_judge.ts ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * LLM-as-Judge for Semantic Search Quality Evaluation
5
+ *
6
+ * This script evaluates the quality of semantic search queries and results
7
+ * using Google's Gemini 3 Pro via Vertex AI as a judge. It verifies:
8
+ * 1. Query quality - Are the embedding and reranking queries well-formed?
9
+ * 2. Result relevance - Do the results match the stated intent?
10
+ * 3. File type accuracy - Are the correct file types returned?
11
+ * 4. Avoidance compliance - Are unwanted file types properly avoided?
12
+ */
13
+
14
+ import { readFileSync } from "fs";
15
+ import { vertex } from "@ai-sdk/google-vertex";
16
+ import { generateObject } from "ai";
17
+ import { z } from "zod";
18
+
19
+ // Evaluation result schema
20
+ const EvaluationSchema = z.object({
21
+ query_quality: z.object({
22
+ embedding_query_score: z
23
+ .number()
24
+ .min(0)
25
+ .max(15)
26
+ .describe("Score for the embedding query quality (0-15)"),
27
+ embedding_query_feedback: z
28
+ .string()
29
+ .describe("Specific feedback on the embedding query"),
30
+ reranking_query_score: z
31
+ .number()
32
+ .min(0)
33
+ .max(15)
34
+ .describe("Score for the reranking query quality (0-15)"),
35
+ reranking_query_feedback: z
36
+ .string()
37
+ .describe("Specific feedback on the reranking query"),
38
+ construct_keywords_score: z
39
+ .number()
40
+ .min(0)
41
+ .max(10)
42
+ .describe(
43
+ "Score for codebase construct keywords in reranking query (0-10)",
44
+ ),
45
+ construct_keywords_feedback: z
46
+ .string()
47
+ .describe(
48
+ "Feedback on use of codebase constructs (struct, trait, impl, interface, class, function, definition, implementation, declaration)",
49
+ ),
50
+ queries_differentiated: z
51
+ .boolean()
52
+ .describe(
53
+ "Whether the embedding and reranking queries are sufficiently different",
54
+ ),
55
+ }),
56
+ result_relevance: z.object({
57
+ intent_match_score: z
58
+ .number()
59
+ .min(0)
60
+ .max(25)
61
+ .describe("How well results match the stated intent (0-25)"),
62
+ intent_match_feedback: z
63
+ .string()
64
+ .describe("Explanation of intent matching"),
65
+ file_type_accuracy_score: z
66
+ .number()
67
+ .min(0)
68
+ .max(20)
69
+ .describe("Correctness of file types in results (0-20)"),
70
+ file_type_feedback: z.string().describe("Feedback on file type selection"),
71
+ avoidance_compliance_score: z
72
+ .number()
73
+ .min(0)
74
+ .max(15)
75
+ .describe("How well unwanted file types were avoided (0-15)"),
76
+ avoidance_feedback: z.string().describe("Feedback on file type avoidance"),
77
+ }),
78
+ overall: z.object({
79
+ passed: z
80
+ .boolean()
81
+ .describe("Whether the search quality passes the evaluation"),
82
+ total_score: z
83
+ .number()
84
+ .min(0)
85
+ .max(100)
86
+ .describe("Total score (0-100)"),
87
+ summary: z
88
+ .string()
89
+ .describe("Brief summary of the evaluation (2-3 sentences)"),
90
+ critical_issues: z
91
+ .array(z.string())
92
+ .describe("List of critical issues that caused failure"),
93
+ }),
94
+ });
95
+
96
+ type Evaluation = z.infer<typeof EvaluationSchema>;
97
+
98
+ interface ToolCall {
99
+ id: string;
100
+ type: string;
101
+ function: {
102
+ name: string;
103
+ arguments: string;
104
+ };
105
+ }
106
+
107
+ interface Message {
108
+ tool_calls?: ToolCall[];
109
+ }
110
+
111
+ interface ContextData {
112
+ messages: Message[];
113
+ }
114
+
115
+ interface Args {
116
+ context: string;
117
+ intent: string;
118
+ expectedFileTypes: string;
119
+ shouldAvoid: string;
120
+ }
121
+
122
+ function parseArgs(): Args {
123
+ const args = process.argv.slice(2);
124
+ const parsed: Record<string, string> = {};
125
+
126
+ for (let i = 0; i < args.length; i += 2) {
127
+ const key = args[i]?.replace(/^--/, "").replace(/-/g, "_");
128
+ const value = args[i + 1];
129
+ if (key && value) {
130
+ parsed[key] = value;
131
+ }
132
+ }
133
+
134
+ if (
135
+ !parsed.context ||
136
+ !parsed.intent ||
137
+ !parsed.expected_file_types ||
138
+ !parsed.should_avoid
139
+ ) {
140
+ console.error("Missing required arguments");
141
+ console.error(
142
+ "Usage: llm_judge.ts --context <file> --intent <intent> --expected-file-types <types> --should-avoid <types>",
143
+ );
144
+ process.exit(1);
145
+ }
146
+
147
+ return {
148
+ context: parsed.context,
149
+ intent: parsed.intent,
150
+ expectedFileTypes: parsed.expected_file_types,
151
+ shouldAvoid: parsed.should_avoid,
152
+ };
153
+ }
154
+
155
+ function extractSemanticSearchCalls(contextData: ContextData): Array<{
156
+ query: string;
157
+ use_case: string;
158
+ }> {
159
+ const searches: Array<{ query: string; use_case: string }> = [];
160
+
161
+ for (const message of contextData.messages) {
162
+ if (message.tool_calls) {
163
+ for (const toolCall of message.tool_calls) {
164
+ if (toolCall.function.name === "sem_search") {
165
+ try {
166
+ const args = JSON.parse(toolCall.function.arguments);
167
+ if (args.queries && Array.isArray(args.queries)) {
168
+ for (const q of args.queries) {
169
+ if (q.query && q.use_case) {
170
+ searches.push({
171
+ query: q.query,
172
+ use_case: q.use_case,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ } catch (e) {
178
+ // Skip invalid JSON
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ return searches;
186
+ }
187
+
188
+ async function evaluateWithLLM(
189
+ searches: Array<{ query: string; use_case: string }>,
190
+ intent: string,
191
+ expectedFileTypes: string,
192
+ shouldAvoid: string,
193
+ ): Promise<Evaluation> {
194
+ const prompt = `You are an expert evaluator of semantic code search quality. Your task is to evaluate the quality of semantic search queries and assess whether they would return relevant results based on the user's intent.
195
+
196
+ ## User's Intent
197
+ The user wants to: ${intent}
198
+
199
+ ## Expected File Types
200
+ The search should primarily return files of these types: ${expectedFileTypes}
201
+
202
+ ## File Types to Avoid
203
+ The search should avoid returning files of these types: ${shouldAvoid}
204
+
205
+ ## Semantic Search Queries Generated
206
+ ${searches.map((s, i) => `### Query ${i + 1}
207
+ **Embedding Query (for vector search):** ${s.query}
208
+ **Reranking Query (for result filtering):** ${s.use_case}
209
+ `).join("\n")}
210
+
211
+ ## Evaluation Criteria
212
+
213
+ ### Query Quality (50 points)
214
+ 1. **Embedding Query (15 points):**
215
+ - Contains domain-specific terms and technical context
216
+ - Describes behavior and functionality, not meta-descriptions
217
+ - Is complete and descriptive, not overly generic
218
+ - Would generate embeddings that match similar code
219
+
220
+ 2. **Reranking Query (15 points):**
221
+ - Expresses the specific intent and use case
222
+ - Provides context about WHY the code is needed
223
+ - Specifies whether implementation/docs/tests are needed
224
+ - Is different from the embedding query (not verbatim)
225
+
226
+ 3. **Codebase Construct Keywords (10 points):**
227
+ - **CRITICAL**: When looking for code (not documentation), the reranking query MUST include specific construct keywords
228
+ - These keywords get HIGH WEIGHTAGE in reranking: struct, trait, impl, interface, class, function, definition, implementation, declaration, type
229
+ - Examples of GOOD use_case queries:
230
+ * "I need the struct definition for User authentication"
231
+ * "Show me the trait implementation for DatabaseConnection"
232
+ * "Find the function implementation that handles file patching"
233
+ * "I need the type declarations and interface definitions for the tool registry"
234
+ - Examples of BAD use_case queries (missing construct keywords):
235
+ * "I need code that handles authentication" (missing: struct/trait/impl/function)
236
+ * "Show me the database logic" (missing: trait/impl/function)
237
+ * "Find file patching code" (missing: function/impl/struct)
238
+ - **Scoring**:
239
+ * 10 points: Multiple specific construct keywords (struct, trait, impl, function, etc.)
240
+ * 7-9 points: At least one specific construct keyword
241
+ * 4-6 points: Generic keywords (code, implementation) without specifics
242
+ * 0-3 points: No construct keywords at all
243
+ - **Exception**: Documentation/config intents don't need construct keywords
244
+
245
+ 4. **Query Differentiation (10 points):**
246
+ - The two queries serve different purposes
247
+ - Embedding query focuses on WHAT (semantic similarity)
248
+ - Reranking query focuses on INTENT + CONSTRUCTS (contextual relevance + code structure)
249
+
250
+ ### Result Relevance (50 points)
251
+ Based on the queries, evaluate whether they would return appropriate results:
252
+
253
+ 1. **Intent Matching (25 points):**
254
+ - Would the queries find code matching the user's intent?
255
+ - For "implementation" intent: would it find actual code, not docs?
256
+ - For "documentation" intent: would it find docs, not code?
257
+ - For "tests" intent: would it find test files?
258
+ - For "flow_understanding": would it find the relevant code flow?
259
+ - For "debugging": would it find the problematic areas?
260
+ - For "modification": would it find the right code to modify?
261
+
262
+ 2. **File Type Accuracy (20 points):**
263
+ - Would the queries naturally return the expected file types?
264
+ - Are the queries specific enough to filter for the right extensions?
265
+
266
+ 3. **Avoidance Compliance (15 points):**
267
+ - Would the queries avoid returning the unwanted file types?
268
+ - Is the intent clear enough to prevent irrelevant results?
269
+
270
+ ## Scoring Guidelines
271
+ - **9-10:** Excellent - Professional quality, no significant issues
272
+ - **7-8:** Good - Minor improvements possible but functional
273
+ - **5-6:** Adequate - Some issues but would work in most cases
274
+ - **3-4:** Poor - Significant issues that impact effectiveness
275
+ - **0-2:** Failing - Critical problems, would not work as intended
276
+
277
+ ## Pass/Fail Criteria
278
+ - **PASS:** Total score >= 70/100 AND no critical issues
279
+ - **FAIL:** Total score < 70/100 OR has critical issues
280
+
281
+ Critical issues include:
282
+ - Embedding and reranking queries are identical or nearly identical
283
+ - Query is too generic to return relevant results
284
+ - Intent mismatch is severe (e.g., asking for implementation but would return docs)
285
+ - Would definitely return wrong file types for the intent
286
+
287
+ Please evaluate these queries and provide detailed, constructive feedback.`;
288
+
289
+ try {
290
+ // @ts-ignore - Type instantiation depth issue with complex Zod schemas
291
+ const result = await generateObject({
292
+ model: vertex("gemini-3-pro-preview"),
293
+ schema: EvaluationSchema,
294
+ prompt,
295
+ temperature: 0.3, // Lower temperature for more consistent evaluations
296
+ });
297
+
298
+ return result.object as Evaluation;
299
+ } catch (error) {
300
+ console.error("Error calling Gemini API:", error);
301
+ throw error;
302
+ }
303
+ }
304
+
305
+ function formatEvaluation(evaluation: Evaluation): string {
306
+ const lines: string[] = [];
307
+
308
+ lines.push("=== SEMANTIC SEARCH QUALITY EVALUATION ===\n");
309
+
310
+ // Query Quality
311
+ lines.push("## Query Quality");
312
+ lines.push(
313
+ `Embedding Query: ${evaluation.query_quality.embedding_query_score}/15`,
314
+ );
315
+ lines.push(` ${evaluation.query_quality.embedding_query_feedback}`);
316
+ lines.push(
317
+ `Reranking Query: ${evaluation.query_quality.reranking_query_score}/15`,
318
+ );
319
+ lines.push(` ${evaluation.query_quality.reranking_query_feedback}`);
320
+ lines.push(
321
+ `Construct Keywords: ${evaluation.query_quality.construct_keywords_score}/10`,
322
+ );
323
+ lines.push(` ${evaluation.query_quality.construct_keywords_feedback}`);
324
+ lines.push(
325
+ `Queries Differentiated: ${evaluation.query_quality.queries_differentiated ? "✓ Yes" : "✗ No"}`,
326
+ );
327
+ lines.push("");
328
+
329
+ // Result Relevance
330
+ lines.push("## Result Relevance");
331
+ lines.push(
332
+ `Intent Match: ${evaluation.result_relevance.intent_match_score}/25`,
333
+ );
334
+ lines.push(` ${evaluation.result_relevance.intent_match_feedback}`);
335
+ lines.push(
336
+ `File Type Accuracy: ${evaluation.result_relevance.file_type_accuracy_score}/20`,
337
+ );
338
+ lines.push(` ${evaluation.result_relevance.file_type_feedback}`);
339
+ lines.push(
340
+ `Avoidance Compliance: ${evaluation.result_relevance.avoidance_compliance_score}/15`,
341
+ );
342
+ lines.push(` ${evaluation.result_relevance.avoidance_feedback}`);
343
+ lines.push("");
344
+
345
+ // Overall
346
+ lines.push("## Overall Assessment");
347
+ lines.push(`Total Score: ${evaluation.overall.total_score}/100`);
348
+ lines.push(`Status: ${evaluation.overall.passed ? "✓ PASS" : "✗ FAIL"}`);
349
+ lines.push(`Summary: ${evaluation.overall.summary}`);
350
+
351
+ if (evaluation.overall.critical_issues.length > 0) {
352
+ lines.push("\n## Critical Issues:");
353
+ for (const issue of evaluation.overall.critical_issues) {
354
+ lines.push(` - ${issue}`);
355
+ }
356
+ }
357
+
358
+ return lines.join("\n");
359
+ }
360
+
361
+ async function main() {
362
+ const args = parseArgs();
363
+
364
+ // Read context file
365
+ let contextData: ContextData;
366
+ try {
367
+ const contextContent = readFileSync(args.context, "utf-8");
368
+ contextData = JSON.parse(contextContent);
369
+ } catch (error) {
370
+ console.error(`Error reading context file: ${error}`);
371
+ process.exit(1);
372
+ }
373
+
374
+ // Extract semantic search calls
375
+ const searches = extractSemanticSearchCalls(contextData);
376
+
377
+ if (searches.length === 0) {
378
+ console.error("No semantic search calls found in context");
379
+ process.exit(1);
380
+ }
381
+
382
+ // Evaluate with LLM
383
+ const evaluation = await evaluateWithLLM(
384
+ searches,
385
+ args.intent,
386
+ args.expectedFileTypes,
387
+ args.shouldAvoid,
388
+ );
389
+
390
+ // Output results
391
+ console.log(formatEvaluation(evaluation));
392
+
393
+ // Exit with appropriate code
394
+ process.exit(evaluation.overall.passed ? 0 : 1);
395
+ }
396
+
397
+ main().catch((error) => {
398
+ console.error("Unexpected error:", error);
399
+ process.exit(1);
400
+ });
benchmarks/evals/semantic_search_quality/run_eval.sh ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ CONTEXT_FILE="$1"
6
+ if [ -z "$CONTEXT_FILE" ]; then
7
+ echo "Usage: $0 <context_file>"
8
+ exit 1
9
+ fi
10
+
11
+ echo "=========================================="
12
+ echo "Running Semantic Search Quality Eval"
13
+ echo "=========================================="
14
+ echo ""
15
+
16
+ # Step 1: Check if sem_search was used
17
+ echo "Step 1: Checking for sem_search tool usage..."
18
+ if cat "$CONTEXT_FILE" | jq -e '[.messages[]?.tool_calls[]? | select(.function.name == "sem_search")] | any' > /dev/null 2>&1; then
19
+ echo "✓ sem_search tool found in context"
20
+ else
21
+ echo "✗ FAILED: sem_search tool not used"
22
+ exit 1
23
+ fi
24
+
25
+ # Step 2: Extract intent and queries
26
+ echo ""
27
+ echo "Step 2: Extracting task intent and queries..."
28
+ INTENT=$(cat "$CONTEXT_FILE" | jq -r '.task.intent // "implementation"')
29
+ echo " Intent: $INTENT"
30
+
31
+ # Extract expected file types and avoid list from task
32
+ EXPECTED_TYPES=$(cat "$CONTEXT_FILE" | jq -r '.task.expected_file_types // [] | join(",")')
33
+ SHOULD_AVOID=$(cat "$CONTEXT_FILE" | jq -r '.task.should_avoid // [] | join(",")')
34
+
35
+ # Extract queries from tool calls
36
+ QUERIES=$(cat "$CONTEXT_FILE" | jq -r '[.messages[]?.tool_calls[]? | select(.function.name == "sem_search") | .function.arguments] | .[0]' 2>/dev/null || echo '{}')
37
+ echo " Queries extracted: $(echo "$QUERIES" | jq -r '.queries | length // 0') query pairs"
38
+
39
+ # Step 3: Check authentication
40
+ echo ""
41
+ echo "Step 3: Checking Google Cloud authentication..."
42
+ if gcloud auth application-default print-access-token > /dev/null 2>&1; then
43
+ echo "✓ Google Cloud authentication available"
44
+ RUN_LLM_JUDGE=true
45
+ elif [ -n "$GOOGLE_APPLICATION_CREDENTIALS" ] && [ -f "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
46
+ echo "✓ Service account credentials found"
47
+ RUN_LLM_JUDGE=true
48
+ else
49
+ echo "⚠ Google Cloud authentication not configured"
50
+ echo " Skipping LLM judge evaluation"
51
+ echo " To enable: run 'gcloud auth application-default login'"
52
+ RUN_LLM_JUDGE=false
53
+ fi
54
+
55
+ # Step 4: Run LLM judge (conditional)
56
+ echo ""
57
+ if [ "$RUN_LLM_JUDGE" = true ]; then
58
+ echo "Step 4: Running LLM judge evaluation..."
59
+
60
+ # Build command with parameters (use empty strings if not set)
61
+ if [ -z "$EXPECTED_TYPES" ]; then
62
+ EXPECTED_TYPES=""
63
+ fi
64
+
65
+ if [ -z "$SHOULD_AVOID" ]; then
66
+ SHOULD_AVOID=""
67
+ fi
68
+
69
+ CMD="npx tsx llm_judge.ts --context \"$CONTEXT_FILE\" --intent \"$INTENT\" --expected-file-types \"$EXPECTED_TYPES\" --should-avoid \"$SHOULD_AVOID\""
70
+
71
+ eval $CMD
72
+
73
+ if [ $? -eq 0 ]; then
74
+ echo ""
75
+ echo "=========================================="
76
+ echo "✓ EVALUATION PASSED"
77
+ echo "=========================================="
78
+ exit 0
79
+ else
80
+ echo ""
81
+ echo "=========================================="
82
+ echo "✗ EVALUATION FAILED"
83
+ echo "=========================================="
84
+ exit 1
85
+ fi
86
+ else
87
+ echo "Step 4: Skipped (authentication required)"
88
+ echo ""
89
+ echo "=========================================="
90
+ echo "✓ VALIDATION PASSED (LLM judge skipped)"
91
+ echo "=========================================="
92
+ echo ""
93
+ echo "Note: Full evaluation requires Google Cloud auth"
94
+ exit 0
95
+ fi
benchmarks/evals/semantic_search_quality/test_context.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "messages": [
3
+ {
4
+ "role": "user",
5
+ "content": "Find the retry mechanism with exponential backoff implementation"
6
+ },
7
+ {
8
+ "role": "assistant",
9
+ "content": "I'll search for the retry mechanism implementation.",
10
+ "tool_calls": [
11
+ {
12
+ "id": "call_1",
13
+ "type": "function",
14
+ "function": {
15
+ "name": "sem_search",
16
+ "arguments": "{\"queries\":[{\"query\":\"retry mechanism with exponential backoff for API calls handling network failures\",\"use_case\":\"I need to find the implementation code for retry logic with backoff, not documentation or test files\"}]}"
17
+ }
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ }
benchmarks/evals/semantic_search_quality/test_queries.ts ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * Manual Query Tester for Semantic Search
5
+ *
6
+ * This script helps developers test and improve their semantic search queries
7
+ * before committing them to the codebase using Gemini 3 Pro via Vertex AI.
8
+ */
9
+
10
+ import { vertex } from "@ai-sdk/google-vertex";
11
+ import { generateObject } from "ai";
12
+ import { z } from "zod";
13
+ import * as readline from "readline";
14
+
15
+ const QueryEvaluationSchema = z.object({
16
+ embedding_query: z.object({
17
+ score: z.number().min(0).max(10),
18
+ strengths: z.array(z.string()),
19
+ weaknesses: z.array(z.string()),
20
+ suggestions: z.array(z.string()),
21
+ }),
22
+ reranking_query: z.object({
23
+ score: z.number().min(0).max(10),
24
+ strengths: z.array(z.string()),
25
+ weaknesses: z.array(z.string()),
26
+ suggestions: z.array(z.string()),
27
+ }),
28
+ differentiation: z.object({
29
+ are_different: z.boolean(),
30
+ explanation: z.string(),
31
+ suggestion: z.string().optional(),
32
+ }),
33
+ overall_recommendation: z.string(),
34
+ });
35
+
36
+ async function evaluateQueries(
37
+ embeddingQuery: string,
38
+ rerankingQuery: string,
39
+ ): Promise<z.infer<typeof QueryEvaluationSchema>> {
40
+ const prompt = `You are an expert in semantic code search. Evaluate these two queries:
41
+
42
+ **Embedding Query (for vector search):**
43
+ ${embeddingQuery}
44
+
45
+ **Reranking Query (for result filtering):**
46
+ ${rerankingQuery}
47
+
48
+ Provide detailed, constructive feedback on:
49
+ 1. The quality of each query
50
+ 2. Whether they are sufficiently different
51
+ 3. How to improve them
52
+
53
+ Scoring (0-10):
54
+ - 9-10: Excellent
55
+ - 7-8: Good
56
+ - 5-6: Adequate
57
+ - 3-4: Poor
58
+ - 0-2: Failing
59
+
60
+ Focus on practical, actionable suggestions.`;
61
+
62
+ // @ts-ignore - Type instantiation depth issue with complex Zod schemas
63
+ const result = await generateObject({
64
+ model: vertex("gemini-3-pro-preview"),
65
+ schema: QueryEvaluationSchema,
66
+ prompt,
67
+ temperature: 0.3,
68
+ });
69
+ return result.object as z.infer<typeof QueryEvaluationSchema>;
70
+ }
71
+
72
+ function formatEvaluation(evaluation: z.infer<typeof QueryEvaluationSchema>): string {
73
+ const lines: string[] = [];
74
+
75
+ lines.push("\n╔═══════════════════════════════════════════════════════════╗");
76
+ lines.push("║ SEMANTIC SEARCH QUERY EVALUATION ║");
77
+ lines.push("╚═══════════════════════════════════════════════════════════╝\n");
78
+
79
+ // Embedding Query
80
+ lines.push("📊 EMBEDDING QUERY ANALYSIS");
81
+ lines.push(` Score: ${evaluation.embedding_query.score}/10 ${getScoreEmoji(evaluation.embedding_query.score)}\n`);
82
+
83
+ if (evaluation.embedding_query.strengths.length > 0) {
84
+ lines.push(" ✅ Strengths:");
85
+ for (const strength of evaluation.embedding_query.strengths) {
86
+ lines.push(` • ${strength}`);
87
+ }
88
+ lines.push("");
89
+ }
90
+
91
+ if (evaluation.embedding_query.weaknesses.length > 0) {
92
+ lines.push(" ⚠️ Weaknesses:");
93
+ for (const weakness of evaluation.embedding_query.weaknesses) {
94
+ lines.push(` • ${weakness}`);
95
+ }
96
+ lines.push("");
97
+ }
98
+
99
+ if (evaluation.embedding_query.suggestions.length > 0) {
100
+ lines.push(" 💡 Suggestions:");
101
+ for (const suggestion of evaluation.embedding_query.suggestions) {
102
+ lines.push(` • ${suggestion}`);
103
+ }
104
+ lines.push("");
105
+ }
106
+
107
+ // Reranking Query
108
+ lines.push("📊 RERANKING QUERY ANALYSIS");
109
+ lines.push(` Score: ${evaluation.reranking_query.score}/10 ${getScoreEmoji(evaluation.reranking_query.score)}\n`);
110
+
111
+ if (evaluation.reranking_query.strengths.length > 0) {
112
+ lines.push(" ✅ Strengths:");
113
+ for (const strength of evaluation.reranking_query.strengths) {
114
+ lines.push(` • ${strength}`);
115
+ }
116
+ lines.push("");
117
+ }
118
+
119
+ if (evaluation.reranking_query.weaknesses.length > 0) {
120
+ lines.push(" ⚠️ Weaknesses:");
121
+ for (const weakness of evaluation.reranking_query.weaknesses) {
122
+ lines.push(` • ${weakness}`);
123
+ }
124
+ lines.push("");
125
+ }
126
+
127
+ if (evaluation.reranking_query.suggestions.length > 0) {
128
+ lines.push(" 💡 Suggestions:");
129
+ for (const suggestion of evaluation.reranking_query.suggestions) {
130
+ lines.push(` • ${suggestion}`);
131
+ }
132
+ lines.push("");
133
+ }
134
+
135
+ // Differentiation
136
+ lines.push("🔄 QUERY DIFFERENTIATION");
137
+ lines.push(` ${evaluation.differentiation.are_different ? "✅" : "❌"} Queries are ${evaluation.differentiation.are_different ? "sufficiently different" : "too similar"}`);
138
+ lines.push(` ${evaluation.differentiation.explanation}\n`);
139
+
140
+ if (evaluation.differentiation.suggestion) {
141
+ lines.push(` 💡 ${evaluation.differentiation.suggestion}\n`);
142
+ }
143
+
144
+ // Overall
145
+ lines.push("🎯 OVERALL RECOMMENDATION");
146
+ lines.push(` ${evaluation.overall_recommendation}\n`);
147
+
148
+ return lines.join("\n");
149
+ }
150
+
151
+ function getScoreEmoji(score: number): string {
152
+ if (score >= 9) return "🌟";
153
+ if (score >= 7) return "👍";
154
+ if (score >= 5) return "👌";
155
+ if (score >= 3) return "⚠️";
156
+ return "❌";
157
+ }
158
+
159
+ async function promptUser(question: string): Promise<string> {
160
+ const rl = readline.createInterface({
161
+ input: process.stdin,
162
+ output: process.stdout,
163
+ });
164
+
165
+ return new Promise((resolve) => {
166
+ rl.question(question, (answer) => {
167
+ rl.close();
168
+ resolve(answer.trim());
169
+ });
170
+ });
171
+ }
172
+
173
+ async function main() {
174
+ console.log("\n╔═══════════════════════════════════════════════════════════╗");
175
+ console.log("║ SEMANTIC SEARCH QUERY TESTER ║");
176
+ console.log("║ Test and improve your queries with AI feedback ║");
177
+ console.log("╚═══════════════════════════════════════════════════════════╝\n");
178
+
179
+ // Get queries from user
180
+ const embeddingQuery = await promptUser(
181
+ "Enter your embedding query (for vector search):\n> ",
182
+ );
183
+
184
+ if (!embeddingQuery) {
185
+ console.error("Embedding query is required");
186
+ process.exit(1);
187
+ }
188
+
189
+ console.log("");
190
+ const rerankingQuery = await promptUser(
191
+ "Enter your reranking query (for result filtering):\n> ",
192
+ );
193
+
194
+ if (!rerankingQuery) {
195
+ console.error("Reranking query is required");
196
+ process.exit(1);
197
+ }
198
+
199
+ console.log("\n🔄 Evaluating your queries...\n");
200
+
201
+ try {
202
+ const evaluation = await evaluateQueries(embeddingQuery, rerankingQuery);
203
+ console.log(formatEvaluation(evaluation));
204
+ } catch (error) {
205
+ console.error("Error evaluating queries:", error);
206
+ process.exit(1);
207
+ }
208
+ }
209
+
210
+ // Check if running in interactive mode or with args
211
+ const args = process.argv.slice(2);
212
+ if (args.length >= 2) {
213
+ // Non-interactive mode
214
+ const embeddingQuery = args[0];
215
+ const rerankingQuery = args[1];
216
+
217
+ console.log("\n🔄 Evaluating your queries...\n");
218
+ evaluateQueries(embeddingQuery!, rerankingQuery!)
219
+ .then((evaluation) => {
220
+ console.log(formatEvaluation(evaluation));
221
+ })
222
+ .catch((error) => {
223
+ console.error("Error evaluating queries:", error);
224
+ process.exit(1);
225
+ });
226
+ } else {
227
+ // Interactive mode
228
+ main().catch((error) => {
229
+ console.error("Unexpected error:", error);
230
+ process.exit(1);
231
+ });
232
+ }
benchmarks/evals/suggest/task.yml ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ run:
2
+ - mkdir -p test_dir
3
+ - cd test_dir && touch file1.txt file2.py README.md
4
+ - cd test_dir && forgee --provider {{provider}} --model {{model}} suggest "{{prompt}}"
5
+
6
+ parallelism: 2
7
+ timeout: 60
8
+ early_exit: true
9
+
10
+ validations:
11
+ - name: "Should generate output (non-empty)"
12
+ type: shell
13
+ command: "test $(wc -c | awk '{print $1}') -gt 0"
14
+
15
+ - name: "Should NOT contain markdown code blocks"
16
+ type: shell
17
+ command: "! grep -qE '^```'"
18
+
19
+ - name: "Should NOT contain JSON formatting tags"
20
+ type: shell
21
+ command: "! grep -qE '\\{\\s*\"command\"\\s*:'"
22
+
23
+ sources:
24
+ - value:
25
+ - provider: "anthropic"
26
+ model: "claude-sonnet-4-5-20250929"
27
+ - provider: "open_router"
28
+ model: "z-ai/glm-4.7"
29
+ - provider: "open_router"
30
+ model: "minimax/minimax-m2.1"
31
+
32
+ - value:
33
+ - prompt: "list all files"
34
+ - prompt: "find all python files"
35
+ - prompt: "show disk usage"
36
+ - prompt: "git status"
37
+ - prompt: "count lines in all files"
38
+ - prompt: "search for TODO in code"
39
+ - prompt: "check running processes"
40
+ - prompt: "compress this directory"
41
+ - prompt: "show last 20 commits"
42
+ - prompt: "find files larger than 1MB"
43
+ - prompt: "kill process on port 3000"
44
+ - prompt: "show system memory usage"
45
+ - prompt: "download file from url"
46
+ - prompt: "create git branch"
47
+ - prompt: "watch log file changes"
benchmarks/model.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Task = {
2
+ before_run: Array<string>;
3
+ run: string | Array<string>;
4
+ parallelism?: number;
5
+ timeout?: number;
6
+ early_exit?: boolean;
7
+ validations?: Array<Validation>;
8
+ sources: Array<Source>;
9
+ };
10
+
11
+ export type Validation =
12
+ | {
13
+ name: string;
14
+ type: "regex";
15
+ regex: string;
16
+ }
17
+ | {
18
+ name: string;
19
+ type: "shell";
20
+ command: string;
21
+ exit_code?: number;
22
+ }
23
+ | {
24
+ name: string;
25
+ type: "llm";
26
+ evaluator: string;
27
+ criteria: Record<string, any>;
28
+ };
29
+
30
+ export type Source = { csv: string } | { cmd: string } | { value: Record<string, string>[] };
31
+
32
+
33
+ export enum TaskStatus {
34
+ Passed = "passed",
35
+ ValidationFailed = "validation_failed",
36
+ Timeout = "timeout",
37
+ Failed = "failed",
38
+ }
benchmarks/parse.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yargs from "yargs";
2
+ import { hideBin } from "yargs/helpers";
3
+ import path from "path";
4
+
5
+ export type CliArgs = {
6
+ evalName: string;
7
+ evalDir: string;
8
+ taskFile: string;
9
+ };
10
+
11
+ /**
12
+ * Parses command line arguments and resolves paths
13
+ */
14
+ export async function parseCliArgs(dirname: string): Promise<CliArgs> {
15
+ const argv = await yargs(hideBin(process.argv))
16
+ .usage("Usage: $0 <eval-name> [options]")
17
+ .command("$0 <eval-name>", "Run an evaluation")
18
+ .positional("eval-name", {
19
+ describe: "Name of the evaluation to run",
20
+ type: "string",
21
+ })
22
+ .help()
23
+ .alias("h", "help")
24
+ .parseAsync();
25
+
26
+ const evalName = argv["eval-name"];
27
+
28
+ // Ensure evalName is provided
29
+ if (!evalName) {
30
+ throw new Error("eval-name is required");
31
+ }
32
+
33
+ // Support both directory path and direct task.yml path
34
+ let evalDir: string;
35
+ let taskFile: string;
36
+
37
+ if (evalName.endsWith("task.yml") || evalName.endsWith(".yml") || evalName.endsWith(".yaml")) {
38
+ // Direct path to task file
39
+ taskFile = path.isAbsolute(evalName) ? evalName : path.join(dirname, evalName);
40
+ evalDir = path.dirname(taskFile);
41
+ } else {
42
+ // Directory path (original behavior)
43
+ evalDir = path.join(dirname, evalName);
44
+ taskFile = path.join(evalDir, "task.yml");
45
+ }
46
+
47
+ return {
48
+ evalName,
49
+ evalDir,
50
+ taskFile,
51
+ };
52
+ }
benchmarks/task-executor.ts ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { spawn } from "child_process";
4
+ import stripAnsi from "strip-ansi";
5
+ import type { Validation, Task } from "./model.js";
6
+ import { runValidations, allValidationsPassed } from "./verification.js";
7
+ import { formatTimestamp } from "./utils.js";
8
+
9
+ export type TaskExecutionResult = {
10
+ index: number;
11
+ command: string;
12
+ duration: number;
13
+ output?: string;
14
+ error?: string;
15
+ isTimeout: boolean;
16
+ earlyExit?: boolean;
17
+ };
18
+
19
+ /**
20
+ * Executes a single task command and returns the result
21
+ */
22
+ export async function executeTask(
23
+ command: string,
24
+ index: number,
25
+ logFile: string,
26
+ cwd: string,
27
+ task: Task,
28
+ context?: Record<string, string>,
29
+ append: boolean = false,
30
+ ): Promise<TaskExecutionResult> {
31
+ const startTime = Date.now();
32
+
33
+ // Create log stream for this task (append if this is not the first command)
34
+ const logStream = fs.createWriteStream(logFile, {
35
+ flags: append ? "a" : "w",
36
+ });
37
+
38
+ // Write command at the top of the log file
39
+ logStream.write(`Command: ${command}\n`);
40
+ logStream.write(`Started: ${formatTimestamp(new Date())}\n`);
41
+ logStream.write(`${"=".repeat(80)}\n\n`);
42
+
43
+ try {
44
+ // Track timeout state outside the promise
45
+ let timedOut = false;
46
+ let exitedEarly = false;
47
+
48
+ // Execute command and stream output to log file
49
+ const output = await new Promise<string>((resolve, reject) => {
50
+ const child = spawn(command, {
51
+ shell: true,
52
+ cwd: cwd,
53
+ stdio: ["ignore", "pipe", "pipe"],
54
+ });
55
+
56
+ let stdout = "";
57
+ let stderr = "";
58
+ let timeoutId: NodeJS.Timeout | null = null;
59
+
60
+ // Helper function to check validations after each write
61
+ const checkValidations = async () => {
62
+ if (exitedEarly || timedOut) return;
63
+
64
+ if (
65
+ task.early_exit &&
66
+ task.validations &&
67
+ task.validations.length > 0
68
+ ) {
69
+ const currentOutput = stdout + stderr;
70
+ if (currentOutput) {
71
+ const results = await runValidations(
72
+ currentOutput,
73
+ task.validations,
74
+ context,
75
+ );
76
+ if (allValidationsPassed(results)) {
77
+ exitedEarly = true;
78
+ if (timeoutId) clearTimeout(timeoutId);
79
+ if (logStream.writable) {
80
+ logStream.write(
81
+ `\n${"=".repeat(80)}\nEarly exit: All validations passed\nKilling process...\n`,
82
+ () => {
83
+ logStream.end();
84
+ },
85
+ );
86
+ }
87
+ child.kill("SIGTERM");
88
+ resolve(currentOutput);
89
+ }
90
+ }
91
+ }
92
+ };
93
+
94
+ // Set up timeout if configured
95
+ if (task.timeout) {
96
+ timeoutId = setTimeout(() => {
97
+ timedOut = true;
98
+ if (logStream.writable) {
99
+ logStream.write(`\n${"=".repeat(80)}\n`);
100
+ logStream.write(`Timeout: ${task.timeout}s exceeded\n`);
101
+ logStream.write(`Killing process...\n`);
102
+ logStream.end();
103
+ }
104
+ child.kill("SIGKILL");
105
+ // Resolve with captured output so far
106
+ resolve(stdout + stderr);
107
+ }, task.timeout * 1000);
108
+ }
109
+
110
+ // Stream stdout to both log file and capture for validation
111
+ child.stdout?.on("data", (data) => {
112
+ const text = data.toString();
113
+ stdout += text;
114
+ if (logStream.writable) {
115
+ logStream.write(stripAnsi(text));
116
+ }
117
+ checkValidations();
118
+ });
119
+
120
+ // Stream stderr to both log file and capture for validation
121
+ child.stderr?.on("data", (data) => {
122
+ const text = data.toString();
123
+ stderr += text;
124
+ if (logStream.writable) {
125
+ logStream.write(stripAnsi(text));
126
+ }
127
+ checkValidations();
128
+ });
129
+
130
+ child.on("close", (code) => {
131
+ if (timeoutId) clearTimeout(timeoutId);
132
+
133
+ // Don't log or resolve if already timed out or exited early
134
+ if (timedOut || exitedEarly) return;
135
+
136
+ logStream.write(`\n${"=".repeat(80)}\n`);
137
+ logStream.write(`Finished: ${formatTimestamp(new Date())}\n`);
138
+ logStream.write(`Exit Code: ${code}\n`);
139
+ logStream.end();
140
+
141
+ if (code === 0) {
142
+ resolve(stdout + stderr);
143
+ } else {
144
+ reject(new Error(`Command failed with exit code ${code}`));
145
+ }
146
+ });
147
+
148
+ child.on("error", (err) => {
149
+ if (timeoutId) clearTimeout(timeoutId);
150
+
151
+ // Don't log if already timed out or exited early
152
+ if (timedOut || exitedEarly) return;
153
+
154
+ logStream.write(`\nError: ${err.message}\n`);
155
+ logStream.end();
156
+ reject(err);
157
+ });
158
+ });
159
+
160
+ const duration = Date.now() - startTime;
161
+
162
+ return {
163
+ index,
164
+ command,
165
+ duration,
166
+ output,
167
+ isTimeout: timedOut,
168
+ earlyExit: exitedEarly,
169
+ };
170
+ } catch (error) {
171
+ const duration = Date.now() - startTime;
172
+ const errorMessage =
173
+ error instanceof Error ? error.message : "Command failed";
174
+
175
+ return {
176
+ index,
177
+ command,
178
+ duration,
179
+ error: errorMessage,
180
+ isTimeout: false,
181
+ earlyExit: false,
182
+ };
183
+ }
184
+ }
benchmarks/tsconfig.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ "rootDir": ".",
6
+ "outDir": "dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "nodenext",
11
+ "target": "esnext",
12
+ "types": [],
13
+ // For nodejs:
14
+ // "lib": ["esnext"],
15
+ // "types": ["node"],
16
+ // and npm install -D @types/node
17
+
18
+ // Other Outputs
19
+ "sourceMap": true,
20
+ "declaration": true,
21
+ "declarationMap": true,
22
+
23
+ // Stricter Typechecking Options
24
+ "noUncheckedIndexedAccess": true,
25
+ "exactOptionalPropertyTypes": true,
26
+
27
+ // Style Options
28
+ // "noImplicitReturns": true,
29
+ // "noImplicitOverride": true,
30
+ // "noUnusedLocals": true,
31
+ // "noUnusedParameters": true,
32
+ // "noFallthroughCasesInSwitch": true,
33
+ // "noPropertyAccessFromIndexSignature": true,
34
+
35
+ // Recommended Options
36
+ "strict": true,
37
+ "jsx": "react-jsx",
38
+ "verbatimModuleSyntax": true,
39
+ "isolatedModules": true,
40
+ "noUncheckedSideEffectImports": true,
41
+ "moduleDetection": "force",
42
+ "skipLibCheck": true,
43
+
44
+ "esModuleInterop": true,
45
+ "resolveJsonModule": true,
46
+ }
47
+ }