relay-flow 0.0.1 → 0.2.0-alpha
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/README.md +148 -143
- package/cmd/relay-flow/commands_test.go +464 -0
- package/cmd/relay-flow/main.go +670 -180
- package/cmd/relay-flow/scenario_test.go +1135 -0
- package/cmd/relay-flow/serve.go +609 -0
- package/go.mod +69 -2
- package/go.sum +185 -0
- package/internal/config/config.go +88 -0
- package/internal/config/machine.go +99 -48
- package/internal/config/machine_test.go +248 -0
- package/internal/config/merge_test.go +118 -0
- package/internal/config/writeatomic.go +36 -0
- package/internal/config/writeatomic_test.go +98 -0
- package/internal/execution/goworkflows/activities.go +490 -0
- package/internal/execution/goworkflows/engine.go +487 -0
- package/internal/execution/goworkflows/engine_test.go +600 -0
- package/internal/execution/goworkflows/fakes_test.go +517 -0
- package/internal/execution/goworkflows/interpreter.go +605 -0
- package/internal/execution/goworkflows/logging_test.go +154 -0
- package/internal/execution/goworkflows/mailbox_test.go +423 -0
- package/internal/execution/goworkflows/node_runtime_integration_test.go +127 -0
- package/internal/execution/goworkflows/node_runtime_test.go +486 -0
- package/internal/execution/goworkflows/projection.go +504 -0
- package/internal/execution/goworkflows/recovery_test.go +1092 -0
- package/internal/execution/goworkflows/retry_log_test.go +59 -0
- package/internal/execution/goworkflows/retry_projection_test.go +98 -0
- package/internal/harness/contract_test.go +169 -0
- package/internal/harness/factory.go +63 -0
- package/internal/harness/harness.go +41 -0
- package/internal/harness/opencode/opencode.go +166 -0
- package/internal/harness/opencode/opencode_test.go +50 -0
- package/internal/harness/plugin_selection_test.go +126 -0
- package/internal/identity/identity.go +37 -0
- package/internal/logging/logging.go +56 -0
- package/internal/logging/logging_test.go +116 -0
- package/internal/paths/paths.go +67 -0
- package/internal/recover/recover.go +115 -0
- package/internal/repo/poller.go +186 -0
- package/internal/repo/poller_test.go +327 -0
- package/internal/repo/repo.go +119 -0
- package/internal/repo/service.go +216 -0
- package/internal/repo/service_test.go +298 -0
- package/internal/retry/retry.go +118 -0
- package/internal/router/router.go +83 -0
- package/internal/router/router_test.go +144 -0
- package/internal/run/manager.go +108 -0
- package/internal/run/run.go +140 -0
- package/internal/run/run_identity_test.go +52 -0
- package/internal/run/run_manager_test.go +266 -0
- package/internal/runner/contract_test.go +221 -0
- package/internal/runner/factory.go +65 -0
- package/internal/runner/orca/orca.go +363 -170
- package/internal/runner/orca/orca_test.go +134 -160
- package/internal/runner/orca/orcacli/orcacli.go +215 -0
- package/internal/runner/orca/orcacli/orcacli_test.go +154 -0
- package/internal/runner/orca/orcacli/testdata/repo-list.json +18 -0
- package/internal/runner/orca/orcacli/testdata/strict-orca.sh +30 -0
- package/internal/runner/orca/orcacli/testdata/terminal-close.json +12 -0
- package/internal/runner/orca/orcacli/testdata/terminal-create.json +18 -0
- package/internal/runner/orca/orcacli/testdata/terminal-list.json +51 -0
- package/internal/runner/orca/orcacli/testdata/terminal-send.json +1 -0
- package/internal/runner/orca/orcacli/testdata/terminal-show.json +1 -0
- package/internal/runner/orca/orcacli/testdata/worktree-create.json +22 -0
- package/internal/runner/orca/orcacli/testdata/worktree-list.json +31 -0
- package/internal/runner/orca/orcacli/testdata/worktree-remove.json +6 -0
- package/internal/runner/runner.go +47 -64
- package/internal/server/api_test.go +300 -0
- package/internal/server/client.go +192 -74
- package/internal/server/fixture_test.go +248 -0
- package/internal/server/server.go +425 -248
- package/internal/server/shutdown_test.go +116 -0
- package/internal/task/contract_test.go +223 -0
- package/internal/task/factory.go +103 -0
- package/internal/task/jira/acli/acli.go +306 -0
- package/internal/task/jira/acli/acli_test.go +208 -0
- package/internal/task/jira/acli/testdata/acli_comments.json +55 -0
- package/internal/task/jira/acli/testdata/search_invalid_assignee.txt +1 -0
- package/internal/task/jira/acli/testdata/search_invalid_status.txt +1 -0
- package/internal/task/jira/acli/testdata/search_success.json +1 -0
- package/internal/task/jira/filters_test.go +234 -0
- package/internal/task/jira/helpers_test.go +60 -0
- package/internal/task/jira/jira.go +507 -0
- package/internal/task/jira/normalize.go +101 -0
- package/internal/task/jira/testdata/acli_search.json +120 -0
- package/internal/task/jira/transition_defaults_test.go +156 -0
- package/internal/task/jira/validation_test.go +94 -0
- package/internal/task/task.go +84 -0
- package/internal/workflow/report.go +85 -0
- package/internal/workflow/report_test.go +259 -0
- package/internal/workflow/service.go +142 -0
- package/internal/workflow/store.go +136 -0
- package/internal/workflow/store_test.go +282 -0
- package/internal/workflow/workflow.go +342 -0
- package/internal/workflow/workflow_test.go +410 -0
- package/package.json +1 -1
- package/internal/acli/acli.go +0 -229
- package/internal/config/demo_test.go +0 -17
- package/internal/config/schema.go +0 -193
- package/internal/config/schema_test.go +0 -162
- package/internal/daemon/daemon.go +0 -218
- package/internal/daemon/daemon_test.go +0 -204
- package/internal/discovery/discovery.go +0 -122
- package/internal/discovery/discovery_test.go +0 -62
- package/internal/opencode/opencode.go +0 -26
- package/internal/orcacli/orcacli.go +0 -264
- package/internal/runner/orca/README.md +0 -64
- package/internal/runner/runner_test.go +0 -64
- package/internal/server/server_test.go +0 -195
- package/internal/tasks/jira/README.md +0 -69
- package/internal/tasks/jira/component_test.go +0 -16
- package/internal/tasks/jira/decode.go +0 -24
- package/internal/tasks/jira/jira.go +0 -231
- package/internal/tasks/jira/jira_test.go +0 -259
- package/internal/tasks/jira/jql_test.go +0 -16
- package/internal/tasks/tasks.go +0 -90
- package/internal/tasks/tasks_test.go +0 -91
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
// Composition root for `relay-flow serve` (task 5.1). Explicit ordered Go
|
|
4
|
+
// wiring per docs/structs-methods-interfaces.md "Startup Wiring"
|
|
5
|
+
// (lines 1081-1097) and design.md decision 24. The flock on server.lock is
|
|
6
|
+
// acquired FIRST; startup refuses to run if another relay-flow process
|
|
7
|
+
// holds it. No container, no DI framework.
|
|
8
|
+
|
|
9
|
+
import (
|
|
10
|
+
"context"
|
|
11
|
+
"errors"
|
|
12
|
+
"fmt"
|
|
13
|
+
"log/slog"
|
|
14
|
+
"net"
|
|
15
|
+
"net/http"
|
|
16
|
+
"os"
|
|
17
|
+
"strings"
|
|
18
|
+
"sync"
|
|
19
|
+
"syscall"
|
|
20
|
+
"time"
|
|
21
|
+
|
|
22
|
+
"github.com/rajpopat27/relay-flow/internal/config"
|
|
23
|
+
"github.com/rajpopat27/relay-flow/internal/execution/goworkflows"
|
|
24
|
+
"github.com/rajpopat27/relay-flow/internal/harness"
|
|
25
|
+
"github.com/rajpopat27/relay-flow/internal/paths"
|
|
26
|
+
recoverpkg "github.com/rajpopat27/relay-flow/internal/recover"
|
|
27
|
+
"github.com/rajpopat27/relay-flow/internal/repo"
|
|
28
|
+
"github.com/rajpopat27/relay-flow/internal/router"
|
|
29
|
+
runsvc "github.com/rajpopat27/relay-flow/internal/run"
|
|
30
|
+
"github.com/rajpopat27/relay-flow/internal/runner"
|
|
31
|
+
"github.com/rajpopat27/relay-flow/internal/server"
|
|
32
|
+
"github.com/rajpopat27/relay-flow/internal/task"
|
|
33
|
+
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
34
|
+
|
|
35
|
+
// Adapter registrations (factories are registered by name via init).
|
|
36
|
+
_ "github.com/rajpopat27/relay-flow/internal/harness/opencode"
|
|
37
|
+
_ "github.com/rajpopat27/relay-flow/internal/runner/orca"
|
|
38
|
+
_ "github.com/rajpopat27/relay-flow/internal/task/jira"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// serveRoot is the composition root. It runs the documented startup order
|
|
42
|
+
// and blocks until ctx is canceled, then shuts down in reverse.
|
|
43
|
+
//
|
|
44
|
+
// Order (docs/structs-methods-interfaces.md lines 1081-1097, verbatim):
|
|
45
|
+
//
|
|
46
|
+
// flock → load machine config → select task/runner/harness factories →
|
|
47
|
+
// construct shared runner/harness → load repos + one task.System per repo →
|
|
48
|
+
// load workflow files → validate each workflow against every referenced
|
|
49
|
+
// repo task system → bind workflows+matchers to repos → open go-workflows
|
|
50
|
+
// SQLite engine and start its workers → construct the Run Manager →
|
|
51
|
+
// start the Repo Poller group → start the Unix-socket server.
|
|
52
|
+
func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
53
|
+
// Flock FIRST (docs lines 1028/1142): refuse to start if another
|
|
54
|
+
// process holds it. The only pre-flock side effect is creating the
|
|
55
|
+
// Root directory so the lock file itself can exist; nothing else in
|
|
56
|
+
// the layout is touched until the lock is held.
|
|
57
|
+
if err := os.MkdirAll(p.Root, 0o700); err != nil {
|
|
58
|
+
return fmt.Errorf("create root %s: %w", p.Root, err)
|
|
59
|
+
}
|
|
60
|
+
if err := os.Chmod(p.Root, 0o700); err != nil {
|
|
61
|
+
return fmt.Errorf("chmod root %s: %w", p.Root, err)
|
|
62
|
+
}
|
|
63
|
+
lockFile, err := os.OpenFile(p.Lock, os.O_CREATE|os.O_RDWR, 0o600)
|
|
64
|
+
if err != nil {
|
|
65
|
+
return fmt.Errorf("open lock %s: %w", p.Lock, err)
|
|
66
|
+
}
|
|
67
|
+
defer lockFile.Close()
|
|
68
|
+
if err := lockFile.Chmod(0o600); err != nil {
|
|
69
|
+
return fmt.Errorf("chmod lock %s: %w", p.Lock, err)
|
|
70
|
+
}
|
|
71
|
+
if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
72
|
+
return fmt.Errorf("another relay-flow server holds %s", p.Lock)
|
|
73
|
+
}
|
|
74
|
+
defer func() { _ = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN) }()
|
|
75
|
+
|
|
76
|
+
// Single-instance held; materialize the rest of the layout.
|
|
77
|
+
if err := paths.Ensure(p); err != nil {
|
|
78
|
+
return err
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Load machine config.
|
|
82
|
+
cfg, err := config.LoadMachine(p.Config)
|
|
83
|
+
if err != nil {
|
|
84
|
+
return err
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Select factories; construct shared runner and harness.
|
|
88
|
+
rnr, err := runner.New(cfg.RunnerPlugin, cfg.RunnerConfig)
|
|
89
|
+
if err != nil {
|
|
90
|
+
return fmt.Errorf("runner plugin %q: %w", cfg.RunnerPlugin, err)
|
|
91
|
+
}
|
|
92
|
+
hrn, err := harness.New(cfg.HarnessPlugin, cfg.HarnessConfig)
|
|
93
|
+
if err != nil {
|
|
94
|
+
return fmt.Errorf("harness plugin %q: %w", cfg.HarnessPlugin, err)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Load registered repos and one task.System per repo. The registry is
|
|
98
|
+
// shared with repo.Service (constructed below) so the engine, pollers,
|
|
99
|
+
// and server handlers observe the same in-memory set.
|
|
100
|
+
repoReg := repo.NewRegistry()
|
|
101
|
+
for name, rc := range cfg.Repos {
|
|
102
|
+
ts, err := task.New(ctx, cfg.TaskPlugin, task.RepoSpec{
|
|
103
|
+
Name: name,
|
|
104
|
+
Path: rc.Path,
|
|
105
|
+
RootConfig: cfg.TaskConfig,
|
|
106
|
+
RepoConfig: rc.TaskConfig,
|
|
107
|
+
})
|
|
108
|
+
if err != nil {
|
|
109
|
+
return fmt.Errorf("repo %q task system: %w", name, err)
|
|
110
|
+
}
|
|
111
|
+
repoReg.Replace(&repo.Repo{
|
|
112
|
+
Name: name,
|
|
113
|
+
Path: rc.Path,
|
|
114
|
+
TaskConfig: rc.TaskConfig,
|
|
115
|
+
TaskSystem: ts,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Load workflow files.
|
|
120
|
+
store := &workflow.Store{Dir: p.Workflows}
|
|
121
|
+
workflowList, err := store.LoadAll()
|
|
122
|
+
if err != nil {
|
|
123
|
+
return fmt.Errorf("load workflows: %w", err)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate each workflow structurally (lifecycle nodes, routes,
|
|
127
|
+
// reachability, nudge templates) before any per-repo validation or
|
|
128
|
+
// binding; an invalid stored workflow must fail startup, not bind.
|
|
129
|
+
for _, wf := range workflowList {
|
|
130
|
+
if err := wf.Validate(); err != nil {
|
|
131
|
+
return fmt.Errorf("workflow %q: %w", wf.Name, err)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Validate each workflow against every referenced repo task system.
|
|
136
|
+
for _, wf := range workflowList {
|
|
137
|
+
nodeCfgs := map[string]config.RawValues{}
|
|
138
|
+
for nodeName, n := range wf.Nodes {
|
|
139
|
+
nodeCfgs[nodeName] = n.TaskConfig
|
|
140
|
+
}
|
|
141
|
+
for _, repoName := range wf.Repos {
|
|
142
|
+
rp, ok := repoReg.Get(repoName)
|
|
143
|
+
if !ok {
|
|
144
|
+
return fmt.Errorf("workflow %q references unregistered repo %q", wf.Name, repoName)
|
|
145
|
+
}
|
|
146
|
+
if err := rp.TaskSystem.ValidateConfig(ctx, wf.TaskConfig, nodeCfgs); err != nil {
|
|
147
|
+
return fmt.Errorf("workflow %q repo %q: %w", wf.Name, repoName, err)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Bind workflows and compiled matchers to repos.
|
|
153
|
+
if err := repoReg.BindWorkflows(workflowList); err != nil {
|
|
154
|
+
return err
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 5.5 normal-start refusal: serve REQUIRES an initialized database
|
|
158
|
+
// (init creates it); --recover is the explicit opt-out.
|
|
159
|
+
if !recover {
|
|
160
|
+
if _, err := os.Stat(p.Database); err != nil {
|
|
161
|
+
if os.IsNotExist(err) {
|
|
162
|
+
return fmt.Errorf("database %s is missing; run `relay-flow init` first, or `relay-flow serve --recover` to rebuild from the task system", p.Database)
|
|
163
|
+
}
|
|
164
|
+
return fmt.Errorf("stat database %s: %w", p.Database, err)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 5.6 (decision 22): --recover replaces the execution state BEFORE any
|
|
169
|
+
// worker starts, with two explicit guarantees:
|
|
170
|
+
// (a) The flock acquired at the top of serveRoot proves no other
|
|
171
|
+
// relay-flow process has this database open — relay-flow is the
|
|
172
|
+
// only process that ever opens state.db, and we hold server.lock.
|
|
173
|
+
// (b) The old database is moved aside as a timestamped sibling (never
|
|
174
|
+
// destroyed in place) and the destination path is verified absent
|
|
175
|
+
// before goworkflows.New runs. Engine.New therefore opens a
|
|
176
|
+
// provably empty database, and Engine.Start has no prior history
|
|
177
|
+
// to resume.
|
|
178
|
+
if recover {
|
|
179
|
+
stamp := time.Now().UTC().Format("20060102T150405Z")
|
|
180
|
+
for _, suffix := range []string{"", "-wal", "-shm"} {
|
|
181
|
+
src := p.Database + suffix
|
|
182
|
+
if _, err := os.Stat(src); os.IsNotExist(err) {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
dst := fmt.Sprintf("%s.recover-%s.bak%s", p.Database, stamp, suffix)
|
|
186
|
+
if err := os.Rename(src, dst); err != nil {
|
|
187
|
+
return fmt.Errorf("preserve stale database %s as %s: %w", src, dst, err)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Provably-empty engine: the primary DB path MUST NOT exist before
|
|
191
|
+
// engine construction. If it does, the rename-aside above failed
|
|
192
|
+
// silently for an unexpected reason; refuse to start rather than
|
|
193
|
+
// resume stale state.
|
|
194
|
+
if _, err := os.Stat(p.Database); err == nil {
|
|
195
|
+
return fmt.Errorf("recover: %s still exists after rename-aside; refusing to open possibly-stale database", p.Database)
|
|
196
|
+
} else if !os.IsNotExist(err) {
|
|
197
|
+
return fmt.Errorf("recover: stat %s: %w", p.Database, err)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Open the go-workflows SQLite engine (migrates relay_runs; does not
|
|
202
|
+
// yet start workers).
|
|
203
|
+
engine, err := goworkflows.New(p.Database, goworkflows.Dependencies{
|
|
204
|
+
Repos: repoReg,
|
|
205
|
+
Runner: rnr,
|
|
206
|
+
Harness: hrn,
|
|
207
|
+
RetentionDays: cfg.CompletedRunRetentionDays,
|
|
208
|
+
Runtime: &runsvc.RuntimePolicy{
|
|
209
|
+
KeepTerminalsAlive: cfg.KeepTerminalsAlive,
|
|
210
|
+
KeepSessionsAlive: cfg.KeepSessionsAlive,
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
if err != nil {
|
|
214
|
+
return fmt.Errorf("open engine: %w", err)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 5.2 fail-fast preflight: before workers/pollers start, validate
|
|
218
|
+
// task-system, runner, and harness credentials/permissions/connectivity,
|
|
219
|
+
// every registered repo, and every configured agent. Known permanent
|
|
220
|
+
// errors abort startup; runtime failures of existing runs retry forever
|
|
221
|
+
// (design.md decision 16).
|
|
222
|
+
if err := preflight(ctx, repoReg, rnr, hrn, workflowList); err != nil {
|
|
223
|
+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
224
|
+
_ = engine.Shutdown(shutdownCtx)
|
|
225
|
+
cancel()
|
|
226
|
+
return fmt.Errorf("startup validation: %w", err)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Start engine workers and the one-shot startup retention sweep
|
|
230
|
+
// (3.25; runs once here, no background ticker).
|
|
231
|
+
if err := engine.Start(ctx); err != nil {
|
|
232
|
+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
233
|
+
_ = engine.Shutdown(shutdownCtx)
|
|
234
|
+
cancel()
|
|
235
|
+
return fmt.Errorf("start engine: %w", err)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 5.4 lifecycle gate: ONE sync.Mutex shared by workflow.Service
|
|
239
|
+
// Submit/Remove and RunManager.EnsureRun (design.md decision 23). Plain
|
|
240
|
+
// mutex in the wiring — no lock service, no new abstraction.
|
|
241
|
+
lifecycleGate := &sync.Mutex{}
|
|
242
|
+
|
|
243
|
+
// Construct the Run Manager.
|
|
244
|
+
runManager := &runsvc.RunManager{Executor: engine, Runs: engine, Gate: lifecycleGate}
|
|
245
|
+
|
|
246
|
+
// Services consumed by the Unix-socket server. The repo.Service takes
|
|
247
|
+
// the SAME registry by pointer (via replaceInternal), so the engine,
|
|
248
|
+
// pollers, and handlers observe one in-memory repo set.
|
|
249
|
+
wfSvc := workflow.NewService(store, engine, repoExists{repoReg})
|
|
250
|
+
wfSvc.Gate = lifecycleGate
|
|
251
|
+
wfSvc.ValidateTaskConfig = workflowConfigValidator(repoReg)
|
|
252
|
+
// Submit/Remove must also rebuild repo bindings under the gate (spec
|
|
253
|
+
// 3.34: bindings rebuilt on submit/remove/startup). Wired as a plain
|
|
254
|
+
// callback — no dispatcher.
|
|
255
|
+
wfSvc.Rebind = func() error {
|
|
256
|
+
return repoReg.BindWorkflows(wfSvc.Registry().List())
|
|
257
|
+
}
|
|
258
|
+
for _, wf := range workflowList {
|
|
259
|
+
wfSvc.Registry().Replace(wf)
|
|
260
|
+
}
|
|
261
|
+
repoSvc := repo.NewServiceWithRegistry(repo.ServiceConfig{
|
|
262
|
+
ConfigPath: p.Config,
|
|
263
|
+
TaskPlugin: cfg.TaskPlugin,
|
|
264
|
+
Runner: rnr,
|
|
265
|
+
Active: engine,
|
|
266
|
+
Workflows: wfSvc.Registry(),
|
|
267
|
+
}, repoReg)
|
|
268
|
+
|
|
269
|
+
// 5.6 database-loss recovery (explicit --recover only; never inferred).
|
|
270
|
+
// Runs after the engine is open and BEFORE normal pollers start so the
|
|
271
|
+
// recovered runs exist before any poll cycle observes them. Mailbox
|
|
272
|
+
// specs come from the engine so the recover path builds the same
|
|
273
|
+
// description content as normal run execution.
|
|
274
|
+
if recover {
|
|
275
|
+
specsFor := func(wf *workflow.Workflow, ticketKey string) []task.MailboxSpec {
|
|
276
|
+
return goworkflows.MailboxSpecs(wf, ticketKey)
|
|
277
|
+
}
|
|
278
|
+
if err := recoverpkg.FromTaskSystem(ctx, repoReg, rnr, runManager, specsFor); err != nil {
|
|
279
|
+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
280
|
+
_ = engine.Shutdown(shutdownCtx)
|
|
281
|
+
cancel()
|
|
282
|
+
return fmt.Errorf("recover: %w", err)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Start the Repo Poller group with the handleBatch callback (5.3,
|
|
287
|
+
// docs/structs-methods-interfaces.md lines 1099-1117 verbatim). No
|
|
288
|
+
// dispatcher type, no extra abstraction.
|
|
289
|
+
pollers := repo.NewPollerGroup(10, handleBatch(runManager))
|
|
290
|
+
pollers.Interval = time.Duration(cfg.PollIntervalSeconds) * time.Second
|
|
291
|
+
pollers.ReplaceRepos(repoSvc.Registry().List())
|
|
292
|
+
pollerCtx, cancelPollers := context.WithCancel(context.Background())
|
|
293
|
+
pollersDone := make(chan struct{})
|
|
294
|
+
go func() {
|
|
295
|
+
pollers.Run(pollerCtx)
|
|
296
|
+
close(pollersDone)
|
|
297
|
+
}()
|
|
298
|
+
|
|
299
|
+
// cleanup is the single bounded shutdown path (5.5). Order:
|
|
300
|
+
// 1. Stop accepting new polls AND new HTTP requests immediately.
|
|
301
|
+
// 2. Wait (within one 30s budget) for in-flight polls and in-flight
|
|
302
|
+
// HTTP calls to return.
|
|
303
|
+
// 3. Cancel worker polling and close SQLite via engine.Shutdown.
|
|
304
|
+
// Every termination path — signal, /stop, listener error, startup
|
|
305
|
+
// failure after pollers launched — funnels through here so no path
|
|
306
|
+
// closes the database under running handlers or skips poller join.
|
|
307
|
+
cleanup := func(httpServer *http.Server, serveErr chan error, serveConsumed bool) {
|
|
308
|
+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
309
|
+
defer cancel()
|
|
310
|
+
// Stop new work immediately: no new polls, no new HTTP requests.
|
|
311
|
+
cancelPollers()
|
|
312
|
+
if httpServer != nil {
|
|
313
|
+
_ = httpServer.Shutdown(shutdownCtx)
|
|
314
|
+
}
|
|
315
|
+
// Wait for in-flight HTTP within the budget.
|
|
316
|
+
if httpServer != nil && !serveConsumed {
|
|
317
|
+
select {
|
|
318
|
+
case <-serveErr:
|
|
319
|
+
case <-shutdownCtx.Done():
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Wait for in-flight pollers within the same 30s budget (spec:
|
|
323
|
+
// "waits up to 30 seconds ... then close the socket and database").
|
|
324
|
+
// PollerGroup.Run returns once every in-flight handleBatch handler
|
|
325
|
+
// completes; on deadline we proceed to engine.Shutdown anyway so
|
|
326
|
+
// shutdown is bounded — the spec's rule is that already-running
|
|
327
|
+
// external work is not interruptible and durable state must remain
|
|
328
|
+
// recoverable (engine history is authoritative; a handler that
|
|
329
|
+
// survives past deadline hits a closed-DB error and logs it).
|
|
330
|
+
select {
|
|
331
|
+
case <-pollersDone:
|
|
332
|
+
case <-shutdownCtx.Done():
|
|
333
|
+
}
|
|
334
|
+
// Cancel worker polling and close the database. Engine.Shutdown
|
|
335
|
+
// uses the shared budget for its own bounded wait on active
|
|
336
|
+
// workflow/activity tasks.
|
|
337
|
+
_ = engine.Shutdown(shutdownCtx)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Start the Unix-socket server.
|
|
341
|
+
_ = os.Remove(p.Socket)
|
|
342
|
+
listener, err := net.Listen("unix", p.Socket)
|
|
343
|
+
if err != nil {
|
|
344
|
+
cleanup(nil, nil, true)
|
|
345
|
+
return fmt.Errorf("listen %s: %w", p.Socket, err)
|
|
346
|
+
}
|
|
347
|
+
if err := os.Chmod(p.Socket, 0o600); err != nil {
|
|
348
|
+
listener.Close()
|
|
349
|
+
_ = os.Remove(p.Socket) // leave no stale socket file on failure
|
|
350
|
+
cleanup(nil, nil, true)
|
|
351
|
+
return fmt.Errorf("chmod %s: %w", p.Socket, err)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// /stop and ctx cancellation funnel into stopCtx; both end at the
|
|
355
|
+
// single cleanup path.
|
|
356
|
+
stopCtx, stopServe := context.WithCancel(context.Background())
|
|
357
|
+
defer stopServe()
|
|
358
|
+
|
|
359
|
+
deps := &serveDeps{
|
|
360
|
+
wf: wfSvc,
|
|
361
|
+
repos: repoSvc,
|
|
362
|
+
engine: engine,
|
|
363
|
+
runManager: runManager,
|
|
364
|
+
// Repo register/remove must reach the running poller group — the
|
|
365
|
+
// startup ReplaceRepos only covers repos present at boot. Re-sync
|
|
366
|
+
// after every successful mutation so a newly registered repo is
|
|
367
|
+
// polled on the next tick without a serve restart.
|
|
368
|
+
onReposChanged: func() {
|
|
369
|
+
pollers.ReplaceRepos(repoSvc.Registry().List())
|
|
370
|
+
},
|
|
371
|
+
shutdown: func(context.Context) error {
|
|
372
|
+
stopServe()
|
|
373
|
+
return nil
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
httpServer := &http.Server{Handler: server.New(deps)}
|
|
377
|
+
|
|
378
|
+
serveErr := make(chan error, 1)
|
|
379
|
+
go func() { serveErr <- httpServer.Serve(listener) }()
|
|
380
|
+
|
|
381
|
+
var serveResult error
|
|
382
|
+
serveConsumed := false
|
|
383
|
+
select {
|
|
384
|
+
case <-ctx.Done():
|
|
385
|
+
case <-stopCtx.Done():
|
|
386
|
+
case err := <-serveErr:
|
|
387
|
+
serveConsumed = true
|
|
388
|
+
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
389
|
+
serveResult = fmt.Errorf("serve %s: %w", p.Socket, err)
|
|
390
|
+
}
|
|
391
|
+
// Listener closed on its own — treat as normal shutdown request.
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
cleanup(httpServer, serveErr, serveConsumed)
|
|
395
|
+
return serveResult
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
func workflowConfigValidator(repoReg *repo.Registry) func(context.Context, *workflow.Workflow) error {
|
|
399
|
+
return func(ctx context.Context, wf *workflow.Workflow) error {
|
|
400
|
+
nodeCfgs := map[string]config.RawValues{}
|
|
401
|
+
for nodeName, n := range wf.Nodes {
|
|
402
|
+
nodeCfgs[nodeName] = n.TaskConfig
|
|
403
|
+
}
|
|
404
|
+
for _, repoName := range wf.Repos {
|
|
405
|
+
rp, ok := repoReg.Get(repoName)
|
|
406
|
+
if !ok {
|
|
407
|
+
return fmt.Errorf("workflow %q references unregistered repo %q", wf.Name, repoName)
|
|
408
|
+
}
|
|
409
|
+
if err := rp.TaskSystem.ValidateConfig(ctx, wf.TaskConfig, nodeCfgs); err != nil {
|
|
410
|
+
return fmt.Errorf("workflow %q repo %q: %w", wf.Name, repoName, err)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return nil
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// repoExists adapts *repo.Registry to workflow.RepoLookup (Exists).
|
|
418
|
+
type repoExists struct{ reg *repo.Registry }
|
|
419
|
+
|
|
420
|
+
func (r repoExists) Exists(name string) bool {
|
|
421
|
+
_, ok := r.reg.Get(name)
|
|
422
|
+
return ok
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// serveDeps adapts composition-root services to server.Deps. Thin forwarder;
|
|
426
|
+
// no logic. Signatures match docs/structs-methods-interfaces.md Client.
|
|
427
|
+
type serveDeps struct {
|
|
428
|
+
wf *workflow.Service
|
|
429
|
+
repos *repo.Service
|
|
430
|
+
engine *goworkflows.Engine
|
|
431
|
+
runManager *runsvc.RunManager
|
|
432
|
+
onReposChanged func()
|
|
433
|
+
shutdown func(context.Context) error
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
func (d *serveDeps) SubmitWorkflow(ctx context.Context, yaml []byte) (*workflow.Workflow, error) {
|
|
437
|
+
return d.wf.Submit(ctx, yaml)
|
|
438
|
+
}
|
|
439
|
+
func (d *serveDeps) GetWorkflow(_ context.Context, name string) (*workflow.Workflow, error) {
|
|
440
|
+
return d.wf.Get(name)
|
|
441
|
+
}
|
|
442
|
+
func (d *serveDeps) ListWorkflows(context.Context) ([]*workflow.Workflow, error) {
|
|
443
|
+
return d.wf.List(), nil
|
|
444
|
+
}
|
|
445
|
+
func (d *serveDeps) RemoveWorkflow(ctx context.Context, name string) error {
|
|
446
|
+
return d.wf.Remove(ctx, name)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
func (d *serveDeps) ListRuns(ctx context.Context, filter runsvc.Filter) ([]runsvc.Run, error) {
|
|
450
|
+
return d.engine.ListRuns(ctx, filter)
|
|
451
|
+
}
|
|
452
|
+
func (d *serveDeps) GetRunByTicket(ctx context.Context, ticket string) (runsvc.Run, error) {
|
|
453
|
+
return d.engine.FindRunByTicket(ctx, ticket)
|
|
454
|
+
}
|
|
455
|
+
func (d *serveDeps) CancelRun(ctx context.Context, ticket, reason string) error {
|
|
456
|
+
return d.runManager.CancelByTicket(ctx, ticket, reason)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
func (d *serveDeps) SubmitReport(ctx context.Context, rep runsvc.ReportRequest) (runsvc.ReportAck, error) {
|
|
460
|
+
return d.engine.SubmitReport(ctx, rep)
|
|
461
|
+
}
|
|
462
|
+
func (d *serveDeps) HasProcessedReport(ctx context.Context, id runsvc.ID, reportID string) (bool, error) {
|
|
463
|
+
return d.engine.HasProcessedReport(ctx, id, reportID)
|
|
464
|
+
}
|
|
465
|
+
func (d *serveDeps) RegisterNodeSession(ctx context.Context, registration runsvc.NodeRuntimeRegistration) (runsvc.NodeRuntimeRegistrationAck, error) {
|
|
466
|
+
return d.engine.RegisterNodeSession(ctx, registration)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
func (d *serveDeps) DiscoverRepos(ctx context.Context) ([]runner.RepoCandidate, error) {
|
|
470
|
+
return d.repos.Discover(ctx)
|
|
471
|
+
}
|
|
472
|
+
func (d *serveDeps) TaskFields(context.Context) ([]string, error) {
|
|
473
|
+
return d.repos.RequiredRepoKeys(), nil
|
|
474
|
+
}
|
|
475
|
+
func (d *serveDeps) RegisterRepo(ctx context.Context, input repo.RegisterInput) (repo.Info, error) {
|
|
476
|
+
info, err := d.repos.Register(ctx, input)
|
|
477
|
+
if err == nil && d.onReposChanged != nil {
|
|
478
|
+
d.onReposChanged()
|
|
479
|
+
}
|
|
480
|
+
return info, err
|
|
481
|
+
}
|
|
482
|
+
func (d *serveDeps) ListRepos(context.Context) ([]repo.Info, error) {
|
|
483
|
+
return d.repos.List(), nil
|
|
484
|
+
}
|
|
485
|
+
func (d *serveDeps) GetRepo(_ context.Context, name string) (repo.Info, error) {
|
|
486
|
+
return d.repos.Get(name)
|
|
487
|
+
}
|
|
488
|
+
func (d *serveDeps) RemoveRepo(ctx context.Context, name string) error {
|
|
489
|
+
err := d.repos.Remove(ctx, name)
|
|
490
|
+
if err == nil && d.onReposChanged != nil {
|
|
491
|
+
d.onReposChanged()
|
|
492
|
+
}
|
|
493
|
+
return err
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
func (d *serveDeps) Shutdown(ctx context.Context) error { return d.shutdown(ctx) }
|
|
497
|
+
|
|
498
|
+
// handleBatch is the Repo Poller group callback (5.3). Implemented verbatim
|
|
499
|
+
// from docs/structs-methods-interfaces.md lines 1101-1117: per ticket,
|
|
500
|
+
// ResolveWorkflow; ErrNoMatch → continue; other routing errors → log and
|
|
501
|
+
// continue (mutate nothing); success → RunManager.EnsureRun, log its errors.
|
|
502
|
+
// No dispatcher type, no extra abstraction. A small closure captures the
|
|
503
|
+
// RunManager so the repo.BatchHandler signature is satisfied.
|
|
504
|
+
//
|
|
505
|
+
// 9.2 poller logging: one info line per repo poll cycle (batch size), one
|
|
506
|
+
// info line per ticket with the routing outcome, one debug line per ticket
|
|
507
|
+
// with normalized fields. EnsureRun emits its own outcome lines from
|
|
508
|
+
// internal/run/manager.go.
|
|
509
|
+
func handleBatch(runManager *runsvc.RunManager) repo.BatchHandler {
|
|
510
|
+
return func(ctx context.Context, rp *repo.Repo, tickets []task.Ticket) {
|
|
511
|
+
slog.Info("poll cycle", "repo", rp.Name, "batch", len(tickets))
|
|
512
|
+
for _, ticket := range tickets {
|
|
513
|
+
slog.Debug("poll ticket",
|
|
514
|
+
"repo", rp.Name, "ticket", ticket.Key, "id", ticket.ID,
|
|
515
|
+
"title", ticket.Title, "claims", strings.Join(ticket.WorkflowClaims, ","))
|
|
516
|
+
wf, err := router.ResolveWorkflow(rp, ticket)
|
|
517
|
+
switch {
|
|
518
|
+
case errors.Is(err, router.ErrNoMatch):
|
|
519
|
+
slog.Info("route outcome",
|
|
520
|
+
"repo", rp.Name, "ticket", ticket.Key, "outcome", "no-match")
|
|
521
|
+
continue
|
|
522
|
+
case err != nil:
|
|
523
|
+
var amb *router.AmbiguousError
|
|
524
|
+
var inv *router.InvalidClaimError
|
|
525
|
+
outcome := "error"
|
|
526
|
+
switch {
|
|
527
|
+
case errors.As(err, &amb):
|
|
528
|
+
outcome = "ambiguous"
|
|
529
|
+
case errors.As(err, &inv):
|
|
530
|
+
outcome = "invalid-claim"
|
|
531
|
+
}
|
|
532
|
+
slog.Info("route outcome",
|
|
533
|
+
"repo", rp.Name, "ticket", ticket.Key, "outcome", outcome, "error", err)
|
|
534
|
+
continue
|
|
535
|
+
}
|
|
536
|
+
routeOutcome := "claimed"
|
|
537
|
+
for _, c := range ticket.WorkflowClaims {
|
|
538
|
+
if c == "wf:"+wf.Name {
|
|
539
|
+
routeOutcome = "already-claimed"
|
|
540
|
+
break
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
slog.Info("route outcome",
|
|
544
|
+
"repo", rp.Name, "ticket", ticket.Key, "workflow", wf.Name, "outcome", routeOutcome)
|
|
545
|
+
if err := runManager.EnsureRun(ctx, rp, wf, ticket); err != nil {
|
|
546
|
+
// EnsureRun already logged the outcome; nothing to add here.
|
|
547
|
+
_ = err
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// preflight is the 5.2 fail-fast startup validation. It runs after the
|
|
554
|
+
// engine is open and BEFORE workers/pollers start. Permanent errors abort
|
|
555
|
+
// startup; runtime errors of existing runs retry forever (different rule).
|
|
556
|
+
//
|
|
557
|
+
// Probes are no-side-effect reads on each adapter boundary:
|
|
558
|
+
// - runner: ValidateRepo per registered repo (Orca connectivity + repo
|
|
559
|
+
// presence at the configured path).
|
|
560
|
+
// - task system: Poll per repo (credentials/permissions/connectivity;
|
|
561
|
+
// returns active parents only, mutates nothing).
|
|
562
|
+
// - harness: ValidateAgent per (repo, workflow) agent node (configured
|
|
563
|
+
// agent must exist for that repo).
|
|
564
|
+
//
|
|
565
|
+
// Repos with no workflows still validate runner + task connectivity; agent
|
|
566
|
+
// validation runs once per distinct (repoPath, agent) pair to avoid
|
|
567
|
+
// redundant CLI calls when several workflows share an agent on one repo.
|
|
568
|
+
func preflight(ctx context.Context, repoReg *repo.Registry, rnr runner.Runner, hrn harness.Harness, workflows []*workflow.Workflow) error {
|
|
569
|
+
for _, rp := range repoReg.List() {
|
|
570
|
+
if err := rnr.ValidateRepo(ctx, rp.Name, rp.Path); err != nil {
|
|
571
|
+
return fmt.Errorf("repo %q runner: %w", rp.Name, err)
|
|
572
|
+
}
|
|
573
|
+
if _, err := rp.TaskSystem.Poll(ctx); err != nil {
|
|
574
|
+
return fmt.Errorf("repo %q task system: %w", rp.Name, err)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
type agentProbe struct {
|
|
578
|
+
repoPath string
|
|
579
|
+
agent string
|
|
580
|
+
}
|
|
581
|
+
seen := map[agentProbe]bool{}
|
|
582
|
+
for _, wf := range workflows {
|
|
583
|
+
for nodeName, n := range wf.Nodes {
|
|
584
|
+
// Validate EVERY configured agent (agent and HITL work nodes
|
|
585
|
+
// both declare one); skip start/end which never carry Agent.
|
|
586
|
+
if (n.Type != workflow.NodeAgent && n.Type != workflow.NodeHITL) || n.Agent == "" {
|
|
587
|
+
continue
|
|
588
|
+
}
|
|
589
|
+
for _, repoName := range wf.Repos {
|
|
590
|
+
rp, ok := repoReg.Get(repoName)
|
|
591
|
+
if !ok {
|
|
592
|
+
return fmt.Errorf("workflow %q references unregistered repo %q", wf.Name, repoName)
|
|
593
|
+
}
|
|
594
|
+
key := agentProbe{repoPath: rp.Path, agent: n.Agent}
|
|
595
|
+
if seen[key] {
|
|
596
|
+
continue
|
|
597
|
+
}
|
|
598
|
+
seen[key] = true
|
|
599
|
+
if err := hrn.ValidateAgent(ctx, rp.Path, n.Agent); err != nil {
|
|
600
|
+
return fmt.Errorf("workflow %q node %q repo %q: %w", wf.Name, nodeName, repoName, err)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return nil
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// The recover composition lives in internal/recover (recoverpkg.FromTaskSystem)
|
|
609
|
+
// so it is importable by both serve and the recover test — see task 6.3.
|
package/go.mod
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
module github.com/rajpopat27/relay-flow
|
|
2
2
|
|
|
3
|
-
go 1.
|
|
3
|
+
go 1.24.6
|
|
4
4
|
|
|
5
|
-
require
|
|
5
|
+
require (
|
|
6
|
+
github.com/cschleiden/go-workflows v1.4.2
|
|
7
|
+
github.com/google/renameio/v2 v2.0.1
|
|
8
|
+
github.com/google/uuid v1.6.0
|
|
9
|
+
gopkg.in/yaml.v3 v3.0.1
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
require (
|
|
13
|
+
github.com/atotto/clipboard v0.1.4 // indirect
|
|
14
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
15
|
+
github.com/benbjohnson/clock v1.3.0 // indirect
|
|
16
|
+
github.com/catppuccin/go v0.3.0 // indirect
|
|
17
|
+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
18
|
+
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
|
19
|
+
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
|
20
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
21
|
+
github.com/charmbracelet/huh v1.0.0 // indirect
|
|
22
|
+
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
23
|
+
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
|
24
|
+
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
|
25
|
+
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
|
26
|
+
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
27
|
+
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
28
|
+
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
29
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
30
|
+
github.com/go-errors/errors v1.4.2 // indirect
|
|
31
|
+
github.com/go-logr/logr v1.4.2 // indirect
|
|
32
|
+
github.com/go-logr/stdr v1.2.2 // indirect
|
|
33
|
+
github.com/golang-migrate/migrate/v4 v4.16.2 // indirect
|
|
34
|
+
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
35
|
+
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
36
|
+
github.com/jellydator/ttlcache/v3 v3.0.0 // indirect
|
|
37
|
+
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
|
38
|
+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
39
|
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
40
|
+
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
41
|
+
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
42
|
+
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
|
43
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
44
|
+
github.com/muesli/cancelreader v0.2.2 // indirect
|
|
45
|
+
github.com/muesli/termenv v0.16.0 // indirect
|
|
46
|
+
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
47
|
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
48
|
+
github.com/rivo/uniseg v0.4.7 // indirect
|
|
49
|
+
github.com/stretchr/objx v0.5.2 // indirect
|
|
50
|
+
github.com/stretchr/testify v1.10.0 // indirect
|
|
51
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
52
|
+
go.opentelemetry.io/otel v1.31.0 // indirect
|
|
53
|
+
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
|
54
|
+
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
|
|
55
|
+
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
|
56
|
+
go.uber.org/atomic v1.11.0 // indirect
|
|
57
|
+
golang.org/x/mod v0.28.0 // indirect
|
|
58
|
+
golang.org/x/sync v0.17.0 // indirect
|
|
59
|
+
golang.org/x/sys v0.36.0 // indirect
|
|
60
|
+
golang.org/x/text v0.29.0 // indirect
|
|
61
|
+
golang.org/x/tools v0.37.0 // indirect
|
|
62
|
+
lukechampine.com/uint128 v1.2.0 // indirect
|
|
63
|
+
modernc.org/cc/v3 v3.40.0 // indirect
|
|
64
|
+
modernc.org/ccgo/v3 v3.16.13 // indirect
|
|
65
|
+
modernc.org/libc v1.29.0 // indirect
|
|
66
|
+
modernc.org/mathutil v1.6.0 // indirect
|
|
67
|
+
modernc.org/memory v1.7.2 // indirect
|
|
68
|
+
modernc.org/opt v0.1.3 // indirect
|
|
69
|
+
modernc.org/sqlite v1.27.0 // indirect
|
|
70
|
+
modernc.org/strutil v1.1.3 // indirect
|
|
71
|
+
modernc.org/token v1.0.1 // indirect
|
|
72
|
+
)
|