relay-flow 0.2.0-alpha → 0.2.2-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 +171 -26
- package/cmd/relay-flow/beads_composition_test.go +451 -0
- package/cmd/relay-flow/commands_test.go +593 -15
- package/cmd/relay-flow/main.go +356 -119
- package/cmd/relay-flow/scenario_test.go +246 -35
- package/cmd/relay-flow/serve.go +5 -2
- package/examples/beads-workflow.yaml +74 -0
- package/examples/default-story-workflow.yaml +88 -0
- package/go.mod +2 -1
- package/go.sum +2 -0
- package/internal/config/config.go +13 -2
- package/internal/config/merge_test.go +18 -0
- package/internal/execution/goworkflows/activities.go +136 -87
- package/internal/execution/goworkflows/end_feedback_test.go +124 -0
- package/internal/execution/goworkflows/engine.go +41 -8
- package/internal/execution/goworkflows/engine_test.go +100 -22
- package/internal/execution/goworkflows/fakes_test.go +63 -25
- package/internal/execution/goworkflows/interpreter.go +45 -30
- package/internal/execution/goworkflows/mailbox_test.go +85 -0
- package/internal/execution/goworkflows/node_runtime_integration_test.go +12 -6
- package/internal/execution/goworkflows/node_runtime_test.go +72 -23
- package/internal/execution/goworkflows/recovery_test.go +7 -7
- package/internal/execution/goworkflows/report_contract_fixture_test.go +31 -0
- package/internal/execution/goworkflows/retry_log_test.go +11 -11
- package/internal/harness/contract_test.go +15 -0
- package/internal/harness/factory.go +25 -3
- package/internal/harness/harness.go +30 -4
- package/internal/harness/opencode/opencode.go +125 -10
- package/internal/harness/opencode/opencode_test.go +183 -0
- package/internal/harness/opencode/repo_setup.go +361 -0
- package/internal/harness/plugin_selection_test.go +5 -5
- package/internal/paths/paths.go +18 -16
- package/internal/recover/recover.go +11 -6
- package/internal/repo/repo.go +13 -0
- package/internal/repo/service.go +10 -0
- package/internal/repo/service_test.go +55 -6
- package/internal/router/router.go +3 -2
- package/internal/router/router_test.go +87 -0
- package/internal/run/manager.go +14 -1
- package/internal/run/run.go +6 -4
- package/internal/run/run_manager_test.go +21 -1
- package/internal/runner/contract_test.go +64 -26
- package/internal/runner/orca/orca.go +30 -54
- package/internal/runner/orca/orca_test.go +143 -4
- package/internal/runner/orca/orcacli/orcacli.go +5 -0
- package/internal/runner/orca/orcacli/orcacli_test.go +3 -0
- package/internal/runner/orca/orcacli/testdata/strict-orca.sh +2 -0
- package/internal/runner/runner.go +15 -8
- package/internal/task/auth_test.go +48 -0
- package/internal/task/beads/bdcli/bdcli.go +323 -0
- package/internal/task/beads/bdcli/bdcli_test.go +297 -0
- package/internal/task/beads/bdcli/testdata/array.json +1 -0
- package/internal/task/beads/bdcli/testdata/children.json +1 -0
- package/internal/task/beads/bdcli/testdata/claimed.json +1 -0
- package/internal/task/beads/bdcli/testdata/commented.json +1 -0
- package/internal/task/beads/bdcli/testdata/comments.json +1 -0
- package/internal/task/beads/bdcli/testdata/created.json +1 -0
- package/internal/task/beads/bdcli/testdata/object.json +1 -0
- package/internal/task/beads/bdcli/testdata/ready.json +1 -0
- package/internal/task/beads/bdcli/testdata/show.json +1 -0
- package/internal/task/beads/bdcli/testdata/strict-bd.sh +149 -0
- package/internal/task/beads/bdcli/testdata/updated.json +1 -0
- package/internal/task/beads/beads.go +840 -0
- package/internal/task/beads/beads_test.go +609 -0
- package/internal/task/beads/comments_test.go +242 -0
- package/internal/task/beads/config_compatibility_test.go +163 -0
- package/internal/task/beads/lifecycle_inheritance_test.go +168 -0
- package/internal/task/beads/repo_composition_test.go +232 -0
- package/internal/task/beads/runtime_config_test.go +81 -0
- package/internal/task/beads/status_compatibility_test.go +233 -0
- package/internal/task/beads/status_test.go +257 -0
- package/internal/task/beads/testdata/strict-bd-repo.sh +27 -0
- package/internal/task/beads/validation_test.go +110 -0
- package/internal/task/contract_test.go +12 -0
- package/internal/task/factory.go +52 -3
- package/internal/task/jira/auth.go +209 -0
- package/internal/task/jira/auth_test.go +160 -0
- package/internal/task/jira/effects_test.go +39 -0
- package/internal/task/jira/filters_test.go +96 -16
- package/internal/task/jira/helpers_test.go +29 -19
- package/internal/task/jira/jira.go +263 -90
- package/internal/task/jira/lifecycle_inheritance_test.go +172 -0
- package/internal/task/jira/normalize.go +32 -14
- package/internal/task/jira/rest/adf.go +165 -0
- package/internal/task/jira/rest/adf_test.go +60 -0
- package/internal/task/jira/rest/client.go +573 -0
- package/internal/task/jira/rest/client_test.go +381 -0
- package/internal/task/jira/templates_test.go +118 -0
- package/internal/task/jira/transition_defaults_test.go +22 -18
- package/internal/task/jira/validation_test.go +1 -1
- package/internal/task/task.go +32 -0
- package/internal/workflow/report_test.go +45 -0
- package/internal/workflow/workflow.go +9 -6
- package/internal/workflow/workflow_test.go +14 -12
- package/package.json +2 -1
- package/internal/task/jira/acli/acli.go +0 -306
- package/internal/task/jira/acli/acli_test.go +0 -208
- package/internal/task/jira/acli/testdata/acli_comments.json +0 -55
- package/internal/task/jira/acli/testdata/search_invalid_assignee.txt +0 -1
- package/internal/task/jira/acli/testdata/search_invalid_status.txt +0 -1
- /package/internal/task/{jira/acli/testdata/search_success.json → beads/bdcli/testdata/empty.json} +0 -0
- /package/internal/task/jira/testdata/{acli_search.json → jira_search_issues.json} +0 -0
|
@@ -2,9 +2,11 @@ package main
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"encoding/json"
|
|
5
6
|
"errors"
|
|
7
|
+
"fmt"
|
|
6
8
|
"path/filepath"
|
|
7
|
-
"
|
|
9
|
+
"reflect"
|
|
8
10
|
"strings"
|
|
9
11
|
"sync"
|
|
10
12
|
"testing"
|
|
@@ -14,23 +16,34 @@ import (
|
|
|
14
16
|
"github.com/rajpopat27/relay-flow/internal/execution/goworkflows"
|
|
15
17
|
"github.com/rajpopat27/relay-flow/internal/harness"
|
|
16
18
|
"github.com/rajpopat27/relay-flow/internal/identity"
|
|
19
|
+
"github.com/rajpopat27/relay-flow/internal/paths"
|
|
17
20
|
"github.com/rajpopat27/relay-flow/internal/repo"
|
|
18
21
|
runsvc "github.com/rajpopat27/relay-flow/internal/run"
|
|
19
22
|
"github.com/rajpopat27/relay-flow/internal/runner"
|
|
23
|
+
"github.com/rajpopat27/relay-flow/internal/server"
|
|
20
24
|
"github.com/rajpopat27/relay-flow/internal/task"
|
|
21
25
|
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
22
26
|
)
|
|
23
27
|
|
|
24
|
-
const
|
|
28
|
+
const (
|
|
29
|
+
scenarioTaskPlugin = "scenario-e2e-task"
|
|
30
|
+
scenarioRunnerPlugin = "scenario-e2e-runner"
|
|
31
|
+
scenarioHarnessPlugin = "scenario-e2e-harness"
|
|
32
|
+
)
|
|
25
33
|
|
|
26
34
|
var (
|
|
27
|
-
scenarioFactoryMu
|
|
28
|
-
scenarioFactorySystem
|
|
35
|
+
scenarioFactoryMu sync.Mutex
|
|
36
|
+
scenarioFactorySystem task.System
|
|
37
|
+
scenarioFactoryRunner runner.Runner
|
|
38
|
+
scenarioFactoryHarness harness.Harness
|
|
29
39
|
)
|
|
30
40
|
|
|
31
41
|
func init() {
|
|
32
42
|
task.Register(scenarioTaskPlugin, task.Factory{
|
|
33
43
|
RequiredRepoKeys: func() []string { return nil },
|
|
44
|
+
DefaultConfig: func() config.RawValues {
|
|
45
|
+
return config.RawValues{"templates": map[string]any{"format": "alternate"}}
|
|
46
|
+
},
|
|
34
47
|
TaskScopeKey: func(config.RawValues, config.RawValues) (string, error) {
|
|
35
48
|
return "scenario-scope", nil
|
|
36
49
|
},
|
|
@@ -40,9 +53,37 @@ func init() {
|
|
|
40
53
|
if scenarioFactorySystem == nil {
|
|
41
54
|
return nil, errors.New("scenario task system not configured")
|
|
42
55
|
}
|
|
56
|
+
if fake, ok := scenarioFactorySystem.(*scenarioTaskSystem); ok {
|
|
57
|
+
fake.log.add("factory:task:" + scenarioTaskPlugin)
|
|
58
|
+
}
|
|
43
59
|
return scenarioFactorySystem, nil
|
|
44
60
|
},
|
|
45
61
|
})
|
|
62
|
+
runner.Register(scenarioRunnerPlugin, func(config.RawValues) (runner.Runner, error) {
|
|
63
|
+
scenarioFactoryMu.Lock()
|
|
64
|
+
defer scenarioFactoryMu.Unlock()
|
|
65
|
+
if scenarioFactoryRunner == nil {
|
|
66
|
+
return nil, errors.New("scenario runner not configured")
|
|
67
|
+
}
|
|
68
|
+
if fake, ok := scenarioFactoryRunner.(*scenarioRunner); ok {
|
|
69
|
+
fake.log.add("factory:runner:" + scenarioRunnerPlugin)
|
|
70
|
+
}
|
|
71
|
+
return scenarioFactoryRunner, nil
|
|
72
|
+
})
|
|
73
|
+
harness.Register(scenarioHarnessPlugin, harness.Factory{
|
|
74
|
+
DefaultConfig: func() config.RawValues { return config.RawValues{"runtime": "alternate"} },
|
|
75
|
+
New: func(config.RawValues) (harness.Harness, error) {
|
|
76
|
+
scenarioFactoryMu.Lock()
|
|
77
|
+
defer scenarioFactoryMu.Unlock()
|
|
78
|
+
if scenarioFactoryHarness == nil {
|
|
79
|
+
return nil, errors.New("scenario harness not configured")
|
|
80
|
+
}
|
|
81
|
+
if fake, ok := scenarioFactoryHarness.(*scenarioHarness); ok {
|
|
82
|
+
fake.log.add("factory:harness:" + scenarioHarnessPlugin)
|
|
83
|
+
}
|
|
84
|
+
return scenarioFactoryHarness, nil
|
|
85
|
+
},
|
|
86
|
+
})
|
|
46
87
|
}
|
|
47
88
|
|
|
48
89
|
func setScenarioFactorySystem(system task.System) {
|
|
@@ -51,6 +92,14 @@ func setScenarioFactorySystem(system task.System) {
|
|
|
51
92
|
scenarioFactoryMu.Unlock()
|
|
52
93
|
}
|
|
53
94
|
|
|
95
|
+
func setScenarioFactoryAdapters(system task.System, rnr runner.Runner, hrn harness.Harness) {
|
|
96
|
+
scenarioFactoryMu.Lock()
|
|
97
|
+
scenarioFactorySystem = system
|
|
98
|
+
scenarioFactoryRunner = rnr
|
|
99
|
+
scenarioFactoryHarness = hrn
|
|
100
|
+
scenarioFactoryMu.Unlock()
|
|
101
|
+
}
|
|
102
|
+
|
|
54
103
|
// These scenarios exercise the same composition chain as serve:
|
|
55
104
|
// RepoPoller/handleBatch -> RunManager -> real go-workflows SQLite engine.
|
|
56
105
|
// The only replacements are the documented task, runner, and harness seams.
|
|
@@ -89,6 +138,130 @@ func TestScenarioHappyPath(t *testing.T) {
|
|
|
89
138
|
assertExactHappyEffects(t, f)
|
|
90
139
|
}
|
|
91
140
|
|
|
141
|
+
func TestCompositionRootSelectsAlternatePluginsForDurableRun(t *testing.T) {
|
|
142
|
+
log := newScenarioLog()
|
|
143
|
+
tasks := newScenarioTaskSystem(log)
|
|
144
|
+
rnr := newScenarioRunner(log)
|
|
145
|
+
hrn := newScenarioHarness(log)
|
|
146
|
+
setScenarioFactoryAdapters(tasks, rnr, hrn)
|
|
147
|
+
|
|
148
|
+
root := filepath.Join(t.TempDir(), ".relay-flow")
|
|
149
|
+
t.Setenv("RELAY_FLOW_HOME", root)
|
|
150
|
+
p, err := home()
|
|
151
|
+
if err != nil {
|
|
152
|
+
t.Fatal(err)
|
|
153
|
+
}
|
|
154
|
+
if err := paths.Ensure(p); err != nil {
|
|
155
|
+
t.Fatal(err)
|
|
156
|
+
}
|
|
157
|
+
if err := config.SaveMachine(p.Config, &config.Machine{
|
|
158
|
+
PollIntervalSeconds: 1,
|
|
159
|
+
TaskPlugin: scenarioTaskPlugin,
|
|
160
|
+
TaskConfig: config.RawValues{"provider": "alternate"},
|
|
161
|
+
RunnerPlugin: scenarioRunnerPlugin,
|
|
162
|
+
RunnerConfig: config.RawValues{"transport": "opaque"},
|
|
163
|
+
HarnessPlugin: scenarioHarnessPlugin,
|
|
164
|
+
HarnessConfig: config.RawValues{"runtime": "alternate"},
|
|
165
|
+
Repos: map[string]config.Repo{
|
|
166
|
+
scenarioRepo: {Path: scenarioRepoPath, TaskConfig: config.RawValues{"scope": "test"}},
|
|
167
|
+
},
|
|
168
|
+
}); err != nil {
|
|
169
|
+
t.Fatal(err)
|
|
170
|
+
}
|
|
171
|
+
if err := (&workflow.Store{Dir: p.Workflows}).Put(scenarioWorkflowName, scenarioWorkflowYAML); err != nil {
|
|
172
|
+
t.Fatal(err)
|
|
173
|
+
}
|
|
174
|
+
if err := goworkflows.InitDatabase(p.Database); err != nil {
|
|
175
|
+
t.Fatal(err)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
179
|
+
done := make(chan error, 1)
|
|
180
|
+
stopped := false
|
|
181
|
+
go func() { done <- serveRoot(ctx, p, false) }()
|
|
182
|
+
t.Cleanup(func() {
|
|
183
|
+
if stopped {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
cancel()
|
|
187
|
+
select {
|
|
188
|
+
case <-done:
|
|
189
|
+
case <-time.After(10 * time.Second):
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
client := server.NewClient(p.Socket)
|
|
194
|
+
waitForServer(t, client)
|
|
195
|
+
var active runsvc.Run
|
|
196
|
+
waitScenario(t, 10*time.Second, func() bool {
|
|
197
|
+
var lookupErr error
|
|
198
|
+
active, lookupErr = client.GetRunByTicket(context.Background(), scenarioTicket)
|
|
199
|
+
return lookupErr == nil && active.State == runsvc.StateWaiting && active.CurrentNode == "implement"
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
launch := hrn.launch("implement")
|
|
203
|
+
wantPrompt := "initial taskSystem=" + scenarioTaskPlugin
|
|
204
|
+
if !strings.Contains(launch.Prompt, wantPrompt) {
|
|
205
|
+
t.Fatalf("selected task system did not reach launch prompt: %q", launch.Prompt)
|
|
206
|
+
}
|
|
207
|
+
command := rnr.command(scenarioTicket + ":implement")
|
|
208
|
+
wantArgs := []string{"opaque", launch.Prompt}
|
|
209
|
+
if command.Executable != "fake-harness" || !reflect.DeepEqual(command.Args, wantArgs) {
|
|
210
|
+
t.Fatalf("runner command = %#v, want opaque harness command args %#v", command, wantArgs)
|
|
211
|
+
}
|
|
212
|
+
for _, event := range []string{
|
|
213
|
+
"factory:task:" + scenarioTaskPlugin,
|
|
214
|
+
"factory:runner:" + scenarioRunnerPlugin,
|
|
215
|
+
"factory:harness:" + scenarioHarnessPlugin,
|
|
216
|
+
"task-config-validated",
|
|
217
|
+
"runner-repo-validated",
|
|
218
|
+
"harness-agent-validated:implementer",
|
|
219
|
+
"harness-launched:" + scenarioTicket + ":implement",
|
|
220
|
+
"terminal-created:" + scenarioTicket + ":implement",
|
|
221
|
+
} {
|
|
222
|
+
if log.countPrefix(event) == 0 {
|
|
223
|
+
t.Fatalf("composition did not call selected interface boundary %q; events=%v", event, log.all())
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for i, next := range []string{"verify", "pr-review", "end"} {
|
|
228
|
+
current, getErr := client.GetRunByTicket(context.Background(), scenarioTicket)
|
|
229
|
+
if getErr != nil {
|
|
230
|
+
t.Fatal(getErr)
|
|
231
|
+
}
|
|
232
|
+
ack, submitErr := client.SubmitReport(context.Background(), runsvc.ReportRequest{
|
|
233
|
+
RunID: current.ID, Node: current.CurrentNode,
|
|
234
|
+
ReportID: "alternate-session:message-" + fmt.Sprint(i+1),
|
|
235
|
+
Report: scenarioReport(workflow.OutcomeSuccess, next),
|
|
236
|
+
})
|
|
237
|
+
if submitErr != nil || !ack.Accepted {
|
|
238
|
+
t.Fatalf("submit durable report %s -> %s: ack=%+v err=%v", current.CurrentNode, next, ack, submitErr)
|
|
239
|
+
}
|
|
240
|
+
if next != "end" {
|
|
241
|
+
waitScenario(t, 10*time.Second, func() bool {
|
|
242
|
+
advanced, advanceErr := client.GetRunByTicket(context.Background(), scenarioTicket)
|
|
243
|
+
return advanceErr == nil && advanced.State == runsvc.StateWaiting && advanced.CurrentNode == next
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
waitScenario(t, 10*time.Second, func() bool {
|
|
248
|
+
finished, getErr := client.GetRunByTicket(context.Background(), scenarioTicket)
|
|
249
|
+
return getErr == nil && finished.State == runsvc.StateCompleted
|
|
250
|
+
})
|
|
251
|
+
if err := client.Stop(context.Background()); err != nil {
|
|
252
|
+
t.Fatal(err)
|
|
253
|
+
}
|
|
254
|
+
select {
|
|
255
|
+
case err := <-done:
|
|
256
|
+
if err != nil {
|
|
257
|
+
t.Fatalf("serveRoot: %v", err)
|
|
258
|
+
}
|
|
259
|
+
case <-time.After(10 * time.Second):
|
|
260
|
+
t.Fatal("serveRoot did not stop")
|
|
261
|
+
}
|
|
262
|
+
stopped = true
|
|
263
|
+
}
|
|
264
|
+
|
|
92
265
|
func TestScenarioHITLRejectLoop(t *testing.T) {
|
|
93
266
|
f := newScenarioFixture(t)
|
|
94
267
|
f.pollOnce()
|
|
@@ -249,7 +422,7 @@ func TestScenarioLateRegisterAndLateSubmit(t *testing.T) {
|
|
|
249
422
|
}
|
|
250
423
|
repoService := repo.NewServiceWithRegistry(repo.ServiceConfig{
|
|
251
424
|
ConfigPath: configPath, TaskPlugin: scenarioTaskPlugin, Runner: fr,
|
|
252
|
-
Active: engine, Workflows: wfService.Registry(),
|
|
425
|
+
Harness: fh, Active: engine, Workflows: wfService.Registry(),
|
|
253
426
|
}, reg)
|
|
254
427
|
deps := &serveDeps{
|
|
255
428
|
repos: repoService,
|
|
@@ -618,6 +791,7 @@ type scenarioTaskSystem struct {
|
|
|
618
791
|
mu sync.Mutex
|
|
619
792
|
|
|
620
793
|
claimed bool
|
|
794
|
+
renderText func(task.TextKind, task.TextData) (string, error)
|
|
621
795
|
mailboxes map[string]task.Mailbox
|
|
622
796
|
specs map[string]task.MailboxSpec
|
|
623
797
|
mailboxStatus map[string]string
|
|
@@ -629,6 +803,8 @@ type scenarioTaskSystem struct {
|
|
|
629
803
|
completeFailures int
|
|
630
804
|
}
|
|
631
805
|
|
|
806
|
+
var _ task.System = (*scenarioTaskSystem)(nil)
|
|
807
|
+
|
|
632
808
|
func newScenarioTaskSystem(log *scenarioLog) *scenarioTaskSystem {
|
|
633
809
|
return &scenarioTaskSystem{
|
|
634
810
|
log: log, mailboxes: map[string]task.Mailbox{}, specs: map[string]task.MailboxSpec{},
|
|
@@ -660,9 +836,26 @@ func (s *scenarioTaskSystem) Claim(_ context.Context, ref task.TicketRef, workfl
|
|
|
660
836
|
}
|
|
661
837
|
|
|
662
838
|
func (s *scenarioTaskSystem) ValidateConfig(context.Context, config.RawValues, map[string]config.RawValues) error {
|
|
839
|
+
s.log.add("task-config-validated")
|
|
663
840
|
return nil
|
|
664
841
|
}
|
|
665
842
|
|
|
843
|
+
func (s *scenarioTaskSystem) RenderText(kind task.TextKind, data task.TextData) (string, error) {
|
|
844
|
+
if s.renderText != nil {
|
|
845
|
+
return s.renderText(kind, data)
|
|
846
|
+
}
|
|
847
|
+
switch kind {
|
|
848
|
+
case task.TextMailboxDescription:
|
|
849
|
+
return "mailbox " + data.Node + ": " + data.NodeDescription, nil
|
|
850
|
+
case task.TextSummaryComment:
|
|
851
|
+
return "SUMMARY\n" + data.SummaryReport, nil
|
|
852
|
+
case task.TextFeedbackComment:
|
|
853
|
+
return "Feedback from " + data.SourceNode + " to " + data.TargetNode + " mailbox " + data.Mailbox + "\n" + data.FeedbackReport, nil
|
|
854
|
+
default:
|
|
855
|
+
return "", fmt.Errorf("unknown task text kind %q", kind)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
666
859
|
func (s *scenarioTaskSystem) EnsureMailboxes(_ context.Context, parent task.TicketRef, _ string, specs []task.MailboxSpec) (map[string]task.Mailbox, error) {
|
|
667
860
|
s.log.add("mailboxes:ensure")
|
|
668
861
|
s.mu.Lock()
|
|
@@ -827,6 +1020,8 @@ type scenarioRunner struct {
|
|
|
827
1020
|
cleanups int
|
|
828
1021
|
}
|
|
829
1022
|
|
|
1023
|
+
var _ runner.Runner = (*scenarioRunner)(nil)
|
|
1024
|
+
|
|
830
1025
|
func newScenarioRunner(log *scenarioLog) *scenarioRunner {
|
|
831
1026
|
return &scenarioRunner{
|
|
832
1027
|
log: log, environments: map[string]runner.Environment{}, terminals: map[string]*scenarioTerminal{},
|
|
@@ -838,7 +1033,10 @@ func (r *scenarioRunner) DiscoverRepos(context.Context) ([]runner.RepoCandidate,
|
|
|
838
1033
|
return []runner.RepoCandidate{{Name: scenarioRepo, Path: scenarioRepoPath}}, nil
|
|
839
1034
|
}
|
|
840
1035
|
|
|
841
|
-
func (r *scenarioRunner) ValidateRepo(context.Context, string, string) error {
|
|
1036
|
+
func (r *scenarioRunner) ValidateRepo(context.Context, string, string) error {
|
|
1037
|
+
r.log.add("runner-repo-validated")
|
|
1038
|
+
return nil
|
|
1039
|
+
}
|
|
842
1040
|
|
|
843
1041
|
func (r *scenarioRunner) EnsureEnvironment(_ context.Context, spec runner.RunSpec) (runner.Environment, error) {
|
|
844
1042
|
r.mu.Lock()
|
|
@@ -852,16 +1050,12 @@ func (r *scenarioRunner) EnsureEnvironment(_ context.Context, spec runner.RunSpe
|
|
|
852
1050
|
return env, nil
|
|
853
1051
|
}
|
|
854
1052
|
|
|
855
|
-
func (r *scenarioRunner)
|
|
856
|
-
r.
|
|
857
|
-
|
|
858
|
-
t, ok := r.terminals[title]
|
|
859
|
-
if !ok || !t.live {
|
|
860
|
-
return runner.Terminal{}, false, nil
|
|
861
|
-
}
|
|
862
|
-
return t.terminal, true, nil
|
|
1053
|
+
func (r *scenarioRunner) SetEnvironmentStatus(_ context.Context, _ runner.Environment, status string) error {
|
|
1054
|
+
r.log.add("workspace-status:" + status)
|
|
1055
|
+
return nil
|
|
863
1056
|
}
|
|
864
|
-
|
|
1057
|
+
|
|
1058
|
+
func (r *scenarioRunner) FindTerminal(_ context.Context, terminal runner.Terminal) (runner.Terminal, bool, error) {
|
|
865
1059
|
r.mu.Lock()
|
|
866
1060
|
defer r.mu.Unlock()
|
|
867
1061
|
for _, current := range r.terminals {
|
|
@@ -872,8 +1066,15 @@ func (r *scenarioRunner) InspectTerminal(_ context.Context, terminal runner.Term
|
|
|
872
1066
|
return runner.Terminal{}, false, nil
|
|
873
1067
|
}
|
|
874
1068
|
func (r *scenarioRunner) SendTerminal(context.Context, runner.Terminal, string) error { return nil }
|
|
875
|
-
func (r *scenarioRunner) CreateTerminal(
|
|
876
|
-
|
|
1069
|
+
func (r *scenarioRunner) CreateTerminal(_ context.Context, _ runner.Environment, title string, command runner.Command) (runner.Terminal, error) {
|
|
1070
|
+
r.mu.Lock()
|
|
1071
|
+
defer r.mu.Unlock()
|
|
1072
|
+
terminal := runner.Terminal{ID: "terminal-" + title, Title: title}
|
|
1073
|
+
r.terminals[title] = &scenarioTerminal{terminal: terminal, live: true}
|
|
1074
|
+
r.commands[title] = append(r.commands[title], command)
|
|
1075
|
+
r.launches[title]++
|
|
1076
|
+
r.log.add("terminal-created:" + title)
|
|
1077
|
+
return terminal, nil
|
|
877
1078
|
}
|
|
878
1079
|
|
|
879
1080
|
func (r *scenarioRunner) CloseTerminal(_ context.Context, terminal runner.Terminal) error {
|
|
@@ -886,18 +1087,13 @@ func (r *scenarioRunner) CloseTerminal(_ context.Context, terminal runner.Termin
|
|
|
886
1087
|
return nil
|
|
887
1088
|
}
|
|
888
1089
|
|
|
889
|
-
func (r *scenarioRunner) EnsureTerminal(
|
|
890
|
-
r.
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
return
|
|
1090
|
+
func (r *scenarioRunner) EnsureTerminal(ctx context.Context, env runner.Environment, stored runner.Terminal, title string, command runner.Command) (runner.Terminal, error) {
|
|
1091
|
+
if terminal, ok, err := r.FindTerminal(ctx, stored); err != nil {
|
|
1092
|
+
return runner.Terminal{}, err
|
|
1093
|
+
} else if ok {
|
|
1094
|
+
return terminal, nil
|
|
894
1095
|
}
|
|
895
|
-
|
|
896
|
-
r.terminals[title] = &scenarioTerminal{terminal: terminal, live: true}
|
|
897
|
-
r.commands[title] = append(r.commands[title], command)
|
|
898
|
-
r.launches[title]++
|
|
899
|
-
r.log.add("terminal-created:" + title)
|
|
900
|
-
return terminal, nil
|
|
1096
|
+
return r.CreateTerminal(ctx, env, title, command)
|
|
901
1097
|
}
|
|
902
1098
|
|
|
903
1099
|
func (r *scenarioRunner) CloseTerminals(context.Context, runner.RunSpec) error {
|
|
@@ -948,11 +1144,18 @@ type scenarioHarness struct {
|
|
|
948
1144
|
launches map[string][]harness.LaunchSpec
|
|
949
1145
|
}
|
|
950
1146
|
|
|
1147
|
+
func (*scenarioHarness) SetupRepo(context.Context, string) error { return nil }
|
|
1148
|
+
|
|
1149
|
+
var _ harness.Harness = (*scenarioHarness)(nil)
|
|
1150
|
+
|
|
951
1151
|
func newScenarioHarness(log *scenarioLog) *scenarioHarness {
|
|
952
1152
|
return &scenarioHarness{log: log, sessions: map[string]harness.Session{}, launches: map[string][]harness.LaunchSpec{}}
|
|
953
1153
|
}
|
|
954
1154
|
|
|
955
|
-
func (h *scenarioHarness) ValidateAgent(context.Context,
|
|
1155
|
+
func (h *scenarioHarness) ValidateAgent(_ context.Context, _, agent string) error {
|
|
1156
|
+
h.log.add("harness-agent-validated:" + agent)
|
|
1157
|
+
return nil
|
|
1158
|
+
}
|
|
956
1159
|
|
|
957
1160
|
func (h *scenarioHarness) FindSession(_ context.Context, _ string, title string) (harness.Session, bool, error) {
|
|
958
1161
|
h.mu.Lock()
|
|
@@ -961,6 +1164,17 @@ func (h *scenarioHarness) FindSession(_ context.Context, _ string, title string)
|
|
|
961
1164
|
return session, ok, nil
|
|
962
1165
|
}
|
|
963
1166
|
|
|
1167
|
+
func (h *scenarioHarness) RenderPrompt(kind harness.PromptKind, data harness.PromptData, nudge string) (string, error) {
|
|
1168
|
+
prompt := string(kind) + " taskSystem=" + data.TaskSystem + " mailbox=" + data.Mailbox
|
|
1169
|
+
if data.NodeType == workflow.NodeHITL {
|
|
1170
|
+
prompt += " hitl"
|
|
1171
|
+
}
|
|
1172
|
+
if nudge != "" {
|
|
1173
|
+
prompt += " " + nudge
|
|
1174
|
+
}
|
|
1175
|
+
return prompt, nil
|
|
1176
|
+
}
|
|
1177
|
+
|
|
964
1178
|
func (h *scenarioHarness) BuildCommand(spec harness.LaunchSpec) (runner.Command, error) {
|
|
965
1179
|
h.mu.Lock()
|
|
966
1180
|
h.launches[spec.Node] = append(h.launches[spec.Node], spec)
|
|
@@ -969,6 +1183,7 @@ func (h *scenarioHarness) BuildCommand(spec harness.LaunchSpec) (runner.Command,
|
|
|
969
1183
|
h.log.add("harness-launched:" + spec.Title)
|
|
970
1184
|
return runner.Command{
|
|
971
1185
|
Executable: "fake-harness",
|
|
1186
|
+
Args: []string{"opaque", spec.Prompt},
|
|
972
1187
|
Env: map[string]string{
|
|
973
1188
|
"RELAY_FLOW_RUN_ID": string(spec.RunID),
|
|
974
1189
|
"RELAY_FLOW_WORKFLOW": spec.Workflow,
|
|
@@ -993,12 +1208,8 @@ func (h *scenarioHarness) launch(node string) harness.LaunchSpec {
|
|
|
993
1208
|
}
|
|
994
1209
|
|
|
995
1210
|
func routesJSON(routes []workflow.Route) string {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
parts = append(parts, route.Target)
|
|
999
|
-
}
|
|
1000
|
-
sort.Strings(parts)
|
|
1001
|
-
return "[\"" + strings.Join(parts, "\",\"") + "\"]"
|
|
1211
|
+
b, _ := json.Marshal(routes)
|
|
1212
|
+
return string(b)
|
|
1002
1213
|
}
|
|
1003
1214
|
|
|
1004
1215
|
type scenarioRepoLookup struct{ reg *repo.Registry }
|
package/cmd/relay-flow/serve.go
CHANGED
|
@@ -35,6 +35,7 @@ import (
|
|
|
35
35
|
// Adapter registrations (factories are registered by name via init).
|
|
36
36
|
_ "github.com/rajpopat27/relay-flow/internal/harness/opencode"
|
|
37
37
|
_ "github.com/rajpopat27/relay-flow/internal/runner/orca"
|
|
38
|
+
_ "github.com/rajpopat27/relay-flow/internal/task/beads"
|
|
38
39
|
_ "github.com/rajpopat27/relay-flow/internal/task/jira"
|
|
39
40
|
)
|
|
40
41
|
|
|
@@ -204,6 +205,7 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
204
205
|
Repos: repoReg,
|
|
205
206
|
Runner: rnr,
|
|
206
207
|
Harness: hrn,
|
|
208
|
+
TaskSystem: cfg.TaskPlugin,
|
|
207
209
|
RetentionDays: cfg.CompletedRunRetentionDays,
|
|
208
210
|
Runtime: &runsvc.RuntimePolicy{
|
|
209
211
|
KeepTerminalsAlive: cfg.KeepTerminalsAlive,
|
|
@@ -262,6 +264,7 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
262
264
|
ConfigPath: p.Config,
|
|
263
265
|
TaskPlugin: cfg.TaskPlugin,
|
|
264
266
|
Runner: rnr,
|
|
267
|
+
Harness: hrn,
|
|
265
268
|
Active: engine,
|
|
266
269
|
Workflows: wfSvc.Registry(),
|
|
267
270
|
}, repoReg)
|
|
@@ -272,8 +275,8 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
272
275
|
// specs come from the engine so the recover path builds the same
|
|
273
276
|
// description content as normal run execution.
|
|
274
277
|
if recover {
|
|
275
|
-
specsFor := func(wf *workflow.Workflow
|
|
276
|
-
return goworkflows.
|
|
278
|
+
specsFor := func(sys task.System, work runsvc.Work, wf *workflow.Workflow) ([]task.MailboxSpec, error) {
|
|
279
|
+
return goworkflows.RenderMailboxSpecs(sys, work, wf)
|
|
277
280
|
}
|
|
278
281
|
if err := recoverpkg.FromTaskSystem(ctx, repoReg, rnr, runManager, specsFor); err != nil {
|
|
279
282
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Beads workflow example. Register the code repository with a required
|
|
2
|
+
# repo-scoped taskConfig.beadsDir before submitting this workflow, for example:
|
|
3
|
+
#
|
|
4
|
+
# relay-flow repo register --name payments --path /work/payments \
|
|
5
|
+
# --set beadsDir=/work/payments/.beads
|
|
6
|
+
#
|
|
7
|
+
# The Beads prefix is optional and is not a workspace/component selector.
|
|
8
|
+
name: beadsStoryFlow
|
|
9
|
+
repos:
|
|
10
|
+
- payments
|
|
11
|
+
cleanupRunnerOnEnd: true
|
|
12
|
+
|
|
13
|
+
taskConfig:
|
|
14
|
+
filters:
|
|
15
|
+
# Beads statuses are open, in_progress, blocked, deferred, hooked, closed.
|
|
16
|
+
parentStatuses:
|
|
17
|
+
- open
|
|
18
|
+
issueTypes:
|
|
19
|
+
- epic
|
|
20
|
+
labels:
|
|
21
|
+
- relay-ready
|
|
22
|
+
# assignees:
|
|
23
|
+
# - owner@example.com
|
|
24
|
+
# Optional shared default; filters.assignees overrides it when provided.
|
|
25
|
+
# An assignee in effect for a node also assigns that node's mailbox.
|
|
26
|
+
# assignee: owner@example.com
|
|
27
|
+
|
|
28
|
+
# transitionTo describes one lifecycle point, so configure it on a node.
|
|
29
|
+
# Omitting it entirely already gives the Jira-equivalent lifecycle:
|
|
30
|
+
# start parent -> in_progress, work mailbox -> in_progress, end parent -> closed.
|
|
31
|
+
# Values are Beads-native, not Jira values such as "In Progress" or "Done".
|
|
32
|
+
nodes:
|
|
33
|
+
start:
|
|
34
|
+
taskConfig:
|
|
35
|
+
transitionTo:
|
|
36
|
+
parentStatus: in_progress
|
|
37
|
+
onSuccess:
|
|
38
|
+
- target: implement
|
|
39
|
+
when: The Beads parent is ready for implementation
|
|
40
|
+
|
|
41
|
+
implement:
|
|
42
|
+
type: agent
|
|
43
|
+
agent: build
|
|
44
|
+
description: Implement the Beads parent and verify the changes.
|
|
45
|
+
taskConfig:
|
|
46
|
+
# assignee: developer@example.com
|
|
47
|
+
transitionTo:
|
|
48
|
+
taskStatus: in_progress
|
|
49
|
+
onSuccess:
|
|
50
|
+
- target: review
|
|
51
|
+
when: Implementation and verification are complete
|
|
52
|
+
onFailure:
|
|
53
|
+
- target: implement
|
|
54
|
+
when: Implementation needs another pass
|
|
55
|
+
|
|
56
|
+
review:
|
|
57
|
+
type: hitl
|
|
58
|
+
agent: plan
|
|
59
|
+
description: Review the implementation and obtain approval.
|
|
60
|
+
taskConfig:
|
|
61
|
+
# assignee: reviewer@example.com
|
|
62
|
+
transitionTo:
|
|
63
|
+
taskStatus: in_progress
|
|
64
|
+
onSuccess:
|
|
65
|
+
- target: end
|
|
66
|
+
when: The review is approved
|
|
67
|
+
onFailure:
|
|
68
|
+
- target: implement
|
|
69
|
+
when: Changes are requested
|
|
70
|
+
|
|
71
|
+
end:
|
|
72
|
+
taskConfig:
|
|
73
|
+
transitionTo:
|
|
74
|
+
parentStatus: closed
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copy this file and replace "payments" with one or more registered repo names.
|
|
2
|
+
# Optional fields are shown commented; uncomment only the behavior you need.
|
|
3
|
+
name: defaultStoryFlow
|
|
4
|
+
repos:
|
|
5
|
+
- test-relay-flow
|
|
6
|
+
cleanupRunnerOnEnd: true
|
|
7
|
+
|
|
8
|
+
taskConfig:
|
|
9
|
+
filters:
|
|
10
|
+
parentStatuses:
|
|
11
|
+
- To Do
|
|
12
|
+
issueTypes:
|
|
13
|
+
- Story
|
|
14
|
+
# labels:
|
|
15
|
+
# - coding
|
|
16
|
+
# assignees:
|
|
17
|
+
# - owner@example.com
|
|
18
|
+
# assignee: default-node-owner@example.com
|
|
19
|
+
# project: PAY
|
|
20
|
+
# component: api
|
|
21
|
+
# transitionTo:
|
|
22
|
+
# parentStatus: In Progress
|
|
23
|
+
# taskStatus: In Progress
|
|
24
|
+
|
|
25
|
+
nodes:
|
|
26
|
+
start:
|
|
27
|
+
taskConfig:
|
|
28
|
+
transitionTo:
|
|
29
|
+
parentStatus: In Progress
|
|
30
|
+
onSuccess:
|
|
31
|
+
- target: implement
|
|
32
|
+
when: The parent story is ready for implementation
|
|
33
|
+
|
|
34
|
+
implement:
|
|
35
|
+
type: agent
|
|
36
|
+
agent: build
|
|
37
|
+
description: Implement the parent story and verify the changes.
|
|
38
|
+
# nudgePrompt: "Finish {{node}} for {{ticket}} and choose one of: {{nextSteps}}"
|
|
39
|
+
taskConfig:
|
|
40
|
+
# assignee: developer@example.com
|
|
41
|
+
transitionTo:
|
|
42
|
+
taskStatus: In Progress
|
|
43
|
+
# parentStatus: In Progress
|
|
44
|
+
onSuccess:
|
|
45
|
+
- target: review
|
|
46
|
+
when: Implementation and verification are complete
|
|
47
|
+
onFailure:
|
|
48
|
+
- target: implement
|
|
49
|
+
when: Implementation needs another pass
|
|
50
|
+
|
|
51
|
+
review:
|
|
52
|
+
type: agent
|
|
53
|
+
agent: plan
|
|
54
|
+
description: Review the implementation for correctness, regressions, and missing tests.
|
|
55
|
+
# nudgePrompt: "Complete the review for {{ticket}}. Valid routes: {{nextSteps}}"
|
|
56
|
+
taskConfig:
|
|
57
|
+
# assignee: reviewer@example.com
|
|
58
|
+
transitionTo:
|
|
59
|
+
taskStatus: In Progress
|
|
60
|
+
# parentStatus: In Review
|
|
61
|
+
onSuccess:
|
|
62
|
+
- target: prReview
|
|
63
|
+
when: The changes are ready for human approval
|
|
64
|
+
onFailure:
|
|
65
|
+
- target: implement
|
|
66
|
+
when: Code changes are required
|
|
67
|
+
|
|
68
|
+
prReview:
|
|
69
|
+
type: hitl
|
|
70
|
+
agent: plan
|
|
71
|
+
description: Discuss the pull request with the human and obtain approval.
|
|
72
|
+
# nudgePrompt: "Review {{ticket}} with the human. Valid routes: {{nextSteps}}"
|
|
73
|
+
taskConfig:
|
|
74
|
+
# assignee: human-reviewer@example.com
|
|
75
|
+
transitionTo:
|
|
76
|
+
taskStatus: In Progress
|
|
77
|
+
# parentStatus: In Review
|
|
78
|
+
onSuccess:
|
|
79
|
+
- target: end
|
|
80
|
+
when: The human approves the review
|
|
81
|
+
onFailure:
|
|
82
|
+
- target: implement
|
|
83
|
+
when: The human requests code changes
|
|
84
|
+
|
|
85
|
+
end:
|
|
86
|
+
taskConfig:
|
|
87
|
+
transitionTo:
|
|
88
|
+
parentStatus: Done
|
package/go.mod
CHANGED
|
@@ -2,10 +2,11 @@ module github.com/rajpopat27/relay-flow
|
|
|
2
2
|
|
|
3
3
|
go 1.24.6
|
|
4
4
|
|
|
5
|
-
require (
|
|
5
|
+
require (
|
|
6
6
|
github.com/cschleiden/go-workflows v1.4.2
|
|
7
7
|
github.com/google/renameio/v2 v2.0.1
|
|
8
8
|
github.com/google/uuid v1.6.0
|
|
9
|
+
github.com/yuin/goldmark v1.7.13
|
|
9
10
|
gopkg.in/yaml.v3 v3.0.1
|
|
10
11
|
)
|
|
11
12
|
|
package/go.sum
CHANGED
|
@@ -122,6 +122,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|
|
122
122
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
123
123
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
124
124
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
125
|
+
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
|
126
|
+
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
|
125
127
|
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
|
126
128
|
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
|
127
129
|
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
|
@@ -26,9 +26,9 @@ func Merge(values ...RawValues) RawValues {
|
|
|
26
26
|
|
|
27
27
|
func mergeInto(dst, src map[string]any) map[string]any {
|
|
28
28
|
for k, sv := range src {
|
|
29
|
-
if sm, ok := sv
|
|
29
|
+
if sm, ok := rawMap(sv); ok {
|
|
30
30
|
fresh := map[string]any{}
|
|
31
|
-
if dm, ok := dst[k]
|
|
31
|
+
if dm, ok := rawMap(dst[k]); ok {
|
|
32
32
|
mergeInto(fresh, dm)
|
|
33
33
|
}
|
|
34
34
|
dst[k] = mergeInto(fresh, sm)
|
|
@@ -39,6 +39,17 @@ func mergeInto(dst, src map[string]any) map[string]any {
|
|
|
39
39
|
return dst
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
func rawMap(value any) (map[string]any, bool) {
|
|
43
|
+
switch values := value.(type) {
|
|
44
|
+
case map[string]any:
|
|
45
|
+
return values, true
|
|
46
|
+
case RawValues:
|
|
47
|
+
return map[string]any(values), true
|
|
48
|
+
default:
|
|
49
|
+
return nil, false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
// DecodeStrict decodes raw values into dst, rejecting unknown fields and
|
|
43
54
|
// explicit null values.
|
|
44
55
|
func DecodeStrict(values RawValues, dst any) error {
|