orch-code 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +624 -0
- package/cmd/apply.go +111 -0
- package/cmd/auth.go +393 -0
- package/cmd/auth_test.go +100 -0
- package/cmd/diff.go +57 -0
- package/cmd/doctor.go +149 -0
- package/cmd/explain.go +192 -0
- package/cmd/explain_test.go +62 -0
- package/cmd/init.go +100 -0
- package/cmd/interactive.go +1372 -0
- package/cmd/interactive_input.go +45 -0
- package/cmd/interactive_input_test.go +55 -0
- package/cmd/logs.go +72 -0
- package/cmd/model.go +84 -0
- package/cmd/plan.go +149 -0
- package/cmd/provider.go +189 -0
- package/cmd/provider_model_doctor_test.go +91 -0
- package/cmd/root.go +67 -0
- package/cmd/run.go +123 -0
- package/cmd/run_engine.go +208 -0
- package/cmd/run_engine_test.go +30 -0
- package/cmd/session.go +589 -0
- package/cmd/session_helpers.go +54 -0
- package/cmd/session_integration_test.go +30 -0
- package/cmd/session_list_current_test.go +87 -0
- package/cmd/session_messages_test.go +163 -0
- package/cmd/session_runs_test.go +68 -0
- package/cmd/sprint1_integration_test.go +119 -0
- package/cmd/stats.go +173 -0
- package/cmd/stats_test.go +71 -0
- package/cmd/version.go +4 -0
- package/go.mod +45 -0
- package/go.sum +108 -0
- package/internal/agents/agent.go +31 -0
- package/internal/agents/coder.go +167 -0
- package/internal/agents/planner.go +155 -0
- package/internal/agents/reviewer.go +118 -0
- package/internal/agents/runtime.go +25 -0
- package/internal/agents/runtime_test.go +77 -0
- package/internal/auth/account.go +78 -0
- package/internal/auth/oauth.go +523 -0
- package/internal/auth/store.go +287 -0
- package/internal/confidence/policy.go +174 -0
- package/internal/confidence/policy_test.go +71 -0
- package/internal/confidence/scorer.go +253 -0
- package/internal/confidence/scorer_test.go +83 -0
- package/internal/config/config.go +331 -0
- package/internal/config/config_defaults_test.go +138 -0
- package/internal/execution/contract_builder.go +160 -0
- package/internal/execution/contract_builder_test.go +68 -0
- package/internal/execution/plan_compliance.go +161 -0
- package/internal/execution/plan_compliance_test.go +71 -0
- package/internal/execution/retry_directive.go +132 -0
- package/internal/execution/scope_guard.go +69 -0
- package/internal/logger/logger.go +120 -0
- package/internal/models/contracts_test.go +100 -0
- package/internal/models/models.go +269 -0
- package/internal/orchestrator/orchestrator.go +701 -0
- package/internal/orchestrator/orchestrator_retry_test.go +135 -0
- package/internal/orchestrator/review_engine_test.go +50 -0
- package/internal/orchestrator/state.go +42 -0
- package/internal/orchestrator/test_classifier_test.go +68 -0
- package/internal/patch/applier.go +131 -0
- package/internal/patch/applier_test.go +25 -0
- package/internal/patch/parser.go +89 -0
- package/internal/patch/patch.go +60 -0
- package/internal/patch/summary.go +30 -0
- package/internal/patch/validator.go +104 -0
- package/internal/planning/normalizer.go +416 -0
- package/internal/planning/normalizer_test.go +64 -0
- package/internal/providers/errors.go +35 -0
- package/internal/providers/openai/client.go +498 -0
- package/internal/providers/openai/client_test.go +187 -0
- package/internal/providers/provider.go +47 -0
- package/internal/providers/registry.go +32 -0
- package/internal/providers/registry_test.go +57 -0
- package/internal/providers/router.go +52 -0
- package/internal/providers/state.go +114 -0
- package/internal/providers/state_test.go +64 -0
- package/internal/repo/analyzer.go +188 -0
- package/internal/repo/context.go +83 -0
- package/internal/review/engine.go +267 -0
- package/internal/review/engine_test.go +103 -0
- package/internal/runstore/store.go +137 -0
- package/internal/runstore/store_test.go +59 -0
- package/internal/runtime/lock.go +150 -0
- package/internal/runtime/lock_test.go +57 -0
- package/internal/session/compaction.go +260 -0
- package/internal/session/compaction_test.go +36 -0
- package/internal/session/service.go +117 -0
- package/internal/session/service_test.go +113 -0
- package/internal/storage/storage.go +1498 -0
- package/internal/storage/storage_test.go +413 -0
- package/internal/testing/classifier.go +80 -0
- package/internal/testing/classifier_test.go +36 -0
- package/internal/tools/command.go +160 -0
- package/internal/tools/command_test.go +56 -0
- package/internal/tools/file.go +111 -0
- package/internal/tools/git.go +77 -0
- package/internal/tools/invalid_params_test.go +36 -0
- package/internal/tools/policy.go +98 -0
- package/internal/tools/policy_test.go +36 -0
- package/internal/tools/registry_test.go +52 -0
- package/internal/tools/result.go +30 -0
- package/internal/tools/search.go +86 -0
- package/internal/tools/tool.go +94 -0
- package/main.go +9 -0
- package/npm/orch.js +25 -0
- package/package.json +41 -0
- package/scripts/changelog.js +20 -0
- package/scripts/check-release-version.js +21 -0
- package/scripts/lib/release-utils.js +223 -0
- package/scripts/postinstall.js +157 -0
- package/scripts/release.js +52 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
package confidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type Scorer struct{}
|
|
11
|
+
|
|
12
|
+
func New() *Scorer {
|
|
13
|
+
return &Scorer{}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func (s *Scorer) Score(state *models.RunState) *models.ConfidenceReport {
|
|
17
|
+
if state == nil {
|
|
18
|
+
return nil
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
reasons := []string{}
|
|
22
|
+
warnings := []string{}
|
|
23
|
+
|
|
24
|
+
planScore := s.planCompleteness(state, &reasons, &warnings)
|
|
25
|
+
scopeScore := s.scopeCompliance(state, &reasons, &warnings)
|
|
26
|
+
validationScore := s.validationQuality(state, &reasons, &warnings)
|
|
27
|
+
testScore := s.testQuality(state, &reasons, &warnings)
|
|
28
|
+
reviewScore := s.reviewQuality(state, &reasons, &warnings)
|
|
29
|
+
retryPenalty := s.retryPenalty(state, &warnings)
|
|
30
|
+
|
|
31
|
+
score := planScore*0.10 + scopeScore*0.20 + validationScore*0.20 + testScore*0.25 + reviewScore*0.20 - retryPenalty*0.05
|
|
32
|
+
if score < 0 {
|
|
33
|
+
score = 0
|
|
34
|
+
}
|
|
35
|
+
if score > 1 {
|
|
36
|
+
score = 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
band := confidenceBand(score)
|
|
40
|
+
if band == "high" {
|
|
41
|
+
reasons = append(reasons, "confidence is high because plan, validation, tests, and review signals are aligned")
|
|
42
|
+
}
|
|
43
|
+
if band == "low" || band == "very_low" {
|
|
44
|
+
warnings = append(warnings, "confidence is below the preferred threshold for hands-off trust")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return &models.ConfidenceReport{
|
|
48
|
+
Score: round2(score),
|
|
49
|
+
Band: band,
|
|
50
|
+
Reasons: uniqueNonEmpty(reasons),
|
|
51
|
+
Warnings: uniqueNonEmpty(warnings),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func (s *Scorer) planCompleteness(state *models.RunState, reasons, warnings *[]string) float64 {
|
|
56
|
+
if state.Plan == nil {
|
|
57
|
+
*warnings = append(*warnings, "structured plan is missing")
|
|
58
|
+
return 0.2
|
|
59
|
+
}
|
|
60
|
+
score := 0.3
|
|
61
|
+
if strings.TrimSpace(state.Plan.Summary) != "" {
|
|
62
|
+
score += 0.2
|
|
63
|
+
}
|
|
64
|
+
if len(state.Plan.FilesToInspect) > 0 {
|
|
65
|
+
score += 0.15
|
|
66
|
+
}
|
|
67
|
+
if len(state.Plan.FilesToModify) > 0 {
|
|
68
|
+
score += 0.15
|
|
69
|
+
}
|
|
70
|
+
if len(state.Plan.AcceptanceCriteria) > 0 {
|
|
71
|
+
score += 0.1
|
|
72
|
+
*reasons = append(*reasons, "structured plan includes explicit acceptance criteria")
|
|
73
|
+
} else {
|
|
74
|
+
*warnings = append(*warnings, "structured plan acceptance criteria are missing")
|
|
75
|
+
}
|
|
76
|
+
if len(state.Plan.TestRequirements) > 0 || strings.TrimSpace(state.Plan.TestStrategy) != "" {
|
|
77
|
+
score += 0.1
|
|
78
|
+
}
|
|
79
|
+
return clamp01(score)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func (s *Scorer) scopeCompliance(state *models.RunState, reasons, warnings *[]string) float64 {
|
|
83
|
+
for _, result := range state.ValidationResults {
|
|
84
|
+
if result.Name != "scope_compliance" {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
switch result.Status {
|
|
88
|
+
case models.ValidationPass:
|
|
89
|
+
*reasons = append(*reasons, "scope compliance gate passed")
|
|
90
|
+
return 1.0
|
|
91
|
+
case models.ValidationWarn:
|
|
92
|
+
*warnings = append(*warnings, result.Summary)
|
|
93
|
+
return 0.6
|
|
94
|
+
default:
|
|
95
|
+
*warnings = append(*warnings, result.Summary)
|
|
96
|
+
return 0.1
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
*warnings = append(*warnings, "scope compliance gate result is missing")
|
|
100
|
+
return 0.4
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func (s *Scorer) validationQuality(state *models.RunState, reasons, warnings *[]string) float64 {
|
|
104
|
+
if len(state.ValidationResults) == 0 {
|
|
105
|
+
*warnings = append(*warnings, "validation results are missing")
|
|
106
|
+
return 0.3
|
|
107
|
+
}
|
|
108
|
+
passWeight := 0.0
|
|
109
|
+
totalWeight := 0.0
|
|
110
|
+
for _, result := range state.ValidationResults {
|
|
111
|
+
weight := validationWeight(result.Severity)
|
|
112
|
+
totalWeight += weight
|
|
113
|
+
switch result.Status {
|
|
114
|
+
case models.ValidationPass:
|
|
115
|
+
passWeight += weight
|
|
116
|
+
case models.ValidationWarn:
|
|
117
|
+
passWeight += weight * 0.5
|
|
118
|
+
*warnings = append(*warnings, result.Summary)
|
|
119
|
+
default:
|
|
120
|
+
*warnings = append(*warnings, result.Summary)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if totalWeight == 0 {
|
|
124
|
+
return 0.3
|
|
125
|
+
}
|
|
126
|
+
ratio := passWeight / totalWeight
|
|
127
|
+
if ratio >= 0.9 {
|
|
128
|
+
*reasons = append(*reasons, "validation gates passed with high coverage")
|
|
129
|
+
}
|
|
130
|
+
return clamp01(ratio)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func (s *Scorer) testQuality(state *models.RunState, reasons, warnings *[]string) float64 {
|
|
134
|
+
if strings.TrimSpace(state.TestResults) == "" {
|
|
135
|
+
*warnings = append(*warnings, "test output is missing")
|
|
136
|
+
return 0.25
|
|
137
|
+
}
|
|
138
|
+
score := 0.8
|
|
139
|
+
lower := strings.ToLower(state.TestResults)
|
|
140
|
+
if len(state.TestFailures) > 0 {
|
|
141
|
+
score = 0.2
|
|
142
|
+
for _, failure := range state.TestFailures {
|
|
143
|
+
*warnings = append(*warnings, failure.Code+": "+failure.Summary)
|
|
144
|
+
if failure.Flaky {
|
|
145
|
+
score = 0.35
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else if strings.Contains(lower, "fail") || strings.Contains(lower, "panic") {
|
|
149
|
+
*warnings = append(*warnings, "test output indicates instability or failure history")
|
|
150
|
+
score = 0.2
|
|
151
|
+
} else {
|
|
152
|
+
*reasons = append(*reasons, "test output was recorded for the run")
|
|
153
|
+
}
|
|
154
|
+
if state.Retries.Testing > 0 {
|
|
155
|
+
score -= 0.2
|
|
156
|
+
*warnings = append(*warnings, fmt.Sprintf("tests required %d retry attempt(s)", state.Retries.Testing))
|
|
157
|
+
}
|
|
158
|
+
return clamp01(score)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func (s *Scorer) reviewQuality(state *models.RunState, reasons, warnings *[]string) float64 {
|
|
162
|
+
if state.ReviewScorecard == nil {
|
|
163
|
+
*warnings = append(*warnings, "review scorecard is missing")
|
|
164
|
+
return 0.3
|
|
165
|
+
}
|
|
166
|
+
avg := float64(
|
|
167
|
+
state.ReviewScorecard.RequirementCoverage+
|
|
168
|
+
state.ReviewScorecard.ScopeControl+
|
|
169
|
+
state.ReviewScorecard.RegressionRisk+
|
|
170
|
+
state.ReviewScorecard.Readability+
|
|
171
|
+
state.ReviewScorecard.Maintainability+
|
|
172
|
+
state.ReviewScorecard.TestAdequacy,
|
|
173
|
+
) / 60.0
|
|
174
|
+
if state.ReviewScorecard.Decision == models.ReviewAccept {
|
|
175
|
+
*reasons = append(*reasons, "review rubric accepted the patch")
|
|
176
|
+
} else {
|
|
177
|
+
*warnings = append(*warnings, "review rubric did not fully accept the patch")
|
|
178
|
+
avg *= 0.7
|
|
179
|
+
}
|
|
180
|
+
return clamp01(avg)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func (s *Scorer) retryPenalty(state *models.RunState, warnings *[]string) float64 {
|
|
184
|
+
total := state.Retries.Validation + state.Retries.Testing + state.Retries.Review
|
|
185
|
+
if total == 0 {
|
|
186
|
+
return 0
|
|
187
|
+
}
|
|
188
|
+
*warnings = append(*warnings, fmt.Sprintf("run used %d total retry attempt(s)", total))
|
|
189
|
+
if total >= 3 {
|
|
190
|
+
return 1
|
|
191
|
+
}
|
|
192
|
+
if total == 2 {
|
|
193
|
+
return 0.6
|
|
194
|
+
}
|
|
195
|
+
return 0.3
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func validationWeight(severity models.ValidationSeverity) float64 {
|
|
199
|
+
switch severity {
|
|
200
|
+
case models.SeverityCritical:
|
|
201
|
+
return 1.5
|
|
202
|
+
case models.SeverityHigh:
|
|
203
|
+
return 1.2
|
|
204
|
+
case models.SeverityMedium:
|
|
205
|
+
return 1.0
|
|
206
|
+
default:
|
|
207
|
+
return 0.8
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func confidenceBand(score float64) string {
|
|
212
|
+
switch {
|
|
213
|
+
case score >= 0.85:
|
|
214
|
+
return "high"
|
|
215
|
+
case score >= 0.70:
|
|
216
|
+
return "medium"
|
|
217
|
+
case score >= 0.50:
|
|
218
|
+
return "low"
|
|
219
|
+
default:
|
|
220
|
+
return "very_low"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
func clamp01(v float64) float64 {
|
|
225
|
+
if v < 0 {
|
|
226
|
+
return 0
|
|
227
|
+
}
|
|
228
|
+
if v > 1 {
|
|
229
|
+
return 1
|
|
230
|
+
}
|
|
231
|
+
return v
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func round2(v float64) float64 {
|
|
235
|
+
return float64(int(v*100+0.5)) / 100
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func uniqueNonEmpty(values []string) []string {
|
|
239
|
+
result := make([]string, 0, len(values))
|
|
240
|
+
seen := map[string]struct{}{}
|
|
241
|
+
for _, value := range values {
|
|
242
|
+
trimmed := strings.TrimSpace(value)
|
|
243
|
+
if trimmed == "" {
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
if _, ok := seen[trimmed]; ok {
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
seen[trimmed] = struct{}{}
|
|
250
|
+
result = append(result, trimmed)
|
|
251
|
+
}
|
|
252
|
+
return result
|
|
253
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package confidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
"time"
|
|
6
|
+
|
|
7
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestScoreHighConfidenceRun(t *testing.T) {
|
|
11
|
+
scorer := New()
|
|
12
|
+
state := &models.RunState{
|
|
13
|
+
Task: models.Task{ID: "task-1", Description: "fix auth bug", CreatedAt: time.Now()},
|
|
14
|
+
Plan: &models.Plan{
|
|
15
|
+
Summary: "Fix auth bug",
|
|
16
|
+
FilesToInspect: []string{"auth.go"},
|
|
17
|
+
FilesToModify: []string{"auth.go"},
|
|
18
|
+
AcceptanceCriteria: []models.AcceptanceCriterion{{ID: "ac-1", Description: "Bug fixed"}},
|
|
19
|
+
TestRequirements: []string{"Run go test ./..."},
|
|
20
|
+
},
|
|
21
|
+
ValidationResults: []models.ValidationResult{
|
|
22
|
+
{Name: "scope_compliance", Status: models.ValidationPass, Severity: models.SeverityLow},
|
|
23
|
+
{Name: "patch_hygiene", Status: models.ValidationPass, Severity: models.SeverityLow},
|
|
24
|
+
{Name: "plan_compliance", Status: models.ValidationPass, Severity: models.SeverityLow},
|
|
25
|
+
},
|
|
26
|
+
ReviewScorecard: &models.ReviewScorecard{
|
|
27
|
+
RequirementCoverage: 9,
|
|
28
|
+
ScopeControl: 9,
|
|
29
|
+
RegressionRisk: 8,
|
|
30
|
+
Readability: 8,
|
|
31
|
+
Maintainability: 8,
|
|
32
|
+
TestAdequacy: 9,
|
|
33
|
+
Decision: models.ReviewAccept,
|
|
34
|
+
},
|
|
35
|
+
TestResults: "ok auth",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
report := scorer.Score(state)
|
|
39
|
+
if report == nil {
|
|
40
|
+
t.Fatalf("expected confidence report")
|
|
41
|
+
}
|
|
42
|
+
if report.Band != "high" && report.Band != "medium" {
|
|
43
|
+
t.Fatalf("expected healthy confidence band, got %s", report.Band)
|
|
44
|
+
}
|
|
45
|
+
if report.Score <= 0.69 {
|
|
46
|
+
t.Fatalf("expected higher score, got %f", report.Score)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestScoreLowConfidenceRun(t *testing.T) {
|
|
51
|
+
scorer := New()
|
|
52
|
+
state := &models.RunState{
|
|
53
|
+
Task: models.Task{ID: "task-2", Description: "feature", CreatedAt: time.Now()},
|
|
54
|
+
Plan: &models.Plan{},
|
|
55
|
+
ValidationResults: []models.ValidationResult{
|
|
56
|
+
{Name: "scope_compliance", Status: models.ValidationFail, Severity: models.SeverityHigh, Summary: "scope fail"},
|
|
57
|
+
},
|
|
58
|
+
ReviewScorecard: &models.ReviewScorecard{
|
|
59
|
+
RequirementCoverage: 4,
|
|
60
|
+
ScopeControl: 3,
|
|
61
|
+
RegressionRisk: 4,
|
|
62
|
+
Readability: 5,
|
|
63
|
+
Maintainability: 5,
|
|
64
|
+
TestAdequacy: 2,
|
|
65
|
+
Decision: models.ReviewRevise,
|
|
66
|
+
},
|
|
67
|
+
Retries: models.RetryState{Validation: 2, Testing: 1},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
report := scorer.Score(state)
|
|
71
|
+
if report == nil {
|
|
72
|
+
t.Fatalf("expected confidence report")
|
|
73
|
+
}
|
|
74
|
+
if report.Band != "low" && report.Band != "very_low" {
|
|
75
|
+
t.Fatalf("expected low confidence band, got %s", report.Band)
|
|
76
|
+
}
|
|
77
|
+
if report.Score >= 0.70 {
|
|
78
|
+
t.Fatalf("expected low score, got %f", report.Score)
|
|
79
|
+
}
|
|
80
|
+
if len(report.Warnings) == 0 {
|
|
81
|
+
t.Fatalf("expected warnings for low confidence")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
package config
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const (
|
|
11
|
+
OrchDir = ".orch"
|
|
12
|
+
ConfigFile = "config.json"
|
|
13
|
+
RepoMapFile = "repo-map.json"
|
|
14
|
+
RunsDir = "runs"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
type Config struct {
|
|
18
|
+
Version int `json:"version"`
|
|
19
|
+
Models ModelConfig `json:"models"`
|
|
20
|
+
Commands CommandConfig `json:"commands"`
|
|
21
|
+
Patch PatchConfig `json:"patch"`
|
|
22
|
+
Safety SafetyConfig `json:"safety"`
|
|
23
|
+
Provider ProviderConfig `json:"provider"`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ModelConfig struct {
|
|
27
|
+
Planner string `json:"planner"`
|
|
28
|
+
Coder string `json:"coder"`
|
|
29
|
+
Reviewer string `json:"reviewer"`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type CommandConfig struct {
|
|
33
|
+
Test string `json:"test"`
|
|
34
|
+
Lint string `json:"lint"`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type PatchConfig struct {
|
|
38
|
+
MaxFiles int `json:"maxFiles"`
|
|
39
|
+
MaxLines int `json:"maxLines"`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type SafetyConfig struct {
|
|
43
|
+
DryRun bool `json:"dryRun"`
|
|
44
|
+
RequireDestructiveApproval bool `json:"requireDestructiveApproval"`
|
|
45
|
+
LockStaleAfterSeconds int `json:"lockStaleAfterSeconds"`
|
|
46
|
+
Retry RetryPolicyConfig `json:"retry"`
|
|
47
|
+
Confidence ConfidencePolicyConfig `json:"confidence"`
|
|
48
|
+
FeatureFlags SafetyFeatureFlags `json:"featureFlags"`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type RetryPolicyConfig struct {
|
|
52
|
+
ValidationMax int `json:"validationMax"`
|
|
53
|
+
TestMax int `json:"testMax"`
|
|
54
|
+
ReviewMax int `json:"reviewMax"`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ConfidencePolicyConfig struct {
|
|
58
|
+
CompleteMin float64 `json:"completeMin"`
|
|
59
|
+
FailBelow float64 `json:"failBelow"`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type SafetyFeatureFlags struct {
|
|
63
|
+
PermissionMode bool `json:"permissionMode"`
|
|
64
|
+
RepoLock bool `json:"repoLock"`
|
|
65
|
+
RetryLimits bool `json:"retryLimits"`
|
|
66
|
+
PatchConflictReporting bool `json:"patchConflictReporting"`
|
|
67
|
+
ConfidenceEnforcement bool `json:"confidenceEnforcement"`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type ProviderConfig struct {
|
|
71
|
+
Default string `json:"default"`
|
|
72
|
+
OpenAI OpenAIProviderConfig `json:"openai"`
|
|
73
|
+
Flags ProviderFeatureFlags `json:"flags"`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type OpenAIProviderConfig struct {
|
|
77
|
+
APIKeyEnv string `json:"apiKeyEnv"`
|
|
78
|
+
AccountTokenEnv string `json:"accountTokenEnv"`
|
|
79
|
+
AuthMode string `json:"authMode"`
|
|
80
|
+
BaseURL string `json:"baseURL"`
|
|
81
|
+
ReasoningEffort string `json:"reasoningEffort"`
|
|
82
|
+
TimeoutSeconds int `json:"timeoutSeconds"`
|
|
83
|
+
MaxRetries int `json:"maxRetries"`
|
|
84
|
+
Models ProviderRoleModels `json:"models"`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type ProviderRoleModels struct {
|
|
88
|
+
Planner string `json:"planner"`
|
|
89
|
+
Coder string `json:"coder"`
|
|
90
|
+
Reviewer string `json:"reviewer"`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type ProviderFeatureFlags struct {
|
|
94
|
+
OpenAIEnabled bool `json:"openaiEnabled"`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func DefaultConfig() *Config {
|
|
98
|
+
return &Config{
|
|
99
|
+
Version: 1,
|
|
100
|
+
Models: ModelConfig{
|
|
101
|
+
Planner: "openai:gpt-4o-mini",
|
|
102
|
+
Coder: "anthropic:claude-sonnet",
|
|
103
|
+
Reviewer: "openai:gpt-4o-mini",
|
|
104
|
+
},
|
|
105
|
+
Commands: CommandConfig{
|
|
106
|
+
Test: "go test ./...",
|
|
107
|
+
Lint: "go vet ./...",
|
|
108
|
+
},
|
|
109
|
+
Patch: PatchConfig{
|
|
110
|
+
MaxFiles: 10,
|
|
111
|
+
MaxLines: 800,
|
|
112
|
+
},
|
|
113
|
+
Safety: SafetyConfig{
|
|
114
|
+
DryRun: true,
|
|
115
|
+
RequireDestructiveApproval: true,
|
|
116
|
+
LockStaleAfterSeconds: 3600,
|
|
117
|
+
Retry: RetryPolicyConfig{
|
|
118
|
+
ValidationMax: 2,
|
|
119
|
+
TestMax: 2,
|
|
120
|
+
ReviewMax: 2,
|
|
121
|
+
},
|
|
122
|
+
Confidence: ConfidencePolicyConfig{
|
|
123
|
+
CompleteMin: 0.70,
|
|
124
|
+
FailBelow: 0.50,
|
|
125
|
+
},
|
|
126
|
+
FeatureFlags: SafetyFeatureFlags{
|
|
127
|
+
PermissionMode: true,
|
|
128
|
+
RepoLock: true,
|
|
129
|
+
RetryLimits: true,
|
|
130
|
+
PatchConflictReporting: true,
|
|
131
|
+
ConfidenceEnforcement: true,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
Provider: ProviderConfig{
|
|
135
|
+
Default: "openai",
|
|
136
|
+
OpenAI: OpenAIProviderConfig{
|
|
137
|
+
APIKeyEnv: "OPENAI_API_KEY",
|
|
138
|
+
AccountTokenEnv: "OPENAI_ACCOUNT_TOKEN",
|
|
139
|
+
AuthMode: "api_key",
|
|
140
|
+
BaseURL: "https://api.openai.com/v1",
|
|
141
|
+
ReasoningEffort: "medium",
|
|
142
|
+
TimeoutSeconds: 90,
|
|
143
|
+
MaxRetries: 3,
|
|
144
|
+
Models: ProviderRoleModels{
|
|
145
|
+
Planner: "gpt-5.3-codex",
|
|
146
|
+
Coder: "gpt-5.3-codex",
|
|
147
|
+
Reviewer: "gpt-5.3-codex",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
Flags: ProviderFeatureFlags{
|
|
151
|
+
OpenAIEnabled: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func Load(repoRoot string) (*Config, error) {
|
|
158
|
+
configPath := filepath.Join(repoRoot, OrchDir, ConfigFile)
|
|
159
|
+
|
|
160
|
+
data, err := os.ReadFile(configPath)
|
|
161
|
+
if err != nil {
|
|
162
|
+
return nil, fmt.Errorf("failed to read config file: %w", err)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
var cfg Config
|
|
166
|
+
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
167
|
+
return nil, fmt.Errorf("failed to parse config: %w", err)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
applyDefaults(&cfg, data)
|
|
171
|
+
|
|
172
|
+
return &cfg, nil
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func applyDefaults(cfg *Config, rawJSON []byte) {
|
|
176
|
+
defaults := DefaultConfig()
|
|
177
|
+
presence := parsePresence(rawJSON)
|
|
178
|
+
|
|
179
|
+
if cfg.Safety.LockStaleAfterSeconds <= 0 {
|
|
180
|
+
cfg.Safety.LockStaleAfterSeconds = defaults.Safety.LockStaleAfterSeconds
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if cfg.Safety.Retry.ValidationMax <= 0 {
|
|
184
|
+
cfg.Safety.Retry.ValidationMax = defaults.Safety.Retry.ValidationMax
|
|
185
|
+
}
|
|
186
|
+
if cfg.Safety.Retry.TestMax <= 0 {
|
|
187
|
+
cfg.Safety.Retry.TestMax = defaults.Safety.Retry.TestMax
|
|
188
|
+
}
|
|
189
|
+
if cfg.Safety.Retry.ReviewMax <= 0 {
|
|
190
|
+
cfg.Safety.Retry.ReviewMax = defaults.Safety.Retry.ReviewMax
|
|
191
|
+
}
|
|
192
|
+
if cfg.Safety.Confidence.CompleteMin <= 0 {
|
|
193
|
+
cfg.Safety.Confidence.CompleteMin = defaults.Safety.Confidence.CompleteMin
|
|
194
|
+
}
|
|
195
|
+
if cfg.Safety.Confidence.FailBelow <= 0 {
|
|
196
|
+
cfg.Safety.Confidence.FailBelow = defaults.Safety.Confidence.FailBelow
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if !presence.featureFlagsPresent {
|
|
200
|
+
cfg.Safety.FeatureFlags = defaults.Safety.FeatureFlags
|
|
201
|
+
}
|
|
202
|
+
if !presence.confidenceEnforcementPresent {
|
|
203
|
+
cfg.Safety.FeatureFlags.ConfidenceEnforcement = defaults.Safety.FeatureFlags.ConfidenceEnforcement
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if !presence.requireDestructiveApprovalPresent {
|
|
207
|
+
cfg.Safety.RequireDestructiveApproval = defaults.Safety.RequireDestructiveApproval
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if !presence.providerPresent {
|
|
211
|
+
cfg.Provider = defaults.Provider
|
|
212
|
+
} else {
|
|
213
|
+
if cfg.Provider.Default == "" {
|
|
214
|
+
cfg.Provider.Default = defaults.Provider.Default
|
|
215
|
+
}
|
|
216
|
+
if cfg.Provider.OpenAI.APIKeyEnv == "" {
|
|
217
|
+
cfg.Provider.OpenAI.APIKeyEnv = defaults.Provider.OpenAI.APIKeyEnv
|
|
218
|
+
}
|
|
219
|
+
if cfg.Provider.OpenAI.AccountTokenEnv == "" {
|
|
220
|
+
cfg.Provider.OpenAI.AccountTokenEnv = defaults.Provider.OpenAI.AccountTokenEnv
|
|
221
|
+
}
|
|
222
|
+
if cfg.Provider.OpenAI.AuthMode == "" {
|
|
223
|
+
cfg.Provider.OpenAI.AuthMode = defaults.Provider.OpenAI.AuthMode
|
|
224
|
+
}
|
|
225
|
+
if cfg.Provider.OpenAI.BaseURL == "" {
|
|
226
|
+
cfg.Provider.OpenAI.BaseURL = defaults.Provider.OpenAI.BaseURL
|
|
227
|
+
}
|
|
228
|
+
if cfg.Provider.OpenAI.ReasoningEffort == "" {
|
|
229
|
+
cfg.Provider.OpenAI.ReasoningEffort = defaults.Provider.OpenAI.ReasoningEffort
|
|
230
|
+
}
|
|
231
|
+
if cfg.Provider.OpenAI.TimeoutSeconds <= 0 {
|
|
232
|
+
cfg.Provider.OpenAI.TimeoutSeconds = defaults.Provider.OpenAI.TimeoutSeconds
|
|
233
|
+
}
|
|
234
|
+
if cfg.Provider.OpenAI.MaxRetries <= 0 {
|
|
235
|
+
cfg.Provider.OpenAI.MaxRetries = defaults.Provider.OpenAI.MaxRetries
|
|
236
|
+
}
|
|
237
|
+
if cfg.Provider.OpenAI.Models.Planner == "" {
|
|
238
|
+
cfg.Provider.OpenAI.Models.Planner = defaults.Provider.OpenAI.Models.Planner
|
|
239
|
+
}
|
|
240
|
+
if cfg.Provider.OpenAI.Models.Coder == "" {
|
|
241
|
+
cfg.Provider.OpenAI.Models.Coder = defaults.Provider.OpenAI.Models.Coder
|
|
242
|
+
}
|
|
243
|
+
if cfg.Provider.OpenAI.Models.Reviewer == "" {
|
|
244
|
+
cfg.Provider.OpenAI.Models.Reviewer = defaults.Provider.OpenAI.Models.Reviewer
|
|
245
|
+
}
|
|
246
|
+
if !presence.providerFlagsPresent {
|
|
247
|
+
cfg.Provider.Flags = defaults.Provider.Flags
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type fieldPresence struct {
|
|
253
|
+
featureFlagsPresent bool
|
|
254
|
+
confidenceEnforcementPresent bool
|
|
255
|
+
requireDestructiveApprovalPresent bool
|
|
256
|
+
providerPresent bool
|
|
257
|
+
providerFlagsPresent bool
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func parsePresence(rawJSON []byte) fieldPresence {
|
|
261
|
+
result := fieldPresence{}
|
|
262
|
+
|
|
263
|
+
var root map[string]json.RawMessage
|
|
264
|
+
if err := json.Unmarshal(rawJSON, &root); err != nil {
|
|
265
|
+
return result
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
safetyRaw, ok := root["safety"]
|
|
269
|
+
if !ok {
|
|
270
|
+
return result
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
var safety map[string]json.RawMessage
|
|
274
|
+
if err := json.Unmarshal(safetyRaw, &safety); err != nil {
|
|
275
|
+
return result
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if featureFlagsRaw, ok := safety["featureFlags"]; ok {
|
|
279
|
+
result.featureFlagsPresent = true
|
|
280
|
+
var featureFlags map[string]json.RawMessage
|
|
281
|
+
if err := json.Unmarshal(featureFlagsRaw, &featureFlags); err == nil {
|
|
282
|
+
_, result.confidenceEnforcementPresent = featureFlags["confidenceEnforcement"]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
_, result.requireDestructiveApprovalPresent = safety["requireDestructiveApproval"]
|
|
286
|
+
|
|
287
|
+
providerRaw, ok := root["provider"]
|
|
288
|
+
if ok {
|
|
289
|
+
result.providerPresent = true
|
|
290
|
+
var provider map[string]json.RawMessage
|
|
291
|
+
if err := json.Unmarshal(providerRaw, &provider); err == nil {
|
|
292
|
+
_, result.providerFlagsPresent = provider["flags"]
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return result
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
func Save(repoRoot string, cfg *Config) error {
|
|
300
|
+
orchDir := filepath.Join(repoRoot, OrchDir)
|
|
301
|
+
if err := os.MkdirAll(orchDir, 0o755); err != nil {
|
|
302
|
+
return fmt.Errorf("failed to create .orch directory: %w", err)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
data, err := json.MarshalIndent(cfg, "", " ")
|
|
306
|
+
if err != nil {
|
|
307
|
+
return fmt.Errorf("failed to serialize config: %w", err)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
configPath := filepath.Join(orchDir, ConfigFile)
|
|
311
|
+
if err := os.WriteFile(configPath, data, 0o644); err != nil {
|
|
312
|
+
return fmt.Errorf("failed to write config file: %w", err)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return nil
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
func EnsureOrchDir(repoRoot string) error {
|
|
319
|
+
dirs := []string{
|
|
320
|
+
filepath.Join(repoRoot, OrchDir),
|
|
321
|
+
filepath.Join(repoRoot, OrchDir, RunsDir),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for _, dir := range dirs {
|
|
325
|
+
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
326
|
+
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return nil
|
|
331
|
+
}
|