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/root.go
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Subcommands: init, plan, run, diff, apply, logs, explain, stats.
|
|
2
|
+
package cmd
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
var (
|
|
12
|
+
verbose bool
|
|
13
|
+
configPath string
|
|
14
|
+
sessionID string
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
var rootCmd = &cobra.Command{
|
|
18
|
+
Use: "orch",
|
|
19
|
+
Short: "AI-powered coding orchestrator",
|
|
20
|
+
Long: `Orch is a CLI orchestration engine that uses AI agents to execute coding tasks inside a repository.
|
|
21
|
+
|
|
22
|
+
Pipeline: Task → Plan → Code → Test → Review → Patch
|
|
23
|
+
|
|
24
|
+
Usage examples:
|
|
25
|
+
orch init # Repository analysis and configuration
|
|
26
|
+
orch plan "add redis caching" # Generate plan only
|
|
27
|
+
orch run "fix auth bug" # Run full pipeline
|
|
28
|
+
orch diff # Show generated patch
|
|
29
|
+
orch apply # Apply patch
|
|
30
|
+
orch logs # Show execution trace
|
|
31
|
+
orch explain # Explain latest run
|
|
32
|
+
orch stats # Show recent run quality stats`,
|
|
33
|
+
Version: version,
|
|
34
|
+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
35
|
+
// Do not auto-init if the command is "init" or any help command
|
|
36
|
+
if cmd.Name() == "init" || cmd.Name() == "help" || cmd.Name() == "version" {
|
|
37
|
+
return nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
cwd, err := os.Getwd()
|
|
41
|
+
if err != nil {
|
|
42
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if err := AutoInitIfNeeded(cwd); err != nil {
|
|
46
|
+
return fmt.Errorf("auto-initialization failed: %w", err)
|
|
47
|
+
}
|
|
48
|
+
return nil
|
|
49
|
+
},
|
|
50
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
51
|
+
return startInteractiveShell(sessionID)
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func Execute() {
|
|
56
|
+
if err := rootCmd.Execute(); err != nil {
|
|
57
|
+
fmt.Fprintln(os.Stderr, err)
|
|
58
|
+
os.Exit(1)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func init() {
|
|
63
|
+
// Global flags
|
|
64
|
+
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output mode")
|
|
65
|
+
rootCmd.PersistentFlags().StringVar(&configPath, "config", "", "Configuration file path")
|
|
66
|
+
rootCmd.PersistentFlags().StringVarP(&sessionID, "session", "s", "", "Resume interactive session id")
|
|
67
|
+
}
|
package/cmd/run.go
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Package cmd implements the run command.
|
|
2
|
+
//
|
|
3
|
+
// Pipeline:
|
|
4
|
+
//
|
|
5
|
+
// Task → Repo Analysis → Context Selection → Planner → Coder
|
|
6
|
+
// → Patch Validation → Test Runner → Reviewer → Result
|
|
7
|
+
package cmd
|
|
8
|
+
|
|
9
|
+
import (
|
|
10
|
+
"fmt"
|
|
11
|
+
"strings"
|
|
12
|
+
|
|
13
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
14
|
+
"github.com/spf13/cobra"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// runCmd represents the `orch run` command.
|
|
18
|
+
var runCmd = &cobra.Command{
|
|
19
|
+
Use: "run [task]",
|
|
20
|
+
Short: "Runs full pipeline",
|
|
21
|
+
Long: `Runs the full pipeline for the given task:
|
|
22
|
+
analyze -> plan -> code -> validate -> test -> review.`,
|
|
23
|
+
Args: cobra.ExactArgs(1),
|
|
24
|
+
RunE: runRun,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func init() {
|
|
28
|
+
rootCmd.AddCommand(runCmd)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func runRun(cmd *cobra.Command, args []string) error {
|
|
32
|
+
result, err := executeRunTask(args[0])
|
|
33
|
+
if err != nil {
|
|
34
|
+
return err
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for _, warning := range result.Warnings {
|
|
38
|
+
fmt.Printf("\n⚠ %s\n", warning)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fmt.Printf("🚀 Starting pipeline: %s\n\n", result.Task.Description)
|
|
42
|
+
printRunContextSummary(result.ProjectID, result.SessionName, result.Worktree, result.CWD, result.ExecRoot)
|
|
43
|
+
|
|
44
|
+
state := result.State
|
|
45
|
+
err = result.Err
|
|
46
|
+
|
|
47
|
+
if err != nil {
|
|
48
|
+
fmt.Printf("\n❌ Pipeline failed: %s\n", err.Error())
|
|
49
|
+
if state != nil {
|
|
50
|
+
printRunResultSummary(state)
|
|
51
|
+
if len(state.UnresolvedFailures) > 0 {
|
|
52
|
+
fmt.Println("\n⚠ Unresolved Failures:")
|
|
53
|
+
for _, failure := range state.UnresolvedFailures {
|
|
54
|
+
fmt.Printf("- %s\n", failure)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if strings.TrimSpace(state.BestPatchSummary) != "" {
|
|
58
|
+
fmt.Printf("\n🩹 Best Patch Summary: %s\n", state.BestPatchSummary)
|
|
59
|
+
}
|
|
60
|
+
if strings.TrimSpace(state.TestResults) != "" {
|
|
61
|
+
fmt.Println("\n🧪 Test Summary:")
|
|
62
|
+
fmt.Println(state.TestResults)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return err
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fmt.Println("\n═══════════════════════════════════════")
|
|
69
|
+
fmt.Printf("✅ Pipeline completed: %s\n", state.Status)
|
|
70
|
+
fmt.Println("═══════════════════════════════════════")
|
|
71
|
+
|
|
72
|
+
if state.Review != nil {
|
|
73
|
+
fmt.Printf("\n📋 Review Decision: %s\n", state.Review.Decision)
|
|
74
|
+
for _, comment := range state.Review.Comments {
|
|
75
|
+
fmt.Printf(" 💬 %s\n", comment)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if state.Confidence != nil {
|
|
79
|
+
fmt.Printf("\n🎯 Confidence: %.2f (%s)\n", state.Confidence.Score, state.Confidence.Band)
|
|
80
|
+
for _, warning := range state.Confidence.Warnings {
|
|
81
|
+
fmt.Printf(" ⚠ %s\n", warning)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if state.Patch != nil && state.Patch.RawDiff != "" {
|
|
86
|
+
fmt.Println("\n💡 To view the patch: orch diff")
|
|
87
|
+
fmt.Println("💡 To apply the patch: orch apply")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
printRunResultSummary(state)
|
|
91
|
+
|
|
92
|
+
return nil
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func printRunContextSummary(projectID, sessionName, worktree, cwd, execRoot string) {
|
|
96
|
+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
97
|
+
fmt.Println("Run Context")
|
|
98
|
+
fmt.Printf("Project: %s\n", projectID)
|
|
99
|
+
fmt.Printf("Session: %s\n", sessionName)
|
|
100
|
+
if worktree != "" {
|
|
101
|
+
fmt.Printf("Worktree: %s\n", worktree)
|
|
102
|
+
}
|
|
103
|
+
if execRoot != cwd {
|
|
104
|
+
fmt.Printf("Execution root: %s\n", execRoot)
|
|
105
|
+
}
|
|
106
|
+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func printRunResultSummary(state *models.RunState) {
|
|
110
|
+
if state == nil {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
fmt.Println("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
114
|
+
fmt.Println("Run Summary")
|
|
115
|
+
fmt.Printf("Run ID: %s\n", state.ID)
|
|
116
|
+
fmt.Printf("Status: %s\n", state.Status)
|
|
117
|
+
fmt.Printf("Retries: validation=%d test=%d review=%d\n", state.Retries.Validation, state.Retries.Testing, state.Retries.Review)
|
|
118
|
+
if state.Confidence != nil {
|
|
119
|
+
fmt.Printf("Confidence: %.2f (%s)\n", state.Confidence.Score, state.Confidence.Band)
|
|
120
|
+
}
|
|
121
|
+
fmt.Printf("Log file: .orch/runs/%s.json\n", state.ID)
|
|
122
|
+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
123
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"strings"
|
|
8
|
+
"time"
|
|
9
|
+
|
|
10
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
12
|
+
"github.com/furkanbeydemir/orch/internal/orchestrator"
|
|
13
|
+
runlock "github.com/furkanbeydemir/orch/internal/runtime"
|
|
14
|
+
"github.com/furkanbeydemir/orch/internal/session"
|
|
15
|
+
"github.com/furkanbeydemir/orch/internal/storage"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
type runExecutionResult struct {
|
|
19
|
+
Task *models.Task
|
|
20
|
+
State *models.RunState
|
|
21
|
+
Err error
|
|
22
|
+
ProjectID string
|
|
23
|
+
SessionName string
|
|
24
|
+
Worktree string
|
|
25
|
+
CWD string
|
|
26
|
+
ExecRoot string
|
|
27
|
+
Warnings []string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func executeRunTask(taskDescription string) (*runExecutionResult, error) {
|
|
31
|
+
cwd, err := getWorkingDirectory()
|
|
32
|
+
if err != nil {
|
|
33
|
+
return nil, err
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cfg, err := config.Load(cwd)
|
|
37
|
+
if err != nil {
|
|
38
|
+
return nil, fmt.Errorf("failed to load configuration: %w", err)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
sessionCtx, err := loadSessionContext(cwd)
|
|
42
|
+
if err != nil {
|
|
43
|
+
return nil, err
|
|
44
|
+
}
|
|
45
|
+
defer sessionCtx.Store.Close()
|
|
46
|
+
|
|
47
|
+
execRoot := sessionCtx.ExecutionRoot(cwd)
|
|
48
|
+
|
|
49
|
+
task := &models.Task{
|
|
50
|
+
ID: fmt.Sprintf("task-%d", time.Now().UnixNano()),
|
|
51
|
+
Description: taskDescription,
|
|
52
|
+
CreatedAt: time.Now(),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
result := &runExecutionResult{
|
|
56
|
+
Task: task,
|
|
57
|
+
ProjectID: sessionCtx.ProjectID,
|
|
58
|
+
SessionName: sessionCtx.Session.Name,
|
|
59
|
+
Worktree: sessionCtx.Session.Worktree,
|
|
60
|
+
CWD: cwd,
|
|
61
|
+
ExecRoot: execRoot,
|
|
62
|
+
Warnings: make([]string, 0),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
svc := session.NewService(sessionCtx.Store)
|
|
66
|
+
if compacted, note, compactErr := svc.MaybeCompact(sessionCtx.Session.ID, cfg.Provider.OpenAI.Models.Coder); compactErr == nil && compacted {
|
|
67
|
+
result.Warnings = append(result.Warnings, note)
|
|
68
|
+
}
|
|
69
|
+
userMsg, sessionErr := svc.AppendText(session.MessageInput{
|
|
70
|
+
SessionID: sessionCtx.Session.ID,
|
|
71
|
+
Role: "user",
|
|
72
|
+
ProviderID: "orch",
|
|
73
|
+
ModelID: "run-engine",
|
|
74
|
+
Text: taskDescription,
|
|
75
|
+
})
|
|
76
|
+
if sessionErr != nil {
|
|
77
|
+
result.Warnings = append(result.Warnings, fmt.Sprintf("failed to persist run user message: %s", sessionErr))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
var unlock func() error
|
|
81
|
+
if cfg.Safety.FeatureFlags.RepoLock {
|
|
82
|
+
lockManager := runlock.NewLockManager(execRoot, time.Duration(cfg.Safety.LockStaleAfterSeconds)*time.Second)
|
|
83
|
+
unlock, err = lockManager.Acquire(task.ID)
|
|
84
|
+
if err != nil {
|
|
85
|
+
return nil, fmt.Errorf("run blocked by repository lock: %w", err)
|
|
86
|
+
}
|
|
87
|
+
defer func() {
|
|
88
|
+
if unlockErr := unlock(); unlockErr != nil {
|
|
89
|
+
result.Warnings = append(result.Warnings, fmt.Sprintf("failed to release lock: %s", unlockErr))
|
|
90
|
+
}
|
|
91
|
+
}()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
orch := orchestrator.New(cfg, execRoot, verbose)
|
|
95
|
+
state, runErr := orch.Run(task)
|
|
96
|
+
result.State = state
|
|
97
|
+
result.Err = runErr
|
|
98
|
+
|
|
99
|
+
if state != nil {
|
|
100
|
+
state.ProjectID = sessionCtx.ProjectID
|
|
101
|
+
state.SessionID = sessionCtx.Session.ID
|
|
102
|
+
if saveErr := sessionCtx.Store.SaveRunState(state); saveErr != nil {
|
|
103
|
+
result.Warnings = append(result.Warnings, fmt.Sprintf("failed to save SQLite run state: %s", saveErr))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
runSummary := summarizeRunForSession(result)
|
|
108
|
+
stagePayload := map[string]any{
|
|
109
|
+
"run_id": "",
|
|
110
|
+
"status": "failed",
|
|
111
|
+
"warnings": result.Warnings,
|
|
112
|
+
}
|
|
113
|
+
finishReason := "error"
|
|
114
|
+
errorText := ""
|
|
115
|
+
if state != nil {
|
|
116
|
+
stagePayload["run_id"] = state.ID
|
|
117
|
+
stagePayload["status"] = string(state.Status)
|
|
118
|
+
if state.Status == models.StatusCompleted && result.Err == nil {
|
|
119
|
+
finishReason = "stop"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if result.Err != nil {
|
|
123
|
+
errorText = result.Err.Error()
|
|
124
|
+
}
|
|
125
|
+
payloadBytes, _ := json.Marshal(stagePayload)
|
|
126
|
+
parentID := ""
|
|
127
|
+
if userMsg != nil {
|
|
128
|
+
parentID = userMsg.Message.ID
|
|
129
|
+
}
|
|
130
|
+
assistantParts := []storage.SessionPart{{Type: "stage", Payload: string(payloadBytes)}}
|
|
131
|
+
assistantParts = append(assistantParts, buildStagePartsFromRunState(state)...)
|
|
132
|
+
if _, appendErr := svc.AppendMessage(session.MessageInput{
|
|
133
|
+
SessionID: sessionCtx.Session.ID,
|
|
134
|
+
Role: "assistant",
|
|
135
|
+
ParentID: parentID,
|
|
136
|
+
ProviderID: "orch",
|
|
137
|
+
ModelID: "run-engine",
|
|
138
|
+
FinishReason: finishReason,
|
|
139
|
+
Error: errorText,
|
|
140
|
+
Text: runSummary,
|
|
141
|
+
}, assistantParts); appendErr != nil {
|
|
142
|
+
result.Warnings = append(result.Warnings, fmt.Sprintf("failed to persist run assistant message: %s", appendErr))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if compacted, note, compactErr := svc.MaybeCompact(sessionCtx.Session.ID, cfg.Provider.OpenAI.Models.Coder); compactErr == nil && compacted {
|
|
146
|
+
result.Warnings = append(result.Warnings, note)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result, nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func buildStagePartsFromRunState(state *models.RunState) []storage.SessionPart {
|
|
153
|
+
if state == nil || len(state.Logs) == 0 {
|
|
154
|
+
return []storage.SessionPart{}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
parts := make([]storage.SessionPart, 0, len(state.Logs))
|
|
158
|
+
for _, entry := range state.Logs {
|
|
159
|
+
payload := map[string]any{
|
|
160
|
+
"actor": strings.TrimSpace(entry.Actor),
|
|
161
|
+
"step": strings.TrimSpace(entry.Step),
|
|
162
|
+
"message": strings.TrimSpace(entry.Message),
|
|
163
|
+
"timestamp": entry.Timestamp.UTC().Format(time.RFC3339Nano),
|
|
164
|
+
}
|
|
165
|
+
body, err := json.Marshal(payload)
|
|
166
|
+
if err != nil {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
parts = append(parts, storage.SessionPart{Type: "stage", Payload: string(body)})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return parts
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func summarizeRunForSession(result *runExecutionResult) string {
|
|
176
|
+
if result == nil || result.State == nil {
|
|
177
|
+
if result != nil && result.Err != nil {
|
|
178
|
+
return fmt.Sprintf("Run failed: %s", result.Err.Error())
|
|
179
|
+
}
|
|
180
|
+
return "Run finished without state output"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
state := result.State
|
|
184
|
+
if result.Err != nil || state.Status == models.StatusFailed {
|
|
185
|
+
errText := strings.TrimSpace(state.Error)
|
|
186
|
+
if errText == "" && result.Err != nil {
|
|
187
|
+
errText = result.Err.Error()
|
|
188
|
+
}
|
|
189
|
+
if errText == "" {
|
|
190
|
+
errText = "unknown failure"
|
|
191
|
+
}
|
|
192
|
+
return fmt.Sprintf("Run failed at status=%s: %s", state.Status, errText)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if state.Patch == nil || len(state.Patch.Files) == 0 {
|
|
196
|
+
return fmt.Sprintf("Run completed with status=%s and no patch files.", state.Status)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return fmt.Sprintf("Run completed with status=%s and %d patch file(s).", state.Status, len(state.Patch.Files))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func getWorkingDirectory() (string, error) {
|
|
203
|
+
return osGetwd()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
var osGetwd = func() (string, error) {
|
|
207
|
+
return os.Getwd()
|
|
208
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
"time"
|
|
6
|
+
|
|
7
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestBuildStagePartsFromRunState(t *testing.T) {
|
|
11
|
+
state := &models.RunState{
|
|
12
|
+
Logs: []models.LogEntry{
|
|
13
|
+
{Timestamp: time.Now().UTC(), Actor: "planner", Step: "plan", Message: "Generating plan..."},
|
|
14
|
+
{Timestamp: time.Now().UTC(), Actor: "coder", Step: "code", Message: "Generating patch..."},
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
parts := buildStagePartsFromRunState(state)
|
|
19
|
+
if len(parts) != 2 {
|
|
20
|
+
t.Fatalf("expected 2 stage parts, got %d", len(parts))
|
|
21
|
+
}
|
|
22
|
+
for _, part := range parts {
|
|
23
|
+
if part.Type != "stage" {
|
|
24
|
+
t.Fatalf("expected stage part type, got %s", part.Type)
|
|
25
|
+
}
|
|
26
|
+
if part.Payload == "" {
|
|
27
|
+
t.Fatalf("expected non-empty payload")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|