relay-flow 0.2.8-alpha → 0.2.10-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 (55) hide show
  1. package/README.md +76 -35
  2. package/cmd/relay-flow/commands_test.go +117 -8
  3. package/cmd/relay-flow/main.go +391 -83
  4. package/cmd/relay-flow/observability_test.go +204 -0
  5. package/cmd/relay-flow/onboarding.go +344 -0
  6. package/cmd/relay-flow/onboarding_test.go +515 -0
  7. package/cmd/relay-flow/render.go +629 -0
  8. package/cmd/relay-flow/repo_registration.go +219 -0
  9. package/cmd/relay-flow/serve.go +35 -1
  10. package/go.mod +12 -8
  11. package/go.sum +24 -17
  12. package/internal/execution/goworkflows/activities.go +9 -0
  13. package/internal/execution/goworkflows/engine.go +15 -0
  14. package/internal/execution/goworkflows/engine_test.go +3 -3
  15. package/internal/execution/goworkflows/interpreter.go +86 -3
  16. package/internal/execution/goworkflows/projection.go +16 -0
  17. package/internal/execution/projection/detail_test.go +259 -0
  18. package/internal/execution/projection/projection.go +290 -6
  19. package/internal/execution/projection/projection_test.go +3 -2
  20. package/internal/execution/temporal/activities.go +9 -0
  21. package/internal/execution/temporal/engine.go +13 -0
  22. package/internal/execution/temporal/interpreter.go +131 -3
  23. package/internal/execution/temporal/operations.go +174 -0
  24. package/internal/execution/temporal/operations_test.go +36 -0
  25. package/internal/harness/opencode/opencode_test.go +4 -4
  26. package/internal/harness/opencode/repo_setup.go +1 -1
  27. package/internal/harness/pi/prompt_test.go +3 -3
  28. package/internal/repo/service.go +34 -3
  29. package/internal/repo/service_test.go +22 -0
  30. package/internal/run/detail.go +277 -0
  31. package/internal/run/detail_test.go +71 -0
  32. package/internal/run/run.go +4 -0
  33. package/internal/runner/herdr/herdr.go +103 -4
  34. package/internal/runner/herdr/herdr_test.go +72 -2
  35. package/internal/runner/herdr/herdrcli/contract.go +12 -3
  36. package/internal/runner/herdr/herdrcli/herdrcli_test.go +17 -2
  37. package/internal/runner/herdr/herdrcli/operations.go +16 -0
  38. package/internal/runner/herdr/herdrcli/testdata/strict-herdr.sh +8 -0
  39. package/internal/runner/herdr/herdrcli/testdata/workspace-create.json +1 -0
  40. package/internal/runner/orca/orca.go +68 -9
  41. package/internal/runner/orca/orca_test.go +134 -2
  42. package/internal/runner/orca/orcacli/orcacli.go +21 -1
  43. package/internal/runner/orca/orcacli/orcacli_test.go +22 -0
  44. package/internal/runner/orca/orcacli/testdata/strict-orca.sh +5 -0
  45. package/internal/runner/runner.go +9 -0
  46. package/internal/server/api_test.go +19 -0
  47. package/internal/server/client.go +49 -0
  48. package/internal/server/fixture_test.go +7 -0
  49. package/internal/server/observability.go +109 -0
  50. package/internal/server/observability_test.go +60 -0
  51. package/internal/server/server.go +77 -0
  52. package/internal/task/jira/testdata/jira_search_issues.json +4 -4
  53. package/internal/workflow/workflow.go +4 -1
  54. package/internal/workflow/workflow_test.go +4 -4
  55. package/package.json +1 -1
package/README.md CHANGED
@@ -24,13 +24,13 @@ same `relay-flow-plugin` package with host-specific entrypoints.
24
24
  **OpenCode**
25
25
 
26
26
  ```sh
27
- opencode plugin relay-flow-plugin@0.2.8-alpha
27
+ opencode plugin relay-flow-plugin@0.2.10-alpha
28
28
  ```
29
29
 
30
30
  **Pi**
31
31
 
32
32
  ```sh
33
- pi install npm:relay-flow-plugin@0.2.8-alpha
33
+ pi install npm:relay-flow-plugin@0.2.10-alpha
34
34
  ```
35
35
 
36
36
  Pi loads the package's `pi.ts` extension from its manifest. Do not add
@@ -38,39 +38,40 @@ Pi loads the package's `pi.ts` extension from its manifest. Do not add
38
38
 
39
39
  ### 3. Initialize relay-flow
40
40
 
41
- Choose one task system, runner, harness, and durable executor. This example
42
- uses Jira, Orca, OpenCode, and the default embedded executor:
41
+ For a first-time interactive setup, run:
43
42
 
44
43
  ```sh
45
- relay-flow init \
46
- --task-plugin jira \
47
- --runner-plugin orca \
48
- --harness-plugin opencode
44
+ relay-flow init
49
45
  ```
50
46
 
51
- The default executor is `goworkflows` with SQLite. To use Temporal instead,
52
- add `--executor-plugin temporal`, `--temporal-address <host:port>`, and
53
- `--temporal-namespace <name>` to the command.
54
-
55
- ### 4. Authenticate the task system
47
+ Relay-flow selects the task system, runner, harness, and durable executor,
48
+ runs the selected task plugin's authentication flow inline, and then asks
49
+ whether repositories should be registered now. Choosing no completes setup
50
+ without starting a server. Choosing yes starts (or reuses) the background
51
+ server, waits for its Unix socket, and opens the existing repository
52
+ registration form. After registration, the server remains running; stop it
53
+ with `relay-flow stop`.
56
54
 
57
- For Jira, authenticate the selected task plugin:
55
+ The equivalent non-interactive setup keeps the independent commands available
56
+ for scripts and existing installations. This example uses Jira, Orca,
57
+ OpenCode, and the default embedded executor:
58
58
 
59
59
  ```sh
60
+ relay-flow init \
61
+ --task-plugin jira \
62
+ --runner-plugin orca \
63
+ --harness-plugin opencode
60
64
  relay-flow task auth
65
+ relay-flow serve --background
61
66
  ```
62
67
 
63
- For Beads, skip this command and initialize/authenticate the Beads workspace
68
+ For Beads, skip `task auth` and initialize/authenticate the Beads workspace
64
69
  with `bd` and, when needed, Dolt. See [Beads task system](#beads-task-system)
65
- below.
70
+ below. The default executor is `goworkflows` with SQLite. To use Temporal
71
+ instead, add `--executor-plugin temporal`, `--temporal-address <host:port>`,
72
+ and `--temporal-namespace <name>` to the command.
66
73
 
67
- ### 5. Start the server
68
-
69
- ```sh
70
- relay-flow serve --background
71
- ```
72
-
73
- ### 6. Register a repository
74
+ ### 4. Register a repository later
74
75
 
75
76
  The repository must already exist in the selected runner. For Orca:
76
77
 
@@ -81,13 +82,18 @@ relay-flow repo register
81
82
 
82
83
  For Herdr, register the repository path directly; relay-flow creates ticket
83
84
  worktrees lazily. The interactive registration asks for the task-system values
84
- required by the selected task plugin.
85
+ required by the selected task plugin. `repo register` remains the standalone
86
+ escape hatch for adding repositories after initialization and requires a
87
+ running `relay-flow serve --background`.
85
88
 
86
- ### 7. Submit a workflow
89
+ ### 5. Submit a workflow
87
90
 
88
91
  ```sh
89
92
  relay-flow workflow submit --file examples/minimal-jira-task-workflow.yaml
90
93
  relay-flow workflow list
94
+ relay-flow workflow list --json # stable automation output
95
+ relay-flow run get --ticket PAY-101 # Argo-shaped human inspection view
96
+ relay-flow run get --ticket PAY-101 --json # stable automation output
91
97
  ```
92
98
 
93
99
  Replace the example workflow with
@@ -154,7 +160,7 @@ OpenCode plugin configuration uses both entrypoints. The server entrypoint is li
154
160
  ```json
155
161
  {
156
162
  "$schema": "https://opencode.ai/config.json",
157
- "plugin": ["relay-flow-plugin@0.2.8-alpha"]
163
+ "plugin": ["relay-flow-plugin@0.2.10-alpha"]
158
164
  }
159
165
  ```
160
166
 
@@ -163,7 +169,7 @@ The native HITL approval entrypoint is listed in `.opencode/tui.json`:
163
169
  ```json
164
170
  {
165
171
  "$schema": "https://opencode.ai/tui.json",
166
- "plugin": ["relay-flow-plugin@0.2.8-alpha"]
172
+ "plugin": ["relay-flow-plugin@0.2.10-alpha"]
167
173
  }
168
174
  ```
169
175
 
@@ -179,7 +185,7 @@ Pi plugin: install the same published package manually in Pi's global package
179
185
  settings before starting a Pi harness session:
180
186
 
181
187
  ```sh
182
- pi install npm:relay-flow-plugin@0.2.8-alpha
188
+ pi install npm:relay-flow-plugin@0.2.10-alpha
183
189
  ```
184
190
 
185
191
  Relay-flow does not install or configure the package automatically. Pi resolves
@@ -199,10 +205,30 @@ internal and is never part of either plugin payload.
199
205
 
200
206
  ```sh
201
207
  relay-flow init
202
- relay-flow task auth
203
208
  ```
204
209
 
205
- `init` selects the task system, runner, harness, and durable executor (singleton options are automatic), writes machine config, and initializes the selected execution backend. `task auth` delegates authentication to that selected task plug-in. Jira prompts for its site, email, and masked API token, validates `/myself`, and owns the system-wide `credentials.yaml`; for scripts, pass `task auth --site`, `--email`, and `--token`. A normal init rerun refuses existing state. `relay-flow init --force` updates safe stopped instances while preserving durable and repo state.
210
+ On a new interactive home, `init` selects the task system, runner, harness,
211
+ and durable executor, runs the selected task plug-in's authentication flow,
212
+ and asks whether to register repositories. It writes machine config and the
213
+ selected execution backend only after authentication succeeds. A skipped
214
+ repository setup does not start the server; selecting it starts or reuses the
215
+ background server, waits for socket readiness, and invokes the same
216
+ `repo register` form described below. The server remains running after a
217
+ successful guided registration.
218
+
219
+ For scripts and existing installations, keep the independent sequence:
220
+
221
+ ```sh
222
+ relay-flow init --task-plugin jira --runner-plugin orca --harness-plugin opencode
223
+ relay-flow task auth --site https://company.atlassian.net --email you@example.com --token "$JIRA_API_TOKEN"
224
+ relay-flow serve --background
225
+ ```
226
+
227
+ `task auth` delegates to the selected task plug-in. Jira prompts for its site,
228
+ email, and masked API token when flags are omitted, validates `/myself`, and
229
+ owns the system-wide `credentials.yaml`. A normal init rerun refuses existing
230
+ state. `relay-flow init --force` updates safe stopped instances while
231
+ preserving durable and repo state; it does not launch repository onboarding.
206
232
 
207
233
  For Beads, select the plugin explicitly when scripting setup:
208
234
 
@@ -234,9 +260,11 @@ workflows/<name>.yaml 0644 submitted workflow definitions
234
260
 
235
261
  ### Register a repo
236
262
 
237
- `repo register` and `workflow submit` are server-backed commands: start the
238
- server first, otherwise they fail with `dial unix ~/.relay-flow/server.sock:
239
- connect: no such file or directory`.
263
+ The guided first-run flow can start the server and open repository registration
264
+ for you. `repo register` remains a server-backed standalone command for later
265
+ additions: if no server is running, start it with
266
+ `relay-flow serve --background` (or rerun interactive `relay-flow init` for a
267
+ new home).
240
268
 
241
269
  ```sh
242
270
  relay-flow serve --background
@@ -369,8 +397,9 @@ Task, runner, harness, and durable executor plugins are selected machine-wide. A
369
397
 
370
398
  ### Run
371
399
 
372
- The server must already be running for `repo register` and `workflow submit`;
373
- the same process polls repos and drives runs.
400
+ The guided first-run flow starts or reuses the server before `repo register`.
401
+ Outside that flow, `repo register` and `workflow submit` require the server to
402
+ already be running; the same process polls repos and drives runs.
374
403
 
375
404
  ```sh
376
405
  relay-flow serve # normal start; requires an initialized database
@@ -455,6 +484,7 @@ Rules enforced at submit:
455
484
  - The graph must be fully reachable from `start`; unknown fields are rejected; `runnerPlugin`/`harnessPlugin`/`closeOn`/legacy `tasks`/`runner` keys are rejected.
456
485
  - Only `agent` and `hitl` nodes receive mailbox subtasks; `start` and `end` never do.
457
486
  - `cleanupRunnerOnEnd` is the only workflow cleanup knob and takes priority over terminal retention after `end`; the word `terminal` refers to runner terminals only.
487
+ - `nudgePrompt` supports `{{taskSystem}}`, `{{ticket}}`, `{{workflow}}`, `{{repo}}`, `{{node}}`, `{{mailbox}}`, and `{{nextSteps}}`; unsupported variables are rejected at submission.
458
488
 
459
489
  ### Task-config merge
460
490
 
@@ -651,6 +681,17 @@ One Repo Poller per registered repo (not per workflow) fetches active parent tic
651
681
 
652
682
  ---
653
683
 
684
+ ## CLI inspection and automation
685
+
686
+ Human-readable workflow and run views are intended for interactive operator
687
+ use. Scripts should always pass `--json`; the JSON output is the stable
688
+ machine-readable interface. `run get --ticket <key>` shows metadata,
689
+ conditions, timing, progress, resource-duration placeholders, and the actual
690
+ node-visit tree, including retries and revisits. Scoped `--help` is available
691
+ at the root, group, and leaf command levels. Build information is available
692
+ without a server through `relay-flow version`, `relay-flow --version`, and
693
+ `relay-flow -v`.
694
+
654
695
  ## Development
655
696
 
656
697
  ```sh
@@ -631,13 +631,16 @@ func insertRun(t *testing.T, path, id, state string) {
631
631
  // 8.4: fully-flagged repo register never prompts and produces the same
632
632
  // repo entry the interactive run would post.
633
633
  type registerServer struct {
634
- ackServer // embed for the unreachable stubs
635
- fields []string
636
- gotInput *repo.RegisterInput
637
- gotInputs []repo.RegisterInput
638
- successful []repo.RegisterInput
639
- failName string
640
- calls int
634
+ ackServer // embed for the unreachable stubs
635
+ fields []string
636
+ gotInput *repo.RegisterInput
637
+ gotInputs []repo.RegisterInput
638
+ successful []repo.RegisterInput
639
+ failName string
640
+ calls int
641
+ candidates []runner.RepoCandidate
642
+ ensureCalls []runner.RepoCandidate
643
+ registered []repo.Info
641
644
  }
642
645
 
643
646
  func (s *registerServer) TaskRegistrationFields(context.Context, config.RawValues) ([]task.RegistrationField, error) {
@@ -648,6 +651,25 @@ func (s *registerServer) TaskRegistrationFields(context.Context, config.RawValue
648
651
  }
649
652
  return out, nil
650
653
  }
654
+ func (s *registerServer) DiscoverRepos(context.Context) ([]runner.RepoCandidate, error) {
655
+ return append([]runner.RepoCandidate(nil), s.candidates...), nil
656
+ }
657
+
658
+ func (s *registerServer) EnsureRepo(_ context.Context, candidate runner.RepoCandidate) error {
659
+ s.ensureCalls = append(s.ensureCalls, candidate)
660
+ for _, existing := range s.candidates {
661
+ if existing.Path == candidate.Path {
662
+ return nil
663
+ }
664
+ }
665
+ s.candidates = append(s.candidates, candidate)
666
+ return nil
667
+ }
668
+
669
+ func (s *registerServer) ListRepos(context.Context) ([]repo.Info, error) {
670
+ return append([]repo.Info(nil), s.registered...), nil
671
+ }
672
+
651
673
  func (s *registerServer) RegisterRepo(_ context.Context, in repo.RegisterInput) (repo.Info, error) {
652
674
  s.calls++
653
675
  cp := in
@@ -657,7 +679,9 @@ func (s *registerServer) RegisterRepo(_ context.Context, in repo.RegisterInput)
657
679
  return repo.Info{}, errors.New("registration failed")
658
680
  }
659
681
  s.successful = append(s.successful, cp)
660
- return repo.Info{Name: in.Name, Path: in.Path, TaskConfig: in.TaskConfig}, nil
682
+ info := repo.Info{Name: in.Name, Path: in.Path, TaskConfig: in.TaskConfig}
683
+ s.registered = append(s.registered, info)
684
+ return info, nil
661
685
  }
662
686
 
663
687
  func serveRegister(t *testing.T, home string, fields []string) *registerServer {
@@ -831,6 +855,91 @@ func TestRegistrationTaskConfigBuildsNestedStatusDefaults(t *testing.T) {
831
855
  }
832
856
  }
833
857
 
858
+ func TestRepoAddValidationRejectsDuplicateNormalizedPathAndName(t *testing.T) {
859
+ candidates := []runner.RepoCandidate{{Name: "payments", Path: "/work/payments"}}
860
+ if err := validateAddedRepo(runner.RepoCandidate{Name: "new", Path: "/work/payments/."}, candidates, nil); err == nil || !strings.Contains(err.Error(), "already discovered") {
861
+ t.Fatalf("duplicate discovered path error = %v", err)
862
+ }
863
+ registered := []repo.Info{{Name: "checkout", Path: "/work/checkout"}}
864
+ if err := validateAddedRepo(runner.RepoCandidate{Name: " checkout ", Path: "/work/new"}, candidates, registered); err == nil || !strings.Contains(err.Error(), "already registered") {
865
+ t.Fatalf("duplicate registered name error = %v", err)
866
+ }
867
+ for _, candidate := range []runner.RepoCandidate{{Name: "", Path: "/work/new"}, {Name: "new", Path: ""}} {
868
+ if err := validateAddedRepo(candidate, nil, nil); err == nil {
869
+ t.Fatalf("validateAddedRepo(%+v) accepted missing input", candidate)
870
+ }
871
+ }
872
+ }
873
+
874
+ func TestInteractiveRepoSelectionPreservesSelectionsAndAddsMultiple(t *testing.T) {
875
+ home := t.TempDir()
876
+ deps := serveRegister(t, home, nil)
877
+ deps.candidates = []runner.RepoCandidate{{Name: "existing", Path: "/work/existing"}}
878
+ client := server.NewClient(filepath.Join(home, ".relay-flow", "server.sock"))
879
+
880
+ selectionSteps := [][]int{{0, addRepositorySelection}, {0, 1, addRepositorySelection}, {0, 1, 2}}
881
+ addSteps := []runner.RepoCandidate{
882
+ {Name: "new-one", Path: filepath.Join(t.TempDir(), "new-one")},
883
+ {Name: "new-two", Path: filepath.Join(t.TempDir(), "new-two")},
884
+ }
885
+ selectionCalls := 0
886
+ addCalls := 0
887
+ candidates, selected, err := selectReposInteractiveWithPrompts(
888
+ context.Background(), client, deps.candidates, nil,
889
+ func(got []runner.RepoCandidate, selected []int) ([]int, error) {
890
+ wantSelected := [][]int{nil, {0, 1}, {0, 1, 2}}[selectionCalls]
891
+ if fmt.Sprint(selected) != fmt.Sprint(wantSelected) {
892
+ t.Fatalf("selection %d started with %v, want %v", selectionCalls, selected, wantSelected)
893
+ }
894
+ if selectionCalls >= len(selectionSteps) {
895
+ t.Fatalf("unexpected selection call %d with candidates %+v", selectionCalls, got)
896
+ }
897
+ step := append([]int(nil), selectionSteps[selectionCalls]...)
898
+ selectionCalls++
899
+ return step, nil
900
+ },
901
+ func() (runner.RepoCandidate, error) {
902
+ if addCalls >= len(addSteps) {
903
+ return runner.RepoCandidate{}, errors.New("unexpected add prompt")
904
+ }
905
+ step := addSteps[addCalls]
906
+ addCalls++
907
+ return step, nil
908
+ },
909
+ )
910
+ if err != nil {
911
+ t.Fatal(err)
912
+ }
913
+ if selectionCalls != 3 || addCalls != 2 || len(deps.ensureCalls) != 2 {
914
+ t.Fatalf("selection calls=%d add calls=%d ensure calls=%v", selectionCalls, addCalls, deps.ensureCalls)
915
+ }
916
+ if len(candidates) != 3 || len(selected) != 3 || fmt.Sprint(selected) != "[0 1 2]" {
917
+ t.Fatalf("final candidates=%+v selected=%v", candidates, selected)
918
+ }
919
+ if candidates[1].Name != "new-one" || candidates[2].Name != "new-two" {
920
+ t.Fatalf("final candidate names=%+v", candidates)
921
+ }
922
+ }
923
+
924
+ func TestRepoSelectionOptionsIncludeVisibleAddAction(t *testing.T) {
925
+ options := repoSelectionOptions([]runner.RepoCandidate{{Name: "payments", Path: "/work/payments"}})
926
+ if len(options) != 2 {
927
+ t.Fatalf("options = %d, want candidate plus Add action", len(options))
928
+ }
929
+ var selected []int
930
+ field := repoMultiSelect(options, &selected)
931
+ var out bytes.Buffer
932
+ if err := field.RunAccessible(&out, strings.NewReader("2\n")); err != nil {
933
+ t.Fatal(err)
934
+ }
935
+ if len(selected) != 1 || selected[0] != addRepositorySelection {
936
+ t.Fatalf("selected = %v, want Add action sentinel", selected)
937
+ }
938
+ if !strings.Contains(out.String(), "Add repository") {
939
+ t.Fatalf("output %q missing Add repository", out.String())
940
+ }
941
+ }
942
+
834
943
  func TestRepoMultiSelectAndPluginMapping(t *testing.T) {
835
944
  selected := []int{0, 1}
836
945
  field := repoMultiSelect([]huh.Option[int]{