relay-flow 0.2.7-alpha → 0.2.9-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 +76 -37
- package/cmd/relay-flow/commands_test.go +199 -19
- package/cmd/relay-flow/main.go +183 -144
- package/cmd/relay-flow/onboarding.go +344 -0
- package/cmd/relay-flow/onboarding_test.go +515 -0
- package/cmd/relay-flow/repo_registration.go +454 -0
- package/cmd/relay-flow/serve.go +5 -2
- package/examples/config-reference.yaml +7 -0
- package/go.mod +12 -8
- package/go.sum +24 -17
- package/internal/execution/goworkflows/engine_test.go +3 -3
- package/internal/harness/opencode/opencode_test.go +4 -4
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/harness/pi/prompt_test.go +3 -3
- package/internal/repo/service.go +47 -3
- package/internal/repo/service_test.go +36 -1
- package/internal/runner/herdr/herdr.go +103 -4
- package/internal/runner/herdr/herdr_test.go +72 -2
- package/internal/runner/herdr/herdrcli/contract.go +12 -3
- package/internal/runner/herdr/herdrcli/herdrcli_test.go +17 -2
- package/internal/runner/herdr/herdrcli/operations.go +16 -0
- package/internal/runner/herdr/herdrcli/testdata/strict-herdr.sh +8 -0
- package/internal/runner/herdr/herdrcli/testdata/workspace-create.json +1 -0
- package/internal/runner/orca/orca.go +68 -9
- package/internal/runner/orca/orca_test.go +134 -2
- package/internal/runner/orca/orcacli/orcacli.go +21 -1
- package/internal/runner/orca/orcacli/orcacli_test.go +22 -0
- package/internal/runner/orca/orcacli/testdata/strict-orca.sh +5 -0
- package/internal/runner/runner.go +9 -0
- package/internal/server/api_test.go +65 -0
- package/internal/server/client.go +40 -2
- package/internal/server/fixture_test.go +27 -16
- package/internal/server/server.go +80 -4
- package/internal/task/beads/beads.go +7 -2
- package/internal/task/beads/beads_test.go +13 -0
- package/internal/task/factory.go +9 -7
- package/internal/task/jira/auth_test.go +9 -1
- package/internal/task/jira/filters_test.go +2 -2
- package/internal/task/jira/helpers_test.go +13 -0
- package/internal/task/jira/jira.go +233 -44
- package/internal/task/jira/lifecycle_inheritance_test.go +5 -5
- package/internal/task/jira/rest/client.go +73 -31
- package/internal/task/jira/rest/client_test.go +25 -0
- package/internal/task/jira/status_defaults_test.go +134 -0
- package/internal/task/jira/templates_test.go +2 -2
- package/internal/task/jira/testdata/jira_search_issues.json +4 -4
- package/internal/task/jira/transition_defaults_test.go +8 -12
- package/internal/task/jira/validation_test.go +3 -1
- package/internal/task/registration.go +59 -0
- package/internal/workflow/workflow.go +4 -1
- package/internal/workflow/workflow_test.go +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/charmbracelet/huh"
|
|
10
|
+
"github.com/rajpopat27/relay-flow/internal/config"
|
|
11
|
+
"github.com/rajpopat27/relay-flow/internal/repo"
|
|
12
|
+
"github.com/rajpopat27/relay-flow/internal/runner"
|
|
13
|
+
"github.com/rajpopat27/relay-flow/internal/server"
|
|
14
|
+
"github.com/rajpopat27/relay-flow/internal/task"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const addRepositorySelection = -1
|
|
18
|
+
|
|
19
|
+
func repoSelectionOptions(candidates []runner.RepoCandidate) []huh.Option[int] {
|
|
20
|
+
options := make([]huh.Option[int], 0, len(candidates)+1)
|
|
21
|
+
for i, candidate := range candidates {
|
|
22
|
+
label := strings.TrimSpace(candidate.Name)
|
|
23
|
+
if label == "" {
|
|
24
|
+
label = candidate.Path
|
|
25
|
+
}
|
|
26
|
+
options = append(options, huh.NewOption(label+" ("+candidate.Path+")", i))
|
|
27
|
+
}
|
|
28
|
+
options = append(options, huh.NewOption("[+] Add repository", addRepositorySelection))
|
|
29
|
+
return options
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func canonicalRepoPath(path string) string {
|
|
33
|
+
path = strings.TrimSpace(path)
|
|
34
|
+
if path == "" {
|
|
35
|
+
return ""
|
|
36
|
+
}
|
|
37
|
+
absolute, err := filepath.Abs(path)
|
|
38
|
+
if err != nil {
|
|
39
|
+
return filepath.Clean(path)
|
|
40
|
+
}
|
|
41
|
+
absolute = filepath.Clean(absolute)
|
|
42
|
+
if resolved, err := filepath.EvalSymlinks(absolute); err == nil {
|
|
43
|
+
return filepath.Clean(resolved)
|
|
44
|
+
}
|
|
45
|
+
return absolute
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func selectedRepoPaths(candidates []runner.RepoCandidate, selected []int) []string {
|
|
49
|
+
paths := make([]string, 0, len(selected))
|
|
50
|
+
seen := map[string]bool{}
|
|
51
|
+
for _, index := range selected {
|
|
52
|
+
if index < 0 || index >= len(candidates) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
path := canonicalRepoPath(candidates[index].Path)
|
|
56
|
+
if path == "" || seen[path] {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
seen[path] = true
|
|
60
|
+
paths = append(paths, path)
|
|
61
|
+
}
|
|
62
|
+
return paths
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func selectedRepoIndices(candidates []runner.RepoCandidate, paths []string) []int {
|
|
66
|
+
wanted := map[string]bool{}
|
|
67
|
+
for _, path := range paths {
|
|
68
|
+
if path != "" {
|
|
69
|
+
wanted[path] = true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
selected := make([]int, 0, len(wanted))
|
|
73
|
+
for i, candidate := range candidates {
|
|
74
|
+
if wanted[canonicalRepoPath(candidate.Path)] {
|
|
75
|
+
selected = append(selected, i)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return selected
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func candidateAtPath(candidates []runner.RepoCandidate, path string) bool {
|
|
82
|
+
path = canonicalRepoPath(path)
|
|
83
|
+
for _, candidate := range candidates {
|
|
84
|
+
if canonicalRepoPath(candidate.Path) == path {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func mergeRepoCandidateNames(candidates []runner.RepoCandidate, names map[string]string) []runner.RepoCandidate {
|
|
92
|
+
out := make([]runner.RepoCandidate, len(candidates))
|
|
93
|
+
copy(out, candidates)
|
|
94
|
+
for i := range out {
|
|
95
|
+
if name := names[canonicalRepoPath(out[i].Path)]; name != "" {
|
|
96
|
+
out[i].Name = name
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func validateAddedRepo(candidate runner.RepoCandidate, candidates []runner.RepoCandidate, registered []repo.Info) error {
|
|
103
|
+
candidate.Name = strings.TrimSpace(candidate.Name)
|
|
104
|
+
candidate.Path = canonicalRepoPath(candidate.Path)
|
|
105
|
+
if candidate.Path == "" {
|
|
106
|
+
return fmt.Errorf("repository path is required")
|
|
107
|
+
}
|
|
108
|
+
if candidate.Name == "" {
|
|
109
|
+
return fmt.Errorf("repository name is required")
|
|
110
|
+
}
|
|
111
|
+
for _, existing := range candidates {
|
|
112
|
+
if canonicalRepoPath(existing.Path) == candidate.Path {
|
|
113
|
+
return fmt.Errorf("repository path %q is already discovered as %q", candidate.Path, existing.Name)
|
|
114
|
+
}
|
|
115
|
+
if strings.TrimSpace(existing.Name) == candidate.Name {
|
|
116
|
+
return fmt.Errorf("repository name %q is already discovered", candidate.Name)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for _, existing := range registered {
|
|
120
|
+
if canonicalRepoPath(existing.Path) == candidate.Path {
|
|
121
|
+
return fmt.Errorf("repository path %q is already registered as %q", candidate.Path, existing.Name)
|
|
122
|
+
}
|
|
123
|
+
if strings.TrimSpace(existing.Name) == candidate.Name {
|
|
124
|
+
return fmt.Errorf("repository name %q is already registered", candidate.Name)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func promptAddedRepo() (runner.RepoCandidate, error) {
|
|
131
|
+
var path, name string
|
|
132
|
+
pathInput := huh.NewInput().Title("Repository path").Value(&path).Validate(func(value string) error {
|
|
133
|
+
if strings.TrimSpace(value) == "" {
|
|
134
|
+
return fmt.Errorf("repository path is required")
|
|
135
|
+
}
|
|
136
|
+
return nil
|
|
137
|
+
})
|
|
138
|
+
nameInput := huh.NewInput().Title("Registered name").Value(&name).Validate(func(value string) error {
|
|
139
|
+
if strings.TrimSpace(value) == "" {
|
|
140
|
+
return fmt.Errorf("repository name is required")
|
|
141
|
+
}
|
|
142
|
+
return nil
|
|
143
|
+
})
|
|
144
|
+
if err := huh.NewForm(huh.NewGroup(pathInput, nameInput)).Run(); err != nil {
|
|
145
|
+
return runner.RepoCandidate{}, err
|
|
146
|
+
}
|
|
147
|
+
return runner.RepoCandidate{Name: strings.TrimSpace(name), Path: canonicalRepoPath(path)}, nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type repoSelectionPrompt func([]runner.RepoCandidate, []int) ([]int, error)
|
|
151
|
+
type repoAddPrompt func() (runner.RepoCandidate, error)
|
|
152
|
+
|
|
153
|
+
func selectReposInteractive(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, registered []repo.Info) ([]runner.RepoCandidate, []int, error) {
|
|
154
|
+
return selectReposInteractiveWithPrompts(ctx, c, candidates, registered, promptRepoSelection, promptAddedRepo)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func promptRepoSelection(candidates []runner.RepoCandidate, selected []int) ([]int, error) {
|
|
158
|
+
pick := huh.NewForm(huh.NewGroup(repoMultiSelect(repoSelectionOptions(candidates), &selected)))
|
|
159
|
+
if err := pick.Run(); err != nil {
|
|
160
|
+
return nil, err
|
|
161
|
+
}
|
|
162
|
+
return selected, nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func selectReposInteractiveWithPrompts(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, registered []repo.Info, selectPrompt repoSelectionPrompt, addPrompt repoAddPrompt) ([]runner.RepoCandidate, []int, error) {
|
|
166
|
+
selectedPaths := []string{}
|
|
167
|
+
nameOverrides := map[string]string{}
|
|
168
|
+
addedCandidates := []runner.RepoCandidate{}
|
|
169
|
+
for {
|
|
170
|
+
candidates = mergeRepoCandidateNames(candidates, nameOverrides)
|
|
171
|
+
selected, err := selectPrompt(candidates, selectedRepoIndices(candidates, selectedPaths))
|
|
172
|
+
if err != nil {
|
|
173
|
+
return nil, nil, err
|
|
174
|
+
}
|
|
175
|
+
selectedPaths = selectedRepoPaths(candidates, selected)
|
|
176
|
+
add := false
|
|
177
|
+
for _, value := range selected {
|
|
178
|
+
if value == addRepositorySelection {
|
|
179
|
+
add = true
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if !add {
|
|
184
|
+
if len(selectedPaths) == 0 {
|
|
185
|
+
return nil, nil, fmt.Errorf("select at least one repository")
|
|
186
|
+
}
|
|
187
|
+
return candidates, selectedRepoIndices(candidates, selectedPaths), nil
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
added, err := addPrompt()
|
|
191
|
+
if err != nil {
|
|
192
|
+
return nil, nil, err
|
|
193
|
+
}
|
|
194
|
+
knownCandidates := append(append([]runner.RepoCandidate(nil), candidates...), addedCandidates...)
|
|
195
|
+
if err := validateAddedRepo(added, knownCandidates, registered); err != nil {
|
|
196
|
+
return nil, nil, err
|
|
197
|
+
}
|
|
198
|
+
if err := c.EnsureRepo(ctx, added); err != nil {
|
|
199
|
+
return nil, nil, fmt.Errorf("ensure repository %q: %w", added.Name, err)
|
|
200
|
+
}
|
|
201
|
+
path := canonicalRepoPath(added.Path)
|
|
202
|
+
nameOverrides[path] = added.Name
|
|
203
|
+
addedCandidates = append(addedCandidates, added)
|
|
204
|
+
selectedPaths = append(selectedPaths, path)
|
|
205
|
+
|
|
206
|
+
refreshed, err := c.DiscoverRepos(ctx)
|
|
207
|
+
if err != nil {
|
|
208
|
+
return nil, nil, err
|
|
209
|
+
}
|
|
210
|
+
refreshed = mergeRepoCandidateNames(refreshed, nameOverrides)
|
|
211
|
+
// Preserve selected entries across a runner refresh even if the
|
|
212
|
+
// external runner's listing is briefly eventually consistent.
|
|
213
|
+
for _, old := range candidates {
|
|
214
|
+
oldPath := canonicalRepoPath(old.Path)
|
|
215
|
+
if containsPath(selectedPaths, oldPath) && !candidateAtPath(refreshed, oldPath) {
|
|
216
|
+
refreshed = append(refreshed, old)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if !candidateAtPath(refreshed, path) {
|
|
220
|
+
refreshed = append(refreshed, added)
|
|
221
|
+
}
|
|
222
|
+
candidates = refreshed
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func containsPath(paths []string, want string) bool {
|
|
227
|
+
for _, path := range paths {
|
|
228
|
+
if path == want {
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// loadRepoRegistration asks the selected task plugin for the fields needed by
|
|
236
|
+
// registration. A second pass lets a plugin expose dependent choices after a
|
|
237
|
+
// value such as a project has been supplied.
|
|
238
|
+
func loadRepoRegistration(ctx context.Context, c *server.Client, supplied kvFlags) (task.Registration, error) {
|
|
239
|
+
initial, err := c.RepoRegistrationFields(ctx, nil)
|
|
240
|
+
if err != nil {
|
|
241
|
+
return task.Registration{}, err
|
|
242
|
+
}
|
|
243
|
+
if len(supplied) == 0 {
|
|
244
|
+
return initial, nil
|
|
245
|
+
}
|
|
246
|
+
dependent, err := c.RepoRegistrationFields(ctx, flatRegistrationValues(supplied))
|
|
247
|
+
if err != nil {
|
|
248
|
+
return task.Registration{}, err
|
|
249
|
+
}
|
|
250
|
+
return mergeRegistration(initial, dependent), nil
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func flatRegistrationValues(values kvFlags) config.RawValues {
|
|
254
|
+
out := make(config.RawValues, len(values))
|
|
255
|
+
for key, value := range values {
|
|
256
|
+
out[key] = value
|
|
257
|
+
}
|
|
258
|
+
return out
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func mergeRegistration(first, second task.Registration) task.Registration {
|
|
262
|
+
fields := make([]task.RegistrationField, 0, len(first.Fields)+len(second.Fields))
|
|
263
|
+
positions := map[string]int{}
|
|
264
|
+
for _, field := range append(append([]task.RegistrationField(nil), first.Fields...), second.Fields...) {
|
|
265
|
+
if index, ok := positions[field.Key]; ok {
|
|
266
|
+
fields[index] = field
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
positions[field.Key] = len(fields)
|
|
270
|
+
fields = append(fields, field)
|
|
271
|
+
}
|
|
272
|
+
return task.Registration{Fields: fields}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// promptRepoRegistration renders only metadata returned by the task plugin.
|
|
276
|
+
// The core CLI does not know which values are Jira statuses or how they are
|
|
277
|
+
// validated.
|
|
278
|
+
func promptRepoRegistration(reg task.Registration, values kvFlags) error {
|
|
279
|
+
for _, field := range reg.Fields {
|
|
280
|
+
if field.Derived || values[field.Key] != "" {
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
title := field.Title
|
|
284
|
+
if title == "" {
|
|
285
|
+
title = field.Key
|
|
286
|
+
}
|
|
287
|
+
if len(field.Options) > 0 {
|
|
288
|
+
selected := field.Default
|
|
289
|
+
if err := huh.NewForm(huh.NewGroup(
|
|
290
|
+
huh.NewSelect[string]().Title(title).Options(registrationSelectOptions(field)...).Value(&selected),
|
|
291
|
+
)).Run(); err != nil {
|
|
292
|
+
return err
|
|
293
|
+
}
|
|
294
|
+
if strings.TrimSpace(selected) == "" {
|
|
295
|
+
return fmt.Errorf("task field %q requires a value", field.Key)
|
|
296
|
+
}
|
|
297
|
+
values[field.Key] = selected
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
value := field.Default
|
|
301
|
+
input := huh.NewInput().Title(title).Value(&value).Validate(func(value string) error {
|
|
302
|
+
if strings.TrimSpace(value) == "" {
|
|
303
|
+
return fmt.Errorf("%s is required", title)
|
|
304
|
+
}
|
|
305
|
+
return nil
|
|
306
|
+
})
|
|
307
|
+
if err := huh.NewForm(huh.NewGroup(input)).Run(); err != nil {
|
|
308
|
+
return err
|
|
309
|
+
}
|
|
310
|
+
values[field.Key] = value
|
|
311
|
+
}
|
|
312
|
+
return nil
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
func registrationSelectOptions(field task.RegistrationField) []huh.Option[string] {
|
|
316
|
+
options := make([]huh.Option[string], 0, len(field.Options)+1)
|
|
317
|
+
if field.Default == "" {
|
|
318
|
+
// Huh selects the first option when Enter is pressed. Keep an explicit
|
|
319
|
+
// empty choice first so a missing conventional default cannot silently
|
|
320
|
+
// become the first real Jira status.
|
|
321
|
+
options = append(options, huh.NewOption("Select a value...", ""))
|
|
322
|
+
}
|
|
323
|
+
for _, option := range field.Options {
|
|
324
|
+
options = append(options, huh.NewOption(option, option))
|
|
325
|
+
}
|
|
326
|
+
return options
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
func registrationTaskConfig(reg task.Registration, supplied kvFlags, repoName string) (config.RawValues, error) {
|
|
330
|
+
known := make(map[string]task.RegistrationField, len(reg.Fields))
|
|
331
|
+
for _, field := range reg.Fields {
|
|
332
|
+
if strings.TrimSpace(field.Key) == "" {
|
|
333
|
+
return nil, errorsForRegistration("task plugin returned an empty registration key")
|
|
334
|
+
}
|
|
335
|
+
if _, exists := known[field.Key]; exists {
|
|
336
|
+
return nil, errorsForRegistration(fmt.Sprintf("duplicate task registration key %q", field.Key))
|
|
337
|
+
}
|
|
338
|
+
known[field.Key] = field
|
|
339
|
+
}
|
|
340
|
+
out := config.RawValues{}
|
|
341
|
+
for key, value := range supplied {
|
|
342
|
+
field, ok := known[key]
|
|
343
|
+
if !ok {
|
|
344
|
+
return nil, errorsForRegistration(fmt.Sprintf("unknown task key %q", key))
|
|
345
|
+
}
|
|
346
|
+
if field.Derived {
|
|
347
|
+
return nil, errorsForRegistration(fmt.Sprintf("task key %q is derived and cannot be overridden", key))
|
|
348
|
+
}
|
|
349
|
+
if strings.TrimSpace(value) == "" {
|
|
350
|
+
return nil, errorsForRegistration(fmt.Sprintf("task key %q requires a non-empty value", key))
|
|
351
|
+
}
|
|
352
|
+
if len(field.Options) > 0 && !containsRegistrationOption(field.Options, value) {
|
|
353
|
+
return nil, errorsForRegistration(fmt.Sprintf("task key %q value %q is not one of the available choices", key, value))
|
|
354
|
+
}
|
|
355
|
+
if err := setRegistrationValue(out, key, value); err != nil {
|
|
356
|
+
return nil, err
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for _, field := range reg.Fields {
|
|
360
|
+
if field.Derived {
|
|
361
|
+
if err := setRegistrationValue(out, field.Key, repoName); err != nil {
|
|
362
|
+
return nil, err
|
|
363
|
+
}
|
|
364
|
+
continue
|
|
365
|
+
}
|
|
366
|
+
if registrationValue(out, field.Key) == "" {
|
|
367
|
+
return nil, errorsForRegistration(fmt.Sprintf("missing required task key %q", field.Key))
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return out, nil
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func errorsForRegistration(message string) error { return fmt.Errorf("%s", message) }
|
|
374
|
+
|
|
375
|
+
func containsRegistrationOption(options []string, value string) bool {
|
|
376
|
+
for _, option := range options {
|
|
377
|
+
if option == value {
|
|
378
|
+
return true
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return false
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
func registrationValue(values config.RawValues, key string) string {
|
|
385
|
+
parts := strings.Split(key, ".")
|
|
386
|
+
var current any = map[string]any(values)
|
|
387
|
+
for _, part := range parts {
|
|
388
|
+
var next map[string]any
|
|
389
|
+
switch value := current.(type) {
|
|
390
|
+
case map[string]any:
|
|
391
|
+
next = value
|
|
392
|
+
case config.RawValues:
|
|
393
|
+
next = map[string]any(value)
|
|
394
|
+
default:
|
|
395
|
+
return ""
|
|
396
|
+
}
|
|
397
|
+
value, ok := next[part]
|
|
398
|
+
if !ok {
|
|
399
|
+
return ""
|
|
400
|
+
}
|
|
401
|
+
current = value
|
|
402
|
+
}
|
|
403
|
+
value, _ := current.(string)
|
|
404
|
+
return strings.TrimSpace(value)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
func setRegistrationValue(values config.RawValues, key, value string) error {
|
|
408
|
+
parts := strings.Split(key, ".")
|
|
409
|
+
if len(parts) == 0 || parts[0] == "" {
|
|
410
|
+
return errorsForRegistration("task plugin returned an invalid registration key")
|
|
411
|
+
}
|
|
412
|
+
current := values
|
|
413
|
+
for _, part := range parts[:len(parts)-1] {
|
|
414
|
+
if part == "" {
|
|
415
|
+
return errorsForRegistration(fmt.Sprintf("task plugin returned an invalid registration key %q", key))
|
|
416
|
+
}
|
|
417
|
+
existing, ok := current[part]
|
|
418
|
+
if !ok {
|
|
419
|
+
nested := map[string]any{}
|
|
420
|
+
current[part] = nested
|
|
421
|
+
current = nested
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
var nested map[string]any
|
|
425
|
+
switch typed := existing.(type) {
|
|
426
|
+
case map[string]any:
|
|
427
|
+
nested = typed
|
|
428
|
+
case config.RawValues:
|
|
429
|
+
nested = map[string]any(typed)
|
|
430
|
+
current[part] = nested
|
|
431
|
+
default:
|
|
432
|
+
return errorsForRegistration(fmt.Sprintf("task registration key %q conflicts with another value", key))
|
|
433
|
+
}
|
|
434
|
+
current = nested
|
|
435
|
+
}
|
|
436
|
+
current[parts[len(parts)-1]] = value
|
|
437
|
+
return nil
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
func registerSelectedReposDynamic(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, selected []int, reg task.Registration, values kvFlags) error {
|
|
441
|
+
for _, index := range selected {
|
|
442
|
+
candidate := candidates[index]
|
|
443
|
+
taskCfg, err := registrationTaskConfig(reg, values, candidate.Name)
|
|
444
|
+
if err != nil {
|
|
445
|
+
return fmt.Errorf("%s: %w", candidate.Name, err)
|
|
446
|
+
}
|
|
447
|
+
info, err := c.RegisterRepo(ctx, repo.RegisterInput{Name: candidate.Name, Path: candidate.Path, TaskConfig: taskCfg})
|
|
448
|
+
if err != nil {
|
|
449
|
+
return fmt.Errorf("%s: %w", candidate.Name, err)
|
|
450
|
+
}
|
|
451
|
+
fmt.Println(info.Name)
|
|
452
|
+
}
|
|
453
|
+
return nil
|
|
454
|
+
}
|
package/cmd/relay-flow/serve.go
CHANGED
|
@@ -565,8 +565,11 @@ func (d *serveDeps) RegisterNodeSession(ctx context.Context, registration runsvc
|
|
|
565
565
|
func (d *serveDeps) DiscoverRepos(ctx context.Context) ([]runner.RepoCandidate, error) {
|
|
566
566
|
return d.repos.Discover(ctx)
|
|
567
567
|
}
|
|
568
|
-
func (d *serveDeps)
|
|
569
|
-
return d.repos.
|
|
568
|
+
func (d *serveDeps) EnsureRepo(ctx context.Context, candidate runner.RepoCandidate) error {
|
|
569
|
+
return d.repos.EnsureRepo(ctx, candidate.Name, candidate.Path)
|
|
570
|
+
}
|
|
571
|
+
func (d *serveDeps) TaskRegistrationFields(ctx context.Context, values config.RawValues) ([]task.RegistrationField, error) {
|
|
572
|
+
return d.repos.RegistrationFields(ctx, values)
|
|
570
573
|
}
|
|
571
574
|
func (d *serveDeps) RegisterRepo(ctx context.Context, input repo.RegisterInput) (repo.Info, error) {
|
|
572
575
|
info, err := d.repos.Register(ctx, input)
|
|
@@ -101,6 +101,13 @@ repos:
|
|
|
101
101
|
# relay-flow repo name during repo registration.
|
|
102
102
|
project: PAY
|
|
103
103
|
component: payments
|
|
104
|
+
# These must be valid statuses in the PAY project. Interactive
|
|
105
|
+
# registration discovers them; scripted registration passes the same
|
|
106
|
+
# three keys with --set.
|
|
107
|
+
statusDefaults:
|
|
108
|
+
start: To Do
|
|
109
|
+
work: In Progress
|
|
110
|
+
end: Done
|
|
104
111
|
|
|
105
112
|
# Optional repo-level overrides inherit into workflows and nodes.
|
|
106
113
|
# assignee: relay-bot@example.com
|
package/go.mod
CHANGED
|
@@ -3,6 +3,7 @@ module github.com/rajpopat27/relay-flow
|
|
|
3
3
|
go 1.26.0
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b
|
|
6
7
|
github.com/cschleiden/go-workflows v1.4.2
|
|
7
8
|
github.com/google/renameio/v2 v2.0.1
|
|
8
9
|
github.com/google/uuid v1.6.0
|
|
@@ -16,6 +17,9 @@ require (
|
|
|
16
17
|
|
|
17
18
|
require (
|
|
18
19
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
20
|
+
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
|
21
|
+
github.com/clipperhouse/stringish v0.1.1 // indirect
|
|
22
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
|
19
23
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
|
20
24
|
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
|
21
25
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
|
@@ -27,15 +31,15 @@ require (
|
|
|
27
31
|
github.com/benbjohnson/clock v1.3.0 // indirect
|
|
28
32
|
github.com/catppuccin/go v0.3.0 // indirect
|
|
29
33
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
30
|
-
github.com/charmbracelet/bubbles
|
|
31
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
32
|
-
github.com/charmbracelet/colorprofile v0.
|
|
34
|
+
github.com/charmbracelet/bubbles v1.0.0 // indirect
|
|
35
|
+
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
|
36
|
+
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
|
33
37
|
github.com/charmbracelet/huh v1.0.0
|
|
34
38
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
35
|
-
github.com/charmbracelet/x/ansi v0.
|
|
36
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
39
|
+
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
|
40
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
|
37
41
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
|
38
|
-
github.com/charmbracelet/x/term v0.2.
|
|
42
|
+
github.com/charmbracelet/x/term v0.2.2 // indirect
|
|
39
43
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
40
44
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
41
45
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
@@ -52,10 +56,10 @@ require (
|
|
|
52
56
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
53
57
|
github.com/jellydator/ttlcache/v3 v3.0.0 // indirect
|
|
54
58
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
|
55
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
59
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
|
56
60
|
github.com/mattn/go-isatty v0.0.20
|
|
57
61
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
58
|
-
github.com/mattn/go-runewidth v0.0.
|
|
62
|
+
github.com/mattn/go-runewidth v0.0.19 // indirect
|
|
59
63
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
|
60
64
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
61
65
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
package/go.sum
CHANGED
|
@@ -14,20 +14,22 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
|
|
|
14
14
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
|
15
15
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
16
16
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
17
|
-
github.com/charmbracelet/bubbles
|
|
18
|
-
github.com/charmbracelet/bubbles
|
|
19
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
20
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
21
|
-
github.com/charmbracelet/colorprofile v0.
|
|
22
|
-
github.com/charmbracelet/colorprofile v0.
|
|
17
|
+
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
|
18
|
+
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
|
19
|
+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
20
|
+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
21
|
+
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
|
22
|
+
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
|
23
23
|
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
|
|
24
24
|
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
|
25
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b h1:deQbW7eR/gYwkXonGX6a1now6H6f8v4kfv0OIKECu0I=
|
|
26
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b/go.mod h1:Y68nuKJuC/Q2lmiq18EkHWkVWi2VGLrwaOfOyPKLkkE=
|
|
25
27
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
26
28
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
27
|
-
github.com/charmbracelet/x/ansi v0.
|
|
28
|
-
github.com/charmbracelet/x/ansi v0.
|
|
29
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
30
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
29
|
+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
|
30
|
+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
|
31
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
|
32
|
+
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
|
31
33
|
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
|
32
34
|
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
|
33
35
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
|
@@ -36,12 +38,18 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payR
|
|
|
36
38
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
|
37
39
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
|
38
40
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
|
39
|
-
github.com/charmbracelet/x/term v0.2.
|
|
40
|
-
github.com/charmbracelet/x/term v0.2.
|
|
41
|
+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
|
42
|
+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
|
41
43
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
|
42
44
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
|
43
45
|
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
|
44
46
|
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
|
47
|
+
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
|
48
|
+
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
|
49
|
+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
|
50
|
+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
|
51
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
|
52
|
+
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
|
45
53
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|
46
54
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
|
47
55
|
github.com/cschleiden/go-workflows v1.4.2 h1:s7wgx3iKvFmwJVzB7wSuUFqbplcpCUGrn8u1J4/T+2c=
|
|
@@ -98,14 +106,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
|
98
106
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
99
107
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|
100
108
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
|
101
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
102
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
109
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
|
110
|
+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
103
111
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
104
112
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
105
113
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
106
114
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
107
|
-
github.com/mattn/go-runewidth v0.0.
|
|
108
|
-
github.com/mattn/go-runewidth v0.0.
|
|
115
|
+
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
|
116
|
+
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
|
109
117
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
|
110
118
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
111
119
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
|
@@ -124,7 +132,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|
|
124
132
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
125
133
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
126
134
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
127
|
-
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
128
135
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
129
136
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
130
137
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
|
@@ -187,7 +187,7 @@ func TestRunBeginsAtStartAndFollowsEntryEdge(t *testing.T) {
|
|
|
187
187
|
|
|
188
188
|
wf := linearWorkflow(false)
|
|
189
189
|
node := wf.Nodes["coding"]
|
|
190
|
-
node.NudgePrompt = "Continue {{ticket}} at {{node}}. Valid next steps: {{nextSteps}}."
|
|
190
|
+
node.NudgePrompt = "Continue {{ticket}} at {{node}} in {{mailbox}}. Valid next steps: {{nextSteps}}."
|
|
191
191
|
wf.Nodes["coding"] = node
|
|
192
192
|
rid, err := startRun(engine, wf)
|
|
193
193
|
if err != nil {
|
|
@@ -221,8 +221,8 @@ func TestRunBeginsAtStartAndFollowsEntryEdge(t *testing.T) {
|
|
|
221
221
|
if call.NudgeTemplate != node.NudgePrompt {
|
|
222
222
|
t.Fatalf("nudge passed to harness = %q, want raw template %q", call.NudgeTemplate, node.NudgePrompt)
|
|
223
223
|
}
|
|
224
|
-
if call.Data.Ticket != "PAY-101" || call.Data.Workflow != wf.Name || call.Data.Repo != "payments" || call.Data.Node != "coding" || call.Data.NextSteps == "" {
|
|
225
|
-
t.Fatalf("nudge prompt data = %+v, want current workflow values", call.Data)
|
|
224
|
+
if call.Data.Ticket != "PAY-101" || call.Data.Workflow != wf.Name || call.Data.Repo != "payments" || call.Data.Node != "coding" || call.Data.Mailbox != "PAY-101-coding" || call.Data.NextSteps == "" {
|
|
225
|
+
t.Fatalf("nudge prompt data = %+v, want current workflow and mailbox values", call.Data)
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// Pre-edge gate: before following the start edge the run ensures the
|
|
@@ -14,7 +14,7 @@ import (
|
|
|
14
14
|
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
const configuredPlugin = "relay-flow-plugin@0.2.
|
|
17
|
+
const configuredPlugin = "relay-flow-plugin@0.2.9-alpha"
|
|
18
18
|
|
|
19
19
|
func TestBuildCommandArgv(t *testing.T) {
|
|
20
20
|
t.Setenv("RELAY_FLOW_HOME", "/var/lib/relay-flow-test")
|
|
@@ -71,12 +71,12 @@ func TestRenderPromptTemplatesExposeAllValues(t *testing.T) {
|
|
|
71
71
|
Node: "review", NodeType: workflow.NodeHITL, Agent: "build", NodeDescription: "Review it.",
|
|
72
72
|
NextSteps: "end (when: approved)", Mailbox: "PAY-234",
|
|
73
73
|
}
|
|
74
|
-
nudge := "nudge {{taskSystem}}|{{ticket}}|{{workflow}}|{{repo}}|{{node}}|{{nextSteps}}"
|
|
74
|
+
nudge := "nudge {{taskSystem}}|{{ticket}}|{{workflow}}|{{repo}}|{{node}}|{{mailbox}}|{{nextSteps}}"
|
|
75
75
|
initial, err := h.RenderPrompt(harness.PromptInitial, data, nudge)
|
|
76
76
|
if err != nil {
|
|
77
77
|
t.Fatal(err)
|
|
78
78
|
}
|
|
79
|
-
wantInitial := "initial linear|PAY-101|basicFlow|payments|review|hitl|build|Review it.|end (when: approved)|PAY-234\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.\n\nnudge linear|PAY-101|basicFlow|payments|review|end (when: approved)"
|
|
79
|
+
wantInitial := "initial linear|PAY-101|basicFlow|payments|review|hitl|build|Review it.|end (when: approved)|PAY-234\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.\n\nnudge linear|PAY-101|basicFlow|payments|review|PAY-234|end (when: approved)"
|
|
80
80
|
if initial != wantInitial {
|
|
81
81
|
t.Fatalf("initial prompt = %q, want %q", initial, wantInitial)
|
|
82
82
|
}
|
|
@@ -84,7 +84,7 @@ func TestRenderPromptTemplatesExposeAllValues(t *testing.T) {
|
|
|
84
84
|
if err != nil {
|
|
85
85
|
t.Fatal(err)
|
|
86
86
|
}
|
|
87
|
-
if want := "feedback PAY-234\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.\n\nnudge linear|PAY-101|basicFlow|payments|review|end (when: approved)"; feedback != want {
|
|
87
|
+
if want := "feedback PAY-234\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.\n\nnudge linear|PAY-101|basicFlow|payments|review|PAY-234|end (when: approved)"; feedback != want {
|
|
88
88
|
t.Fatalf("feedback prompt = %q, want %q", feedback, want)
|
|
89
89
|
}
|
|
90
90
|
}
|