relay-flow 0.2.8-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 +62 -35
- package/cmd/relay-flow/commands_test.go +117 -8
- package/cmd/relay-flow/main.go +167 -60
- package/cmd/relay-flow/onboarding.go +344 -0
- package/cmd/relay-flow/onboarding_test.go +515 -0
- package/cmd/relay-flow/repo_registration.go +219 -0
- package/cmd/relay-flow/serve.go +3 -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 +34 -3
- package/internal/repo/service_test.go +22 -0
- 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 +19 -0
- package/internal/server/client.go +10 -0
- package/internal/server/fixture_test.go +7 -0
- package/internal/server/server.go +41 -0
- package/internal/task/jira/testdata/jira_search_issues.json +4 -4
- package/internal/workflow/workflow.go +4 -1
- package/internal/workflow/workflow_test.go +4 -4
- 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.
|
|
27
|
+
opencode plugin relay-flow-plugin@0.2.9-alpha
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
**Pi**
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
pi install npm:relay-flow-plugin@0.2.
|
|
33
|
+
pi install npm:relay-flow-plugin@0.2.9-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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
###
|
|
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,9 +82,11 @@ 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
|
-
###
|
|
89
|
+
### 5. Submit a workflow
|
|
87
90
|
|
|
88
91
|
```sh
|
|
89
92
|
relay-flow workflow submit --file examples/minimal-jira-task-workflow.yaml
|
|
@@ -154,7 +157,7 @@ OpenCode plugin configuration uses both entrypoints. The server entrypoint is li
|
|
|
154
157
|
```json
|
|
155
158
|
{
|
|
156
159
|
"$schema": "https://opencode.ai/config.json",
|
|
157
|
-
"plugin": ["relay-flow-plugin@0.2.
|
|
160
|
+
"plugin": ["relay-flow-plugin@0.2.9-alpha"]
|
|
158
161
|
}
|
|
159
162
|
```
|
|
160
163
|
|
|
@@ -163,7 +166,7 @@ The native HITL approval entrypoint is listed in `.opencode/tui.json`:
|
|
|
163
166
|
```json
|
|
164
167
|
{
|
|
165
168
|
"$schema": "https://opencode.ai/tui.json",
|
|
166
|
-
"plugin": ["relay-flow-plugin@0.2.
|
|
169
|
+
"plugin": ["relay-flow-plugin@0.2.9-alpha"]
|
|
167
170
|
}
|
|
168
171
|
```
|
|
169
172
|
|
|
@@ -179,7 +182,7 @@ Pi plugin: install the same published package manually in Pi's global package
|
|
|
179
182
|
settings before starting a Pi harness session:
|
|
180
183
|
|
|
181
184
|
```sh
|
|
182
|
-
pi install npm:relay-flow-plugin@0.2.
|
|
185
|
+
pi install npm:relay-flow-plugin@0.2.9-alpha
|
|
183
186
|
```
|
|
184
187
|
|
|
185
188
|
Relay-flow does not install or configure the package automatically. Pi resolves
|
|
@@ -199,10 +202,30 @@ internal and is never part of either plugin payload.
|
|
|
199
202
|
|
|
200
203
|
```sh
|
|
201
204
|
relay-flow init
|
|
202
|
-
relay-flow task auth
|
|
203
205
|
```
|
|
204
206
|
|
|
205
|
-
`init` selects the task system, runner, harness,
|
|
207
|
+
On a new interactive home, `init` selects the task system, runner, harness,
|
|
208
|
+
and durable executor, runs the selected task plug-in's authentication flow,
|
|
209
|
+
and asks whether to register repositories. It writes machine config and the
|
|
210
|
+
selected execution backend only after authentication succeeds. A skipped
|
|
211
|
+
repository setup does not start the server; selecting it starts or reuses the
|
|
212
|
+
background server, waits for socket readiness, and invokes the same
|
|
213
|
+
`repo register` form described below. The server remains running after a
|
|
214
|
+
successful guided registration.
|
|
215
|
+
|
|
216
|
+
For scripts and existing installations, keep the independent sequence:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
relay-flow init --task-plugin jira --runner-plugin orca --harness-plugin opencode
|
|
220
|
+
relay-flow task auth --site https://company.atlassian.net --email you@example.com --token "$JIRA_API_TOKEN"
|
|
221
|
+
relay-flow serve --background
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`task auth` delegates to the selected task plug-in. Jira prompts for its site,
|
|
225
|
+
email, and masked API token when flags are omitted, validates `/myself`, and
|
|
226
|
+
owns the system-wide `credentials.yaml`. A normal init rerun refuses existing
|
|
227
|
+
state. `relay-flow init --force` updates safe stopped instances while
|
|
228
|
+
preserving durable and repo state; it does not launch repository onboarding.
|
|
206
229
|
|
|
207
230
|
For Beads, select the plugin explicitly when scripting setup:
|
|
208
231
|
|
|
@@ -234,9 +257,11 @@ workflows/<name>.yaml 0644 submitted workflow definitions
|
|
|
234
257
|
|
|
235
258
|
### Register a repo
|
|
236
259
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
260
|
+
The guided first-run flow can start the server and open repository registration
|
|
261
|
+
for you. `repo register` remains a server-backed standalone command for later
|
|
262
|
+
additions: if no server is running, start it with
|
|
263
|
+
`relay-flow serve --background` (or rerun interactive `relay-flow init` for a
|
|
264
|
+
new home).
|
|
240
265
|
|
|
241
266
|
```sh
|
|
242
267
|
relay-flow serve --background
|
|
@@ -369,8 +394,9 @@ Task, runner, harness, and durable executor plugins are selected machine-wide. A
|
|
|
369
394
|
|
|
370
395
|
### Run
|
|
371
396
|
|
|
372
|
-
The
|
|
373
|
-
|
|
397
|
+
The guided first-run flow starts or reuses the server before `repo register`.
|
|
398
|
+
Outside that flow, `repo register` and `workflow submit` require the server to
|
|
399
|
+
already be running; the same process polls repos and drives runs.
|
|
374
400
|
|
|
375
401
|
```sh
|
|
376
402
|
relay-flow serve # normal start; requires an initialized database
|
|
@@ -455,6 +481,7 @@ Rules enforced at submit:
|
|
|
455
481
|
- The graph must be fully reachable from `start`; unknown fields are rejected; `runnerPlugin`/`harnessPlugin`/`closeOn`/legacy `tasks`/`runner` keys are rejected.
|
|
456
482
|
- Only `agent` and `hitl` nodes receive mailbox subtasks; `start` and `end` never do.
|
|
457
483
|
- `cleanupRunnerOnEnd` is the only workflow cleanup knob and takes priority over terminal retention after `end`; the word `terminal` refers to runner terminals only.
|
|
484
|
+
- `nudgePrompt` supports `{{taskSystem}}`, `{{ticket}}`, `{{workflow}}`, `{{repo}}`, `{{node}}`, `{{mailbox}}`, and `{{nextSteps}}`; unsupported variables are rejected at submission.
|
|
458
485
|
|
|
459
486
|
### Task-config merge
|
|
460
487
|
|
|
@@ -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
|
|
635
|
-
fields
|
|
636
|
-
gotInput
|
|
637
|
-
gotInputs
|
|
638
|
-
successful
|
|
639
|
-
failName
|
|
640
|
-
calls
|
|
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
|
-
|
|
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]{
|