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.
Files changed (116) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +624 -0
  4. package/cmd/apply.go +111 -0
  5. package/cmd/auth.go +393 -0
  6. package/cmd/auth_test.go +100 -0
  7. package/cmd/diff.go +57 -0
  8. package/cmd/doctor.go +149 -0
  9. package/cmd/explain.go +192 -0
  10. package/cmd/explain_test.go +62 -0
  11. package/cmd/init.go +100 -0
  12. package/cmd/interactive.go +1372 -0
  13. package/cmd/interactive_input.go +45 -0
  14. package/cmd/interactive_input_test.go +55 -0
  15. package/cmd/logs.go +72 -0
  16. package/cmd/model.go +84 -0
  17. package/cmd/plan.go +149 -0
  18. package/cmd/provider.go +189 -0
  19. package/cmd/provider_model_doctor_test.go +91 -0
  20. package/cmd/root.go +67 -0
  21. package/cmd/run.go +123 -0
  22. package/cmd/run_engine.go +208 -0
  23. package/cmd/run_engine_test.go +30 -0
  24. package/cmd/session.go +589 -0
  25. package/cmd/session_helpers.go +54 -0
  26. package/cmd/session_integration_test.go +30 -0
  27. package/cmd/session_list_current_test.go +87 -0
  28. package/cmd/session_messages_test.go +163 -0
  29. package/cmd/session_runs_test.go +68 -0
  30. package/cmd/sprint1_integration_test.go +119 -0
  31. package/cmd/stats.go +173 -0
  32. package/cmd/stats_test.go +71 -0
  33. package/cmd/version.go +4 -0
  34. package/go.mod +45 -0
  35. package/go.sum +108 -0
  36. package/internal/agents/agent.go +31 -0
  37. package/internal/agents/coder.go +167 -0
  38. package/internal/agents/planner.go +155 -0
  39. package/internal/agents/reviewer.go +118 -0
  40. package/internal/agents/runtime.go +25 -0
  41. package/internal/agents/runtime_test.go +77 -0
  42. package/internal/auth/account.go +78 -0
  43. package/internal/auth/oauth.go +523 -0
  44. package/internal/auth/store.go +287 -0
  45. package/internal/confidence/policy.go +174 -0
  46. package/internal/confidence/policy_test.go +71 -0
  47. package/internal/confidence/scorer.go +253 -0
  48. package/internal/confidence/scorer_test.go +83 -0
  49. package/internal/config/config.go +331 -0
  50. package/internal/config/config_defaults_test.go +138 -0
  51. package/internal/execution/contract_builder.go +160 -0
  52. package/internal/execution/contract_builder_test.go +68 -0
  53. package/internal/execution/plan_compliance.go +161 -0
  54. package/internal/execution/plan_compliance_test.go +71 -0
  55. package/internal/execution/retry_directive.go +132 -0
  56. package/internal/execution/scope_guard.go +69 -0
  57. package/internal/logger/logger.go +120 -0
  58. package/internal/models/contracts_test.go +100 -0
  59. package/internal/models/models.go +269 -0
  60. package/internal/orchestrator/orchestrator.go +701 -0
  61. package/internal/orchestrator/orchestrator_retry_test.go +135 -0
  62. package/internal/orchestrator/review_engine_test.go +50 -0
  63. package/internal/orchestrator/state.go +42 -0
  64. package/internal/orchestrator/test_classifier_test.go +68 -0
  65. package/internal/patch/applier.go +131 -0
  66. package/internal/patch/applier_test.go +25 -0
  67. package/internal/patch/parser.go +89 -0
  68. package/internal/patch/patch.go +60 -0
  69. package/internal/patch/summary.go +30 -0
  70. package/internal/patch/validator.go +104 -0
  71. package/internal/planning/normalizer.go +416 -0
  72. package/internal/planning/normalizer_test.go +64 -0
  73. package/internal/providers/errors.go +35 -0
  74. package/internal/providers/openai/client.go +498 -0
  75. package/internal/providers/openai/client_test.go +187 -0
  76. package/internal/providers/provider.go +47 -0
  77. package/internal/providers/registry.go +32 -0
  78. package/internal/providers/registry_test.go +57 -0
  79. package/internal/providers/router.go +52 -0
  80. package/internal/providers/state.go +114 -0
  81. package/internal/providers/state_test.go +64 -0
  82. package/internal/repo/analyzer.go +188 -0
  83. package/internal/repo/context.go +83 -0
  84. package/internal/review/engine.go +267 -0
  85. package/internal/review/engine_test.go +103 -0
  86. package/internal/runstore/store.go +137 -0
  87. package/internal/runstore/store_test.go +59 -0
  88. package/internal/runtime/lock.go +150 -0
  89. package/internal/runtime/lock_test.go +57 -0
  90. package/internal/session/compaction.go +260 -0
  91. package/internal/session/compaction_test.go +36 -0
  92. package/internal/session/service.go +117 -0
  93. package/internal/session/service_test.go +113 -0
  94. package/internal/storage/storage.go +1498 -0
  95. package/internal/storage/storage_test.go +413 -0
  96. package/internal/testing/classifier.go +80 -0
  97. package/internal/testing/classifier_test.go +36 -0
  98. package/internal/tools/command.go +160 -0
  99. package/internal/tools/command_test.go +56 -0
  100. package/internal/tools/file.go +111 -0
  101. package/internal/tools/git.go +77 -0
  102. package/internal/tools/invalid_params_test.go +36 -0
  103. package/internal/tools/policy.go +98 -0
  104. package/internal/tools/policy_test.go +36 -0
  105. package/internal/tools/registry_test.go +52 -0
  106. package/internal/tools/result.go +30 -0
  107. package/internal/tools/search.go +86 -0
  108. package/internal/tools/tool.go +94 -0
  109. package/main.go +9 -0
  110. package/npm/orch.js +25 -0
  111. package/package.json +41 -0
  112. package/scripts/changelog.js +20 -0
  113. package/scripts/check-release-version.js +21 -0
  114. package/scripts/lib/release-utils.js +223 -0
  115. package/scripts/postinstall.js +157 -0
  116. 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
+ }