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
package/cmd/doctor.go
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"strings"
|
|
8
|
+
"time"
|
|
9
|
+
|
|
10
|
+
"github.com/furkanbeydemir/orch/internal/auth"
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
12
|
+
"github.com/furkanbeydemir/orch/internal/providers/openai"
|
|
13
|
+
"github.com/spf13/cobra"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
var doctorCmd = &cobra.Command{
|
|
17
|
+
Use: "doctor",
|
|
18
|
+
Short: "Validate Orch runtime readiness",
|
|
19
|
+
RunE: runDoctor,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func init() {
|
|
23
|
+
rootCmd.AddCommand(doctorCmd)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func runDoctor(cmd *cobra.Command, args []string) error {
|
|
27
|
+
cwd, err := os.Getwd()
|
|
28
|
+
if err != nil {
|
|
29
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
cfg, err := config.Load(cwd)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return fmt.Errorf("failed to load configuration: %w", err)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type check struct {
|
|
38
|
+
name string
|
|
39
|
+
ok bool
|
|
40
|
+
detail string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
checks := make([]check, 0)
|
|
44
|
+
|
|
45
|
+
defaultProvider := strings.TrimSpace(cfg.Provider.Default)
|
|
46
|
+
checks = append(checks, check{
|
|
47
|
+
name: "provider.default",
|
|
48
|
+
ok: defaultProvider == "openai",
|
|
49
|
+
detail: fmt.Sprintf("value=%q", defaultProvider),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
checks = append(checks, check{
|
|
53
|
+
name: "provider.flags.openaiEnabled",
|
|
54
|
+
ok: cfg.Provider.Flags.OpenAIEnabled,
|
|
55
|
+
detail: fmt.Sprintf("value=%t", cfg.Provider.Flags.OpenAIEnabled),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
authMode := strings.ToLower(strings.TrimSpace(cfg.Provider.OpenAI.AuthMode))
|
|
59
|
+
if authMode == "" {
|
|
60
|
+
authMode = "api_key"
|
|
61
|
+
}
|
|
62
|
+
checks = append(checks, check{name: "openai.auth_mode", ok: authMode == "api_key" || authMode == "account", detail: authMode})
|
|
63
|
+
|
|
64
|
+
key := strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.APIKeyEnv))
|
|
65
|
+
accountToken := strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.AccountTokenEnv))
|
|
66
|
+
|
|
67
|
+
storedCred, credErr := auth.Get(cwd, "openai")
|
|
68
|
+
storedAPIKey := credErr == nil && storedCred != nil && storedCred.Type == "api" && strings.TrimSpace(storedCred.Key) != ""
|
|
69
|
+
storedAccount := credErr == nil && storedCred != nil && storedCred.Type == "oauth" && strings.TrimSpace(storedCred.AccessToken) != ""
|
|
70
|
+
storedRefresh := credErr == nil && storedCred != nil && storedCred.Type == "oauth" && strings.TrimSpace(storedCred.RefreshToken) != ""
|
|
71
|
+
if credErr != nil {
|
|
72
|
+
checks = append(checks, check{name: "openai.stored_credential", ok: false, detail: credErr.Error()})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
checks = append(checks, check{
|
|
76
|
+
name: "openai.api_key",
|
|
77
|
+
ok: key != "" || storedAPIKey || authMode == "account",
|
|
78
|
+
detail: fmt.Sprintf("env=%s", cfg.Provider.OpenAI.APIKeyEnv),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
checks = append(checks, check{
|
|
82
|
+
name: "openai.account_token",
|
|
83
|
+
ok: accountToken != "" || storedAccount || authMode == "api_key",
|
|
84
|
+
detail: fmt.Sprintf("env=%s stored=%t refresh=%t", cfg.Provider.OpenAI.AccountTokenEnv, storedAccount, storedRefresh),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
checks = append(checks, check{
|
|
88
|
+
name: "openai.account_refresh",
|
|
89
|
+
ok: authMode != "account" || accountToken != "" || !storedAccount || storedRefresh || (storedCred != nil && storedCred.ExpiresAt.IsZero()) || time.Now().UTC().Before(storedCred.ExpiresAt),
|
|
90
|
+
detail: fmt.Sprintf("required_when_expired=%t", authMode == "account" && accountToken == ""),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
checks = append(checks, check{name: "openai.model.planner", ok: strings.TrimSpace(cfg.Provider.OpenAI.Models.Planner) != "", detail: cfg.Provider.OpenAI.Models.Planner})
|
|
94
|
+
checks = append(checks, check{name: "openai.model.coder", ok: strings.TrimSpace(cfg.Provider.OpenAI.Models.Coder) != "", detail: cfg.Provider.OpenAI.Models.Coder})
|
|
95
|
+
checks = append(checks, check{name: "openai.model.reviewer", ok: strings.TrimSpace(cfg.Provider.OpenAI.Models.Reviewer) != "", detail: cfg.Provider.OpenAI.Models.Reviewer})
|
|
96
|
+
|
|
97
|
+
if cfg.Provider.Flags.OpenAIEnabled && defaultProvider == "openai" {
|
|
98
|
+
client := openai.New(cfg.Provider.OpenAI)
|
|
99
|
+
client.SetTokenResolver(func(ctx context.Context) (string, error) {
|
|
100
|
+
_ = ctx
|
|
101
|
+
if authMode == "api_key" {
|
|
102
|
+
if storedCred != nil && strings.TrimSpace(storedCred.Key) != "" {
|
|
103
|
+
return strings.TrimSpace(storedCred.Key), nil
|
|
104
|
+
}
|
|
105
|
+
return "", nil
|
|
106
|
+
}
|
|
107
|
+
resolved, resolveErr := auth.ResolveAccountAccessToken(cwd, "openai")
|
|
108
|
+
if resolveErr != nil {
|
|
109
|
+
return "", resolveErr
|
|
110
|
+
}
|
|
111
|
+
return resolved, nil
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
115
|
+
defer cancel()
|
|
116
|
+
validateErr := client.Validate(ctx)
|
|
117
|
+
checks = append(checks, check{
|
|
118
|
+
name: "openai.auth",
|
|
119
|
+
ok: validateErr == nil,
|
|
120
|
+
detail: errDetail(validateErr, "ok"),
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
failed := 0
|
|
125
|
+
fmt.Println("Orch Doctor")
|
|
126
|
+
fmt.Println("-----------")
|
|
127
|
+
for _, c := range checks {
|
|
128
|
+
status := "PASS"
|
|
129
|
+
if !c.ok {
|
|
130
|
+
status = "FAIL"
|
|
131
|
+
failed++
|
|
132
|
+
}
|
|
133
|
+
fmt.Printf("%-6s %-32s %s\n", status, c.name, c.detail)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if failed > 0 {
|
|
137
|
+
return fmt.Errorf("doctor failed: %d checks failed", failed)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fmt.Println("All checks passed.")
|
|
141
|
+
return nil
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func errDetail(err error, fallback string) string {
|
|
145
|
+
if err == nil {
|
|
146
|
+
return fallback
|
|
147
|
+
}
|
|
148
|
+
return err.Error()
|
|
149
|
+
}
|
package/cmd/explain.go
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"sort"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
10
|
+
"github.com/spf13/cobra"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var explainCmd = &cobra.Command{
|
|
14
|
+
Use: "explain [run-id]",
|
|
15
|
+
Short: "Explain why a run passed, failed, or was downgraded",
|
|
16
|
+
Long: `Shows the structured reasoning artifacts for a run, including plan, validation, test, review, and confidence signals.`,
|
|
17
|
+
Args: cobra.MaximumNArgs(1),
|
|
18
|
+
RunE: runExplain,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func init() {
|
|
22
|
+
rootCmd.AddCommand(explainCmd)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func runExplain(cmd *cobra.Command, args []string) error {
|
|
26
|
+
cwd, err := os.Getwd()
|
|
27
|
+
if err != nil {
|
|
28
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ctx, err := loadSessionContext(cwd)
|
|
32
|
+
if err != nil {
|
|
33
|
+
return err
|
|
34
|
+
}
|
|
35
|
+
defer ctx.Store.Close()
|
|
36
|
+
|
|
37
|
+
var state *models.RunState
|
|
38
|
+
if len(args) == 0 {
|
|
39
|
+
state, err = ctx.Store.GetLatestRunStateBySession(ctx.Session.ID)
|
|
40
|
+
if err != nil {
|
|
41
|
+
return fmt.Errorf("failed to load latest run state: %w", err)
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
state, err = ctx.Store.GetRunState(ctx.ProjectID, args[0])
|
|
45
|
+
if err != nil {
|
|
46
|
+
return fmt.Errorf("failed to load run state %s: %w", args[0], err)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
printExplain(state)
|
|
51
|
+
return nil
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func printExplain(state *models.RunState) {
|
|
55
|
+
if state == nil {
|
|
56
|
+
fmt.Println("No run state available.")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fmt.Println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
|
61
|
+
fmt.Println("š RUN EXPLANATION")
|
|
62
|
+
fmt.Println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
|
63
|
+
fmt.Printf("Run ID: %s\n", state.ID)
|
|
64
|
+
fmt.Printf("Status: %s\n", state.Status)
|
|
65
|
+
fmt.Printf("Task: %s\n", state.Task.Description)
|
|
66
|
+
if state.Error != "" {
|
|
67
|
+
fmt.Printf("Error: %s\n", state.Error)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if state.TaskBrief != nil {
|
|
71
|
+
fmt.Println("\nš§ Task Brief")
|
|
72
|
+
fmt.Printf(" Type: %s\n", state.TaskBrief.TaskType)
|
|
73
|
+
fmt.Printf(" Risk: %s\n", state.TaskBrief.RiskLevel)
|
|
74
|
+
fmt.Printf(" Goal: %s\n", state.TaskBrief.NormalizedGoal)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if state.Plan != nil {
|
|
78
|
+
fmt.Println("\nš Plan")
|
|
79
|
+
if state.Plan.Summary != "" {
|
|
80
|
+
fmt.Printf(" Summary: %s\n", state.Plan.Summary)
|
|
81
|
+
}
|
|
82
|
+
if len(state.Plan.FilesToModify) > 0 {
|
|
83
|
+
fmt.Printf(" Files To Modify: %s\n", strings.Join(state.Plan.FilesToModify, ", "))
|
|
84
|
+
}
|
|
85
|
+
if len(state.Plan.AcceptanceCriteria) > 0 {
|
|
86
|
+
fmt.Println(" Acceptance Criteria:")
|
|
87
|
+
for _, criterion := range state.Plan.AcceptanceCriteria {
|
|
88
|
+
fmt.Printf(" - %s\n", criterion.Description)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if state.ExecutionContract != nil {
|
|
94
|
+
fmt.Println("\nš§© Execution Contract")
|
|
95
|
+
if len(state.ExecutionContract.AllowedFiles) > 0 {
|
|
96
|
+
fmt.Printf(" Allowed Files: %s\n", strings.Join(state.ExecutionContract.AllowedFiles, ", "))
|
|
97
|
+
}
|
|
98
|
+
if len(state.ExecutionContract.RequiredEdits) > 0 {
|
|
99
|
+
fmt.Printf(" Required Edits: %s\n", strings.Join(state.ExecutionContract.RequiredEdits, " | "))
|
|
100
|
+
}
|
|
101
|
+
fmt.Printf(" Patch Budget: max_files=%d max_changed_lines=%d\n", state.ExecutionContract.PatchBudget.MaxFiles, state.ExecutionContract.PatchBudget.MaxChangedLines)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if len(state.ValidationResults) > 0 {
|
|
105
|
+
fmt.Println("\nš”ļø Validation + Review/Test Gates")
|
|
106
|
+
results := append([]models.ValidationResult(nil), state.ValidationResults...)
|
|
107
|
+
sort.SliceStable(results, func(i, j int) bool {
|
|
108
|
+
if results[i].Stage == results[j].Stage {
|
|
109
|
+
return results[i].Name < results[j].Name
|
|
110
|
+
}
|
|
111
|
+
return results[i].Stage < results[j].Stage
|
|
112
|
+
})
|
|
113
|
+
for _, result := range results {
|
|
114
|
+
fmt.Printf(" - [%s] %s = %s (%s)\n", result.Stage, result.Name, result.Status, result.Summary)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if len(state.TestFailures) > 0 {
|
|
119
|
+
fmt.Println("\nš§Ŗ Test Failure Classification")
|
|
120
|
+
for _, failure := range state.TestFailures {
|
|
121
|
+
line := fmt.Sprintf(" - %s: %s", failure.Code, failure.Summary)
|
|
122
|
+
if failure.Flaky {
|
|
123
|
+
line += " [flaky-suspected]"
|
|
124
|
+
}
|
|
125
|
+
fmt.Println(line)
|
|
126
|
+
}
|
|
127
|
+
} else if strings.TrimSpace(state.TestResults) != "" {
|
|
128
|
+
fmt.Println("\nš§Ŗ Test Output")
|
|
129
|
+
fmt.Printf(" %s\n", strings.ReplaceAll(strings.TrimSpace(state.TestResults), "\n", "\n "))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if state.ReviewScorecard != nil {
|
|
133
|
+
fmt.Println("\nš§ Review Scorecard")
|
|
134
|
+
fmt.Printf(" Requirement Coverage: %d\n", state.ReviewScorecard.RequirementCoverage)
|
|
135
|
+
fmt.Printf(" Scope Control: %d\n", state.ReviewScorecard.ScopeControl)
|
|
136
|
+
fmt.Printf(" Regression Risk: %d\n", state.ReviewScorecard.RegressionRisk)
|
|
137
|
+
fmt.Printf(" Readability: %d\n", state.ReviewScorecard.Readability)
|
|
138
|
+
fmt.Printf(" Maintainability: %d\n", state.ReviewScorecard.Maintainability)
|
|
139
|
+
fmt.Printf(" Test Adequacy: %d\n", state.ReviewScorecard.TestAdequacy)
|
|
140
|
+
fmt.Printf(" Decision: %s\n", state.ReviewScorecard.Decision)
|
|
141
|
+
if len(state.ReviewScorecard.Findings) > 0 {
|
|
142
|
+
fmt.Println(" Findings:")
|
|
143
|
+
for _, finding := range state.ReviewScorecard.Findings {
|
|
144
|
+
fmt.Printf(" - %s\n", finding)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if state.Confidence != nil {
|
|
150
|
+
fmt.Println("\nšÆ Confidence")
|
|
151
|
+
fmt.Printf(" Score: %.2f\n", state.Confidence.Score)
|
|
152
|
+
fmt.Printf(" Band: %s\n", state.Confidence.Band)
|
|
153
|
+
if len(state.Confidence.Reasons) > 0 {
|
|
154
|
+
fmt.Println(" Reasons:")
|
|
155
|
+
for _, reason := range state.Confidence.Reasons {
|
|
156
|
+
fmt.Printf(" - %s\n", reason)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if len(state.Confidence.Warnings) > 0 {
|
|
160
|
+
fmt.Println(" Warnings:")
|
|
161
|
+
for _, warning := range state.Confidence.Warnings {
|
|
162
|
+
fmt.Printf(" - %s\n", warning)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if state.Review != nil {
|
|
168
|
+
fmt.Println("\nš¬ Final Review")
|
|
169
|
+
fmt.Printf(" Decision: %s\n", state.Review.Decision)
|
|
170
|
+
for _, comment := range state.Review.Comments {
|
|
171
|
+
fmt.Printf(" - %s\n", comment)
|
|
172
|
+
}
|
|
173
|
+
if len(state.Review.Suggestions) > 0 {
|
|
174
|
+
fmt.Println(" Suggestions:")
|
|
175
|
+
for _, suggestion := range state.Review.Suggestions {
|
|
176
|
+
fmt.Printf(" - %s\n", suggestion)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if state.RetryDirective != nil {
|
|
182
|
+
fmt.Println("\nš Last Retry Directive")
|
|
183
|
+
fmt.Printf(" Stage: %s\n", state.RetryDirective.Stage)
|
|
184
|
+
fmt.Printf(" Attempt: %d\n", state.RetryDirective.Attempt)
|
|
185
|
+
if len(state.RetryDirective.Reasons) > 0 {
|
|
186
|
+
fmt.Printf(" Reasons: %s\n", strings.Join(state.RetryDirective.Reasons, " | "))
|
|
187
|
+
}
|
|
188
|
+
if len(state.RetryDirective.Instructions) > 0 {
|
|
189
|
+
fmt.Printf(" Instructions: %s\n", strings.Join(state.RetryDirective.Instructions, " | "))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"io"
|
|
6
|
+
"os"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestPrintExplainIncludesConfidenceAndReview(t *testing.T) {
|
|
15
|
+
state := &models.RunState{
|
|
16
|
+
ID: "run-explain-1",
|
|
17
|
+
Task: models.Task{ID: "task-1", Description: "demo", CreatedAt: time.Now()},
|
|
18
|
+
Status: models.StatusCompleted,
|
|
19
|
+
StartedAt: time.Now(),
|
|
20
|
+
Review: &models.ReviewResult{Decision: models.ReviewAccept, Comments: []string{"looks good"}},
|
|
21
|
+
Confidence: &models.ConfidenceReport{
|
|
22
|
+
Score: 0.91,
|
|
23
|
+
Band: "high",
|
|
24
|
+
Reasons: []string{"tests and review aligned"},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
output := captureStdout(t, func() {
|
|
29
|
+
printExplain(state)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if !strings.Contains(output, "RUN EXPLANATION") {
|
|
33
|
+
t.Fatalf("expected explanation header, got: %s", output)
|
|
34
|
+
}
|
|
35
|
+
if !strings.Contains(output, "Score: 0.91") {
|
|
36
|
+
t.Fatalf("expected confidence score in output, got: %s", output)
|
|
37
|
+
}
|
|
38
|
+
if !strings.Contains(output, "Decision: accept") {
|
|
39
|
+
t.Fatalf("expected review decision in output, got: %s", output)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func captureStdout(t *testing.T, fn func()) string {
|
|
44
|
+
t.Helper()
|
|
45
|
+
original := os.Stdout
|
|
46
|
+
r, w, err := os.Pipe()
|
|
47
|
+
if err != nil {
|
|
48
|
+
t.Fatalf("pipe: %v", err)
|
|
49
|
+
}
|
|
50
|
+
os.Stdout = w
|
|
51
|
+
defer func() { os.Stdout = original }()
|
|
52
|
+
|
|
53
|
+
fn()
|
|
54
|
+
|
|
55
|
+
_ = w.Close()
|
|
56
|
+
var buf bytes.Buffer
|
|
57
|
+
if _, err := io.Copy(&buf, r); err != nil {
|
|
58
|
+
t.Fatalf("copy stdout: %v", err)
|
|
59
|
+
}
|
|
60
|
+
_ = r.Close()
|
|
61
|
+
return buf.String()
|
|
62
|
+
}
|
package/cmd/init.go
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Package cmd implements the init command.
|
|
2
|
+
package cmd
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
|
|
9
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
10
|
+
"github.com/furkanbeydemir/orch/internal/repo"
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/storage"
|
|
12
|
+
"github.com/spf13/cobra"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// initCmd represents the `orch init` command.
|
|
16
|
+
var initCmd = &cobra.Command{
|
|
17
|
+
Use: "init",
|
|
18
|
+
Short: "Analyze repository and create configuration",
|
|
19
|
+
Long: `Analyzes repository and creates .orch/ and configuration files.`,
|
|
20
|
+
RunE: runInit,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func init() {
|
|
24
|
+
rootCmd.AddCommand(initCmd)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func runInit(cmd *cobra.Command, args []string) error {
|
|
28
|
+
cwd, err := os.Getwd()
|
|
29
|
+
if err != nil {
|
|
30
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return performInit(cwd, false)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func AutoInitIfNeeded(cwd string) error {
|
|
37
|
+
configPath := filepath.Join(cwd, config.OrchDir, config.ConfigFile)
|
|
38
|
+
if _, err := os.Stat(configPath); err == nil {
|
|
39
|
+
// Already initialized
|
|
40
|
+
return nil
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Print a notice since it's happening automatically
|
|
44
|
+
fmt.Println("⨠First run detected. Initializing Orch automatically...")
|
|
45
|
+
return performInit(cwd, true)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func performInit(cwd string, quiet bool) error {
|
|
49
|
+
if !quiet {
|
|
50
|
+
fmt.Println("š Analyzing repository...")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if err := config.EnsureOrchDir(cwd); err != nil {
|
|
54
|
+
return err
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
cfg := config.DefaultConfig()
|
|
58
|
+
if err := config.Save(cwd, cfg); err != nil {
|
|
59
|
+
return err
|
|
60
|
+
}
|
|
61
|
+
if !quiet {
|
|
62
|
+
fmt.Println("ā
.orch/config.json created")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Repository analysis
|
|
66
|
+
analyzer := repo.NewAnalyzer(cwd)
|
|
67
|
+
repoMap, err := analyzer.Analyze()
|
|
68
|
+
if err != nil {
|
|
69
|
+
return fmt.Errorf("repository analysis failed: %w", err)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if !quiet {
|
|
73
|
+
fmt.Printf("ā
.orch/repo-map.json created (%d files scanned)\n", len(repoMap.Files))
|
|
74
|
+
fmt.Printf("š Language: %s | Package Manager: %s | Test: %s\n",
|
|
75
|
+
repoMap.Language, repoMap.PackageManager, repoMap.TestFramework)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
store, err := storage.Open(cwd)
|
|
79
|
+
if err != nil {
|
|
80
|
+
return fmt.Errorf("failed to initialize sqlite storage: %w", err)
|
|
81
|
+
}
|
|
82
|
+
defer store.Close()
|
|
83
|
+
|
|
84
|
+
projectID, err := store.GetOrCreateProject()
|
|
85
|
+
if err != nil {
|
|
86
|
+
return fmt.Errorf("failed to resolve project: %w", err)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
session, err := store.EnsureDefaultSession(projectID)
|
|
90
|
+
if err != nil {
|
|
91
|
+
return fmt.Errorf("failed to resolve default session: %w", err)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if !quiet {
|
|
95
|
+
fmt.Printf("ā
SQLite storage ready (.orch/orch.db), active session: %s (%s)\n", session.Name, session.ID)
|
|
96
|
+
fmt.Println("\nšÆ Orch configured successfully!")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return nil
|
|
100
|
+
}
|