relay-flow 0.2.4-alpha → 0.2.6-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 (60) hide show
  1. package/README.md +29 -18
  2. package/cmd/relay-flow/backend_selection_test.go +149 -0
  3. package/cmd/relay-flow/beads_composition_test.go +3 -3
  4. package/cmd/relay-flow/main.go +94 -13
  5. package/cmd/relay-flow/scenario_test.go +19 -2
  6. package/cmd/relay-flow/serve.go +98 -19
  7. package/cmd/relay-flow/serve_recovery_test.go +100 -0
  8. package/cmd/relay-flow/temporal_init.go +170 -0
  9. package/cmd/relay-flow/temporal_init_test.go +217 -0
  10. package/cmd/relay-flow/temporal_report_test.go +733 -0
  11. package/examples/beads-workflow.yaml +3 -0
  12. package/examples/config-reference.yaml +7 -3
  13. package/examples/minimal-beads-task-workflow.yaml +2 -1
  14. package/examples/workflow-reference.yaml +2 -1
  15. package/go.mod +37 -16
  16. package/go.sum +129 -61
  17. package/internal/config/machine.go +33 -1
  18. package/internal/config/machine_test.go +76 -0
  19. package/internal/execution/goworkflows/activities.go +19 -0
  20. package/internal/execution/goworkflows/engine.go +13 -38
  21. package/internal/execution/goworkflows/engine_test.go +1 -1
  22. package/internal/execution/goworkflows/node_runtime_test.go +113 -4
  23. package/internal/execution/goworkflows/projection.go +47 -464
  24. package/internal/execution/projection/projection.go +867 -0
  25. package/internal/execution/projection/projection_test.go +347 -0
  26. package/internal/execution/temporal/activities.go +586 -0
  27. package/internal/execution/temporal/engine.go +384 -0
  28. package/internal/execution/temporal/engine_test.go +277 -0
  29. package/internal/execution/temporal/interpreter.go +736 -0
  30. package/internal/execution/temporal/operations.go +455 -0
  31. package/internal/execution/temporal/operations_test.go +101 -0
  32. package/internal/execution/temporal/recovery.go +194 -0
  33. package/internal/execution/temporal/recovery_runtime.go +41 -0
  34. package/internal/execution/temporal/recovery_test.go +102 -0
  35. package/internal/execution/temporal/snapshot_restart_test.go +72 -0
  36. package/internal/execution/temporal/spike_test.go +934 -0
  37. package/internal/execution/temporal/visibility_lag_test.go +415 -0
  38. package/internal/harness/harness.go +5 -0
  39. package/internal/harness/opencode/opencode.go +14 -3
  40. package/internal/harness/opencode/opencode_test.go +1 -1
  41. package/internal/harness/opencode/repo_setup.go +1 -1
  42. package/internal/harness/opencode/task_env_test.go +57 -0
  43. package/internal/harness/pi/pi.go +58 -47
  44. package/internal/harness/pi/pi_test.go +26 -10
  45. package/internal/harness/pi/prompt_test.go +30 -1
  46. package/internal/harness/pi/task_env_test.go +51 -0
  47. package/internal/harness/pi/validation_test.go +27 -51
  48. package/internal/repo/service.go +18 -8
  49. package/internal/runner/herdr/herdr.go +14 -0
  50. package/internal/runner/herdr/herdr_test.go +20 -0
  51. package/internal/runner/orca/orca.go +33 -0
  52. package/internal/runner/orca/orca_test.go +33 -4
  53. package/internal/runner/runner.go +8 -0
  54. package/internal/task/beads/agent_env_test.go +52 -0
  55. package/internal/task/beads/beads.go +87 -7
  56. package/internal/task/beads/beads_test.go +78 -9
  57. package/internal/task/beads/repo_composition_test.go +47 -3
  58. package/internal/task/factory.go +31 -2
  59. package/internal/task/task.go +10 -0
  60. package/package.json +1 -1
@@ -44,6 +44,21 @@ func (a *Activities) runSpec(w run.Work) runner.RunSpec {
44
44
  }
45
45
  }
46
46
 
47
+ // agentEnv returns the repo task system's agent workspace environment, or nil
48
+ // when the adapter exposes none. It is resolved at launch time so no workspace
49
+ // value is carried in durable workflow history.
50
+ func (a *Activities) agentEnv(repoName string) (map[string]string, error) {
51
+ sys, err := a.taskSystem(repoName)
52
+ if err != nil {
53
+ return nil, err
54
+ }
55
+ provider, ok := sys.(task.AgentEnvironment)
56
+ if !ok {
57
+ return nil, nil
58
+ }
59
+ return provider.AgentEnv(), nil
60
+ }
61
+
47
62
  // EnsureMailboxes ensures one mailbox per work node and returns the
48
63
  // node-to-mailbox map.
49
64
  func (a *Activities) EnsureMailboxes(ctx context.Context, w run.Work, specs []task.MailboxSpec) (map[string]task.Mailbox, error) {
@@ -220,6 +235,10 @@ func (a *Activities) EnsureNodeRuntime(ctx context.Context, nw run.NodeWork, rep
220
235
  if err != nil {
221
236
  return err
222
237
  }
238
+ spec.TaskEnv, err = a.agentEnv(nw.Repo)
239
+ if err != nil {
240
+ return err
241
+ }
223
242
  cmd, err := a.Harness.BuildCommand(spec)
224
243
  if err != nil {
225
244
  return err
@@ -23,6 +23,7 @@ import (
23
23
  goworkflow "github.com/cschleiden/go-workflows/workflow"
24
24
  "github.com/google/uuid"
25
25
 
26
+ "github.com/rajpopat27/relay-flow/internal/execution/projection"
26
27
  "github.com/rajpopat27/relay-flow/internal/harness"
27
28
  "github.com/rajpopat27/relay-flow/internal/identity"
28
29
  "github.com/rajpopat27/relay-flow/internal/repo"
@@ -68,46 +69,13 @@ type Engine struct {
68
69
  workerName string
69
70
  }
70
71
 
71
- // InitDatabase creates the SQLite database at path (mode 0600) with the
72
- // relay_runs projection schema and closes it. Used by `relay-flow init`;
73
- // serve uses New to open the full engine.
74
- func InitDatabase(path string) error {
75
- db, err := sql.Open("sqlite", fmt.Sprintf("file:%s?_txlock=immediate", path))
76
- if err != nil {
77
- return fmt.Errorf("open %s: %w", path, err)
78
- }
79
- defer db.Close()
80
- if _, err := db.Exec(`PRAGMA schema_version`); err != nil {
81
- return fmt.Errorf("open %s: %w", path, err)
82
- }
83
- if _, err := db.Exec("PRAGMA journal_mode=WAL;"); err != nil {
84
- return fmt.Errorf("open %s: %w", path, err)
85
- }
86
- if err := os.Chmod(path, 0o600); err != nil && !os.IsNotExist(err) {
87
- return fmt.Errorf("chmod %s: %w", path, err)
88
- }
89
- proj := &RunProjection{DB: db}
90
- if err := proj.migrate(); err != nil {
91
- return fmt.Errorf("migrate relay_runs: %w", err)
92
- }
93
- return nil
94
- }
72
+ // InitDatabase preserves the embedded-engine public helper while delegating
73
+ // relay-owned schema lifecycle to the shared projection package.
74
+ func InitDatabase(path string) error { return projection.InitDatabase(path) }
95
75
 
96
- // HasNonterminalRuns inspects an existing database without migrating or
97
- // otherwise modifying it. It is used by init --force before config changes.
76
+ // HasNonterminalRuns inspects the shared projection without migrating it.
98
77
  func HasNonterminalRuns(path string) (bool, error) {
99
- db, err := sql.Open("sqlite", fmt.Sprintf("file:%s?mode=ro", path))
100
- if err != nil {
101
- return false, fmt.Errorf("open %s: %w", path, err)
102
- }
103
- defer db.Close()
104
- var active bool
105
- if err := db.QueryRow(`SELECT EXISTS(
106
- SELECT 1 FROM relay_runs WHERE state NOT IN ('completed', 'canceled')
107
- )`).Scan(&active); err != nil {
108
- return false, fmt.Errorf("inspect %s: %w", path, err)
109
- }
110
- return active, nil
78
+ return projection.HasNonterminalRuns(path)
111
79
  }
112
80
 
113
81
  // New opens the SQLite database at path (created with mode 0600 when
@@ -148,6 +116,13 @@ func New(path string, deps Dependencies) (*Engine, error) {
148
116
  db.Close()
149
117
  return nil, fmt.Errorf("migrate relay_runs: %w", err)
150
118
  }
119
+ // A marker-less legacy database is adopted only by the embedded executor;
120
+ // a persisted Temporal identity fails closed rather than being combined
121
+ // with go-workflows state.
122
+ if err := (&projection.RunProjection{DB: db}).VerifyIdentity(context.Background(), projection.ExecutorIdentity{ExecutorPlugin: "goworkflows"}); err != nil {
123
+ db.Close()
124
+ return nil, fmt.Errorf("verify executor identity: %w", err)
125
+ }
151
126
  activities := &Activities{
152
127
  Repos: deps.Repos,
153
128
  Runner: deps.Runner,
@@ -71,7 +71,7 @@ func TestMailboxDescriptionAndLaunchPromptAreTaskSystemNeutral(t *testing.T) {
71
71
  if err != nil {
72
72
  t.Fatal(err)
73
73
  }
74
- want := "Task system: " + taskSystem + "\nUse the " + taskSystem + " tools to read the parent ticket PAY-101.\n\nYour mailbox is PAY-234. Read its description and comments for node instructions and feedback.\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."
74
+ want := "Task system: " + taskSystem + "\nUse the " + taskSystem + " tools to read the parent ticket PAY-101.\n\nYour mailbox is PAY-234. Read its description and comments for node instructions and feedback.\n\nKeep the summary brief, and make the feedback as detailed and actionable as possible for the next agent.\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."
75
75
  if prompt != want {
76
76
  t.Fatalf("RenderPrompt(%s) = %q, want %q", taskSystem, prompt, want)
77
77
  }
@@ -9,6 +9,7 @@ import (
9
9
  "testing"
10
10
  "time"
11
11
 
12
+ "github.com/rajpopat27/relay-flow/internal/config"
12
13
  "github.com/rajpopat27/relay-flow/internal/harness"
13
14
  "github.com/rajpopat27/relay-flow/internal/repo"
14
15
  "github.com/rajpopat27/relay-flow/internal/run"
@@ -210,7 +211,7 @@ func TestEnsureNodeRuntimeUsesDirectIDsAndFallsBackFresh(t *testing.T) {
210
211
  }); err != nil {
211
212
  t.Fatal(err)
212
213
  }
213
- a := &Activities{Runner: fr, Harness: fh, Runs: p}
214
+ a := &Activities{Repos: runtimeTestRepos(&runtimeTestTaskSystem{}), Runner: fr, Harness: fh, Runs: p}
214
215
  nw := run.NodeWork{Work: run.Work{RunID: id, Repo: "payments", Workflow: "basic", Parent: task.TicketRef{Key: "PAY-101"}}, Node: "implement", NodeVisitID: "visit-2"}
215
216
  spec := harness.LaunchSpec{RunID: id, NodeVisitID: "visit-2", RepoName: "payments", Workflow: "basic", Ticket: "PAY-101", Node: "implement", Agent: "build", Title: "PAY-101:implement", Prompt: "work", NudgePrompt: "custom instructions"}
216
217
  if err := a.EnsureNodeRuntime(ctx, nw, "/srv/payments", spec, NodeRuntime{NodeVisitID: "visit-2"}); err != nil {
@@ -259,8 +260,8 @@ func TestEnsureNodeRuntimeInitialLaunchAppendsCustomInstructions(t *testing.T) {
259
260
  }
260
261
  fh := &runtimeTestHarness{}
261
262
  fr := &runtimeTestRunner{}
262
- a := &Activities{Runner: fr, Harness: fh, Runs: p}
263
- nw := run.NodeWork{Work: run.Work{RunID: id}, Node: "implement", NodeVisitID: "visit-first"}
263
+ a := &Activities{Repos: runtimeTestRepos(&runtimeTestTaskSystem{}), Runner: fr, Harness: fh, Runs: p}
264
+ nw := run.NodeWork{Work: run.Work{RunID: id, Repo: "payments"}, Node: "implement", NodeVisitID: "visit-first"}
264
265
  spec := harness.LaunchSpec{RunID: id, NodeVisitID: "visit-first", Node: "implement", NodeType: workflow.NodeHITL, Agent: "build", Prompt: "standard prompt", NudgePrompt: "custom instructions"}
265
266
  if err := a.EnsureNodeRuntime(ctx, nw, "", spec, NodeRuntime{}); err != nil {
266
267
  t.Fatal(err)
@@ -273,6 +274,63 @@ func TestEnsureNodeRuntimeInitialLaunchAppendsCustomInstructions(t *testing.T) {
273
274
  }
274
275
  }
275
276
 
277
+ // TestEnsureNodeRuntimeSuppliesTaskSystemAgentEnv asserts the repo's task
278
+ // system supplies the agent workspace environment at launch time, so the
279
+ // harness/runner command addresses the same workspace as relay-flow. Adapters
280
+ // without the optional capability supply nothing.
281
+ func TestEnsureNodeRuntimeSuppliesTaskSystemAgentEnv(t *testing.T) {
282
+ ctx := context.Background()
283
+ db := openProjectionDB(t, filepath.Join(t.TempDir(), "state.db"))
284
+ defer db.Close()
285
+ p := &RunProjection{DB: db}
286
+ if err := p.migrate(); err != nil {
287
+ t.Fatal(err)
288
+ }
289
+ id := run.ID("payments/basic/PAY-105")
290
+ if err := p.insertStart(ctx, run.Start{ID: id, Repo: "payments", Workflow: workflow.Workflow{Name: "basic"}, Ticket: task.TicketRef{ID: "5", Key: "PAY-105"}}, time.Now().UTC()); err != nil {
291
+ t.Fatal(err)
292
+ }
293
+ if err := p.updateNodeRuntimeVisit(ctx, id, "implement", "visit-first"); err != nil {
294
+ t.Fatal(err)
295
+ }
296
+ want := map[string]string{"BEADS_DIR": "/var/lib/beads/payments/.beads", "BEADS_DB": "", "BD_DB": ""}
297
+ fh := &runtimeTestHarness{}
298
+ a := &Activities{
299
+ Repos: runtimeTestRepos(&runtimeTestTaskSystem{agentEnv: want}),
300
+ Runner: &runtimeTestRunner{},
301
+ Harness: fh,
302
+ Runs: p,
303
+ }
304
+ nw := run.NodeWork{Work: run.Work{RunID: id, Repo: "payments", Workflow: "basic", Parent: task.TicketRef{Key: "PAY-105"}}, Node: "implement", NodeVisitID: "visit-first"}
305
+ spec := harness.LaunchSpec{RunID: id, NodeVisitID: "visit-first", RepoName: "payments", Node: "implement", Agent: "build", Title: "PAY-105:implement"}
306
+ if err := a.EnsureNodeRuntime(ctx, nw, "/srv/payments", spec, NodeRuntime{}); err != nil {
307
+ t.Fatal(err)
308
+ }
309
+ if len(fh.taskEnvs) != 1 || !reflect.DeepEqual(fh.taskEnvs[0], want) {
310
+ t.Fatalf("launch TaskEnv = %#v, want [%#v]", fh.taskEnvs, want)
311
+ }
312
+
313
+ // An adapter without the optional capability supplies no environment.
314
+ plain := &runtimeTestHarness{}
315
+ b := &Activities{
316
+ Repos: runtimeTestRepos(&runtimeTestPlainTaskSystem{System: &runtimeTestTaskSystem{}}),
317
+ Runner: &runtimeTestRunner{},
318
+ Harness: plain,
319
+ Runs: p,
320
+ }
321
+ if err := p.updateNodeRuntimeVisit(ctx, id, "review", "visit-review"); err != nil {
322
+ t.Fatal(err)
323
+ }
324
+ nw.Node, nw.NodeVisitID = "review", "visit-review"
325
+ spec.Node, spec.NodeVisitID, spec.Title = "review", "visit-review", "PAY-105:review"
326
+ if err := b.EnsureNodeRuntime(ctx, nw, "/srv/payments", spec, NodeRuntime{}); err != nil {
327
+ t.Fatal(err)
328
+ }
329
+ if len(plain.taskEnvs) != 1 || plain.taskEnvs[0] != nil {
330
+ t.Fatalf("TaskEnv without a configured workspace = %#v, want [nil]", plain.taskEnvs)
331
+ }
332
+ }
333
+
276
334
  func TestEnsureNodeRuntimeSendFailureClosesLiveTerminal(t *testing.T) {
277
335
  ctx := context.Background()
278
336
  db := openProjectionDB(t, filepath.Join(t.TempDir(), "state.db"))
@@ -293,7 +351,7 @@ func TestEnsureNodeRuntimeSendFailureClosesLiveTerminal(t *testing.T) {
293
351
  }
294
352
  fr := &runtimeTestRunner{live: true, sendErr: errors.New("send failed")}
295
353
  fh := &runtimeTestHarness{}
296
- a := &Activities{Runner: fr, Harness: fh, Runs: p}
354
+ a := &Activities{Repos: runtimeTestRepos(&runtimeTestTaskSystem{}), Runner: fr, Harness: fh, Runs: p}
297
355
  nw := run.NodeWork{Work: run.Work{RunID: id, Repo: "payments", Workflow: "basic", Parent: task.TicketRef{Key: "PAY-102"}}, Node: "implement", NodeVisitID: "visit-new", Mailbox: task.Mailbox{Key: "PAY-234", Node: "implement"}}
298
356
  spec := harness.LaunchSpec{RunID: id, NodeVisitID: "visit-new", Node: "implement", Agent: "build", Title: "PAY-102:implement", Prompt: "work", NudgePrompt: "Read the latest review feedback."}
299
357
  if err := a.EnsureNodeRuntime(ctx, nw, "/srv/payments", spec, NodeRuntime{RunID: id, Node: "implement", TerminalID: "live-old", SessionID: "session-old", NodeVisitID: "visit-old"}); err != nil {
@@ -499,6 +557,7 @@ type runtimeTestHarness struct {
499
557
  prompts []string
500
558
  resumeIDs []string
501
559
  rendered []harness.PromptKind
560
+ taskEnvs []map[string]string
502
561
  }
503
562
 
504
563
  func (h *runtimeTestHarness) RenderPrompt(kind harness.PromptKind, _ harness.PromptData, nudge string) (string, error) {
@@ -522,9 +581,59 @@ func (h *runtimeTestHarness) BuildCommand(spec harness.LaunchSpec) (runner.Comma
522
581
  h.buildCalls++
523
582
  h.prompts = append(h.prompts, spec.Prompt)
524
583
  h.resumeIDs = append(h.resumeIDs, spec.ResumeID)
584
+ h.taskEnvs = append(h.taskEnvs, spec.TaskEnv)
525
585
  return runner.Command{Executable: "opencode", Args: []string{spec.ResumeID}}, nil
526
586
  }
527
587
 
588
+ // runtimeTestTaskSystem is a repo-bound task system stub. Only the optional
589
+ // task.AgentEnvironment capability is exercised here; every other method is an
590
+ // unused stub for the node runtime tests.
591
+ type runtimeTestTaskSystem struct {
592
+ agentEnv map[string]string
593
+ }
594
+
595
+ func (s *runtimeTestTaskSystem) AgentEnv() map[string]string { return s.agentEnv }
596
+
597
+ func (*runtimeTestTaskSystem) Poll(context.Context) ([]task.Ticket, error) { return nil, nil }
598
+ func (*runtimeTestTaskSystem) CompileFilter(config.RawValues) (func(task.Ticket) bool, error) {
599
+ return func(task.Ticket) bool { return true }, nil
600
+ }
601
+ func (*runtimeTestTaskSystem) Claim(context.Context, task.TicketRef, string) error { return nil }
602
+ func (*runtimeTestTaskSystem) ValidateConfig(context.Context, config.RawValues, map[string]config.RawValues) error {
603
+ return nil
604
+ }
605
+ func (*runtimeTestTaskSystem) RenderText(task.TextKind, task.TextData) (string, error) {
606
+ return "", nil
607
+ }
608
+ func (*runtimeTestTaskSystem) EnsureMailboxes(context.Context, task.TicketRef, string, []task.MailboxSpec) (map[string]task.Mailbox, error) {
609
+ return map[string]task.Mailbox{}, nil
610
+ }
611
+ func (*runtimeTestTaskSystem) ApplyTaskConfig(context.Context, task.Target, config.RawValues) error {
612
+ return nil
613
+ }
614
+ func (*runtimeTestTaskSystem) CompleteMailbox(context.Context, task.Mailbox) error { return nil }
615
+ func (*runtimeTestTaskSystem) HasComment(context.Context, task.Target, string) (bool, error) {
616
+ return false, nil
617
+ }
618
+ func (*runtimeTestTaskSystem) Comment(context.Context, task.Target, string, string) error {
619
+ return nil
620
+ }
621
+ func (*runtimeTestTaskSystem) ResetForRecovery(context.Context, task.TicketRef, []task.Mailbox, config.RawValues) error {
622
+ return nil
623
+ }
624
+
625
+ // runtimeTestPlainTaskSystem forwards the task.System contract without
626
+ // exposing the optional AgentEnvironment capability.
627
+ type runtimeTestPlainTaskSystem struct {
628
+ task.System
629
+ }
630
+
631
+ func runtimeTestRepos(sys task.System) *repo.Registry {
632
+ reg := &repo.Registry{}
633
+ reg.Replace(&repo.Repo{Name: "payments", Path: "/srv/payments", TaskSystem: sys})
634
+ return reg
635
+ }
636
+
528
637
  func openProjectionDB(t *testing.T, path string) *sql.DB {
529
638
  t.Helper()
530
639
  db, err := sql.Open("sqlite", path)