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.
Files changed (52) hide show
  1. package/README.md +76 -37
  2. package/cmd/relay-flow/commands_test.go +199 -19
  3. package/cmd/relay-flow/main.go +183 -144
  4. package/cmd/relay-flow/onboarding.go +344 -0
  5. package/cmd/relay-flow/onboarding_test.go +515 -0
  6. package/cmd/relay-flow/repo_registration.go +454 -0
  7. package/cmd/relay-flow/serve.go +5 -2
  8. package/examples/config-reference.yaml +7 -0
  9. package/go.mod +12 -8
  10. package/go.sum +24 -17
  11. package/internal/execution/goworkflows/engine_test.go +3 -3
  12. package/internal/harness/opencode/opencode_test.go +4 -4
  13. package/internal/harness/opencode/repo_setup.go +1 -1
  14. package/internal/harness/pi/prompt_test.go +3 -3
  15. package/internal/repo/service.go +47 -3
  16. package/internal/repo/service_test.go +36 -1
  17. package/internal/runner/herdr/herdr.go +103 -4
  18. package/internal/runner/herdr/herdr_test.go +72 -2
  19. package/internal/runner/herdr/herdrcli/contract.go +12 -3
  20. package/internal/runner/herdr/herdrcli/herdrcli_test.go +17 -2
  21. package/internal/runner/herdr/herdrcli/operations.go +16 -0
  22. package/internal/runner/herdr/herdrcli/testdata/strict-herdr.sh +8 -0
  23. package/internal/runner/herdr/herdrcli/testdata/workspace-create.json +1 -0
  24. package/internal/runner/orca/orca.go +68 -9
  25. package/internal/runner/orca/orca_test.go +134 -2
  26. package/internal/runner/orca/orcacli/orcacli.go +21 -1
  27. package/internal/runner/orca/orcacli/orcacli_test.go +22 -0
  28. package/internal/runner/orca/orcacli/testdata/strict-orca.sh +5 -0
  29. package/internal/runner/runner.go +9 -0
  30. package/internal/server/api_test.go +65 -0
  31. package/internal/server/client.go +40 -2
  32. package/internal/server/fixture_test.go +27 -16
  33. package/internal/server/server.go +80 -4
  34. package/internal/task/beads/beads.go +7 -2
  35. package/internal/task/beads/beads_test.go +13 -0
  36. package/internal/task/factory.go +9 -7
  37. package/internal/task/jira/auth_test.go +9 -1
  38. package/internal/task/jira/filters_test.go +2 -2
  39. package/internal/task/jira/helpers_test.go +13 -0
  40. package/internal/task/jira/jira.go +233 -44
  41. package/internal/task/jira/lifecycle_inheritance_test.go +5 -5
  42. package/internal/task/jira/rest/client.go +73 -31
  43. package/internal/task/jira/rest/client_test.go +25 -0
  44. package/internal/task/jira/status_defaults_test.go +134 -0
  45. package/internal/task/jira/templates_test.go +2 -2
  46. package/internal/task/jira/testdata/jira_search_issues.json +4 -4
  47. package/internal/task/jira/transition_defaults_test.go +8 -12
  48. package/internal/task/jira/validation_test.go +3 -1
  49. package/internal/task/registration.go +59 -0
  50. package/internal/workflow/workflow.go +4 -1
  51. package/internal/workflow/workflow_test.go +4 -4
  52. 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
+ }
@@ -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) TaskFields(context.Context) ([]string, error) {
569
- return d.repos.RequiredRepoKeys(), nil
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 v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
31
- github.com/charmbracelet/bubbletea v1.3.6 // indirect
32
- github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
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.9.3 // indirect
36
- github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
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.1 // indirect
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.2.0 // indirect
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.16 // indirect
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 v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
18
- github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
19
- github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
20
- github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
21
- github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
22
- github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
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.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
28
- github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
29
- github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
30
- github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
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.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
40
- github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
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.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
102
- github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
108
- github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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.7-alpha"
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
  }