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
@@ -0,0 +1,109 @@
1
+ package server
2
+
3
+ import (
4
+ "context"
5
+ "sort"
6
+
7
+ "github.com/rajpopat27/relay-flow/internal/run"
8
+ "github.com/rajpopat27/relay-flow/internal/workflow"
9
+ )
10
+
11
+ // WorkflowSummary is a read-only consumer DTO for compact workflow listing.
12
+ // The embedded definition keeps the existing JSON fields available while the
13
+ // additive fields provide the one-request execution summary used by the CLI.
14
+ type WorkflowSummary struct {
15
+ *workflow.Workflow
16
+ Repositories []string `json:"repositories,omitempty"`
17
+ NodeCount int `json:"nodeCount"`
18
+ ActiveRuns int `json:"activeRuns"`
19
+ LatestRun *run.Run `json:"latestRun,omitempty"`
20
+ }
21
+
22
+ // WorkflowDetail is a read-only query DTO. It deliberately contains domain
23
+ // values only; no executor, database, runner, or task-system implementation
24
+ // type crosses the Unix-socket boundary.
25
+ type WorkflowDetail struct {
26
+ *workflow.Workflow
27
+ Valid bool `json:"valid"`
28
+ ActiveRuns int `json:"activeRuns"`
29
+ RecentRuns []run.Run `json:"recentRuns"`
30
+ }
31
+
32
+ // WorkflowSummaryQueries is an optional server capability. Keeping it
33
+ // separate from Deps preserves the small existing service contract for test
34
+ // fakes and older composition roots.
35
+ type WorkflowSummaryQueries interface {
36
+ ListWorkflowSummaries(context.Context) ([]WorkflowSummary, error)
37
+ }
38
+
39
+ type WorkflowDetailQueries interface {
40
+ GetWorkflowDetail(context.Context, string) (WorkflowDetail, error)
41
+ }
42
+
43
+ type RunDetailQueries interface {
44
+ GetRunDetail(context.Context, string) (run.RunDetail, error)
45
+ }
46
+
47
+ // BuildWorkflowSummaries computes all summaries from one workflow list and one
48
+ // run list. The helper is used by the composition root and is intentionally
49
+ // independent of a durable executor implementation.
50
+ func BuildWorkflowSummaries(workflows []*workflow.Workflow, runs []run.Run) []WorkflowSummary {
51
+ latest := map[string]run.Run{}
52
+ active := map[string]int{}
53
+ for _, candidate := range runs {
54
+ if current, ok := latest[candidate.Workflow]; !ok || newerRun(candidate, current) {
55
+ latest[candidate.Workflow] = candidate
56
+ }
57
+ if candidate.State != run.StateCompleted && candidate.State != run.StateCanceled {
58
+ active[candidate.Workflow]++
59
+ }
60
+ }
61
+ out := make([]WorkflowSummary, 0, len(workflows))
62
+ for _, wf := range workflows {
63
+ if wf == nil {
64
+ continue
65
+ }
66
+ repos := append([]string(nil), wf.Repos...)
67
+ sort.Strings(repos)
68
+ var latestRun *run.Run
69
+ if candidate, ok := latest[wf.Name]; ok {
70
+ copy := candidate
71
+ latestRun = &copy
72
+ }
73
+ out = append(out, WorkflowSummary{
74
+ Workflow: wf, Repositories: repos, NodeCount: len(wf.Nodes),
75
+ ActiveRuns: active[wf.Name], LatestRun: latestRun,
76
+ })
77
+ }
78
+ sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
79
+ return out
80
+ }
81
+
82
+ func newerRun(candidate, current run.Run) bool {
83
+ if candidate.StartedAt.After(current.StartedAt) {
84
+ return true
85
+ }
86
+ return candidate.StartedAt.Equal(current.StartedAt) && candidate.UpdatedAt.After(current.UpdatedAt)
87
+ }
88
+
89
+ // BuildWorkflowDetail builds the static-definition plus recent execution
90
+ // summary response without changing workflow validation or execution.
91
+ func BuildWorkflowDetail(wf *workflow.Workflow, runs []run.Run) WorkflowDetail {
92
+ recent := append([]run.Run(nil), runs...)
93
+ sort.SliceStable(recent, func(i, j int) bool {
94
+ if recent[i].UpdatedAt.Equal(recent[j].UpdatedAt) {
95
+ return recent[i].StartedAt.After(recent[j].StartedAt)
96
+ }
97
+ return recent[i].UpdatedAt.After(recent[j].UpdatedAt)
98
+ })
99
+ if len(recent) > 5 {
100
+ recent = recent[:5]
101
+ }
102
+ active := 0
103
+ for _, candidate := range runs {
104
+ if candidate.State != run.StateCompleted && candidate.State != run.StateCanceled {
105
+ active++
106
+ }
107
+ }
108
+ return WorkflowDetail{Workflow: wf, Valid: wf != nil, ActiveRuns: active, RecentRuns: recent}
109
+ }
@@ -0,0 +1,60 @@
1
+ package server
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+ "time"
7
+
8
+ "github.com/rajpopat27/relay-flow/internal/run"
9
+ "github.com/rajpopat27/relay-flow/internal/task"
10
+ "github.com/rajpopat27/relay-flow/internal/workflow"
11
+ )
12
+
13
+ func TestWorkflowSummaryKeepsDefinitionFieldsOnJSONRoundTrip(t *testing.T) {
14
+ definition := &workflow.Workflow{Name: "basicFlow", Repos: []string{"payments"}, Nodes: map[string]workflow.Node{"start": {}, "end": {}}}
15
+ summary := WorkflowSummary{Workflow: definition, Repositories: []string{"payments"}, NodeCount: 2, ActiveRuns: 1}
16
+ raw, err := json.Marshal(summary)
17
+ if err != nil {
18
+ t.Fatal(err)
19
+ }
20
+ var decoded WorkflowSummary
21
+ if err := json.Unmarshal(raw, &decoded); err != nil {
22
+ t.Fatal(err)
23
+ }
24
+ if decoded.Workflow == nil || decoded.Name != definition.Name || decoded.NodeCount != 2 || decoded.ActiveRuns != 1 {
25
+ t.Fatalf("decoded summary = %#v", decoded)
26
+ }
27
+ arrayRaw, err := json.Marshal([]WorkflowSummary{summary})
28
+ if err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ var legacy []*workflow.Workflow
32
+ if err := json.Unmarshal(arrayRaw, &legacy); err != nil || len(legacy) != 1 || legacy[0].Name != definition.Name {
33
+ t.Fatalf("legacy workflow decode = %#v, err=%v", legacy, err)
34
+ }
35
+ baseRaw, err := json.Marshal([]*workflow.Workflow{definition})
36
+ if err != nil {
37
+ t.Fatal(err)
38
+ }
39
+ var fallback []WorkflowSummary
40
+ if err := json.Unmarshal(baseRaw, &fallback); err != nil || len(fallback) != 1 || fallback[0].Workflow == nil || fallback[0].Name != definition.Name {
41
+ t.Fatalf("fallback summary decode = %#v, err=%v", fallback, err)
42
+ }
43
+ }
44
+
45
+ func TestBuildWorkflowSummariesUsesLatestRunAndActiveCount(t *testing.T) {
46
+ start := time.Now().UTC().Add(-time.Hour)
47
+ workflows := []*workflow.Workflow{
48
+ {Name: "basicFlow", Repos: []string{"payments"}, Nodes: map[string]workflow.Node{
49
+ "start": {}, "end": {},
50
+ }},
51
+ }
52
+ runs := []run.Run{
53
+ {Workflow: "basicFlow", State: run.StateCompleted, StartedAt: start, UpdatedAt: start.Add(10 * time.Minute), Ticket: task.TicketRef{Key: "PAY-1"}},
54
+ {Workflow: "basicFlow", State: run.StateWaiting, StartedAt: start.Add(time.Minute), UpdatedAt: start.Add(20 * time.Minute), Ticket: task.TicketRef{Key: "PAY-2"}},
55
+ }
56
+ got := BuildWorkflowSummaries(workflows, runs)
57
+ if len(got) != 1 || got[0].ActiveRuns != 1 || got[0].LatestRun == nil || got[0].LatestRun.Ticket.Key != "PAY-2" {
58
+ t.Fatalf("summaries = %#v", got)
59
+ }
60
+ }
@@ -85,6 +85,7 @@ func New(deps Deps) http.Handler {
85
85
  mux.HandleFunc("/workflows", s.handleWorkflows)
86
86
  mux.HandleFunc("/workflows/", s.handleWorkflowByName)
87
87
  mux.HandleFunc("/repos/discover", s.handleReposDiscover)
88
+ mux.HandleFunc("/repos/ensure", s.handleRepoEnsure)
88
89
  mux.HandleFunc("/repos/task-fields", s.handleRepoTaskFields)
89
90
  mux.HandleFunc("/repos", s.handleRepos)
90
91
  mux.HandleFunc("/repos/", s.handleRepoByName)
@@ -99,6 +100,14 @@ type server struct {
99
100
  deps Deps
100
101
  }
101
102
 
103
+ // repoEnsurer is implemented by the composition root when the selected
104
+ // runner can provision a repository resource. It is intentionally separate
105
+ // from Deps so existing task/run service seams do not need a runner-specific
106
+ // method.
107
+ type repoEnsurer interface {
108
+ EnsureRepo(context.Context, runner.RepoCandidate) error
109
+ }
110
+
102
111
  // --- envelope helpers ---
103
112
 
104
113
  func writeOK(w http.ResponseWriter, status int, data any) {
@@ -122,6 +131,8 @@ func mapErr(w http.ResponseWriter, err error) {
122
131
  switch {
123
132
  case errors.Is(err, run.ErrRestartConflict):
124
133
  writeErr(w, http.StatusConflict, "conflict", err.Error())
134
+ case errors.Is(err, run.ErrNotFound):
135
+ writeErr(w, http.StatusNotFound, "notFound", err.Error())
125
136
  case errors.Is(err, ErrNotFound):
126
137
  writeErr(w, http.StatusNotFound, "notFound", err.Error())
127
138
  case errors.Is(err, ErrConflict):
@@ -182,6 +193,18 @@ func (s *server) handleStop(w http.ResponseWriter, r *http.Request) {
182
193
  func (s *server) handleWorkflows(w http.ResponseWriter, r *http.Request) {
183
194
  switch r.Method {
184
195
  case http.MethodGet:
196
+ // The summary DTO embeds the original definition, so returning it is
197
+ // additive for clients that still decode []workflow.Workflow. Older
198
+ // composition roots without this optional capability use the base list.
199
+ if queries, ok := s.deps.(WorkflowSummaryQueries); ok {
200
+ summaries, err := queries.ListWorkflowSummaries(r.Context())
201
+ if err != nil {
202
+ mapErr(w, err)
203
+ return
204
+ }
205
+ writeOK(w, http.StatusOK, summaries)
206
+ return
207
+ }
185
208
  wfs, err := s.deps.ListWorkflows(r.Context())
186
209
  if err != nil {
187
210
  mapErr(w, err)
@@ -216,6 +239,15 @@ func (s *server) handleWorkflowByName(w http.ResponseWriter, r *http.Request) {
216
239
  }
217
240
  switch r.Method {
218
241
  case http.MethodGet:
242
+ if queries, ok := s.deps.(WorkflowDetailQueries); ok {
243
+ detail, err := queries.GetWorkflowDetail(r.Context(), name)
244
+ if err != nil {
245
+ mapErr(w, err)
246
+ return
247
+ }
248
+ writeOK(w, http.StatusOK, detail)
249
+ return
250
+ }
219
251
  wf, err := s.deps.GetWorkflow(r.Context(), name)
220
252
  if err != nil {
221
253
  mapErr(w, err)
@@ -247,6 +279,38 @@ func (s *server) handleReposDiscover(w http.ResponseWriter, r *http.Request) {
247
279
  writeOK(w, http.StatusOK, candidates)
248
280
  }
249
281
 
282
+ func (s *server) handleRepoEnsure(w http.ResponseWriter, r *http.Request) {
283
+ if !methodOnly(w, r, http.MethodPost) {
284
+ return
285
+ }
286
+ ensurer, ok := s.deps.(repoEnsurer)
287
+ if !ok {
288
+ mapErr(w, fmt.Errorf("%w: runner repository provisioning is unavailable", ErrInvalid))
289
+ return
290
+ }
291
+ body, err := readBody(r)
292
+ if err != nil {
293
+ writeErr(w, http.StatusBadRequest, "invalid", err.Error())
294
+ return
295
+ }
296
+ var candidate runner.RepoCandidate
297
+ if err := decodeStrict(body, &candidate); err != nil {
298
+ writeErr(w, http.StatusBadRequest, "invalid", err.Error())
299
+ return
300
+ }
301
+ candidate.Name = strings.TrimSpace(candidate.Name)
302
+ candidate.Path = strings.TrimSpace(candidate.Path)
303
+ if candidate.Name == "" || candidate.Path == "" {
304
+ writeErr(w, http.StatusBadRequest, "invalid", "repository name and path are required")
305
+ return
306
+ }
307
+ if err := ensurer.EnsureRepo(r.Context(), candidate); err != nil {
308
+ mapErr(w, err)
309
+ return
310
+ }
311
+ writeOK(w, http.StatusOK, candidate)
312
+ }
313
+
250
314
  func (s *server) handleRepoTaskFields(w http.ResponseWriter, r *http.Request) {
251
315
  if r.Method == http.MethodGet {
252
316
  fields, err := s.deps.TaskRegistrationFields(r.Context(), nil)
@@ -503,6 +567,10 @@ func (s *server) handleRuns(w http.ResponseWriter, r *http.Request) {
503
567
  filter.Repo = q.Get("repo")
504
568
  filter.Workflow = q.Get("workflow")
505
569
  filter.Ticket = q.Get("ticket")
570
+ if active := q.Get("active"); active != "" {
571
+ value := active == "1" || strings.EqualFold(active, "true")
572
+ filter.Active = &value
573
+ }
506
574
  runs, err := s.deps.ListRuns(r.Context(), filter)
507
575
  if err != nil {
508
576
  mapErr(w, err)
@@ -521,6 +589,15 @@ func (s *server) handleRunByTicket(w http.ResponseWriter, r *http.Request) {
521
589
  if !methodOnly(w, r, http.MethodGet) {
522
590
  return
523
591
  }
592
+ if queries, ok := s.deps.(RunDetailQueries); ok {
593
+ detail, err := queries.GetRunDetail(r.Context(), parts[0])
594
+ if err != nil {
595
+ mapErr(w, err)
596
+ return
597
+ }
598
+ writeOK(w, http.StatusOK, detail)
599
+ return
600
+ }
524
601
  rn, err := s.deps.GetRunByTicket(r.Context(), parts[0])
525
602
  if err != nil {
526
603
  mapErr(w, err)
@@ -15,7 +15,7 @@
15
15
  "48x48": "https://secure.gravatar.com/avatar/09110b9326675e772c94aa61fc89195b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRP-6.png"
16
16
  },
17
17
  "displayName": "Raj Popat",
18
- "emailAddress": "raj.popat@wolterskluwer.com",
18
+ "emailAddress": "test.user@example.com",
19
19
  "self": "https://jira-prod-us-26-3.prod.atl-paas.net/rest/api/3/user?accountId=712020%3Adad5df85-a7bd-4337-b858-0faf9012a009",
20
20
  "timeZone": "America/New_York"
21
21
  },
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "fieldsToInclude": null,
50
50
  "id": "157320",
51
- "key": "GHCOS-41242",
51
+ "key": "DEMO-41242",
52
52
  "names": null,
53
53
  "operations": null,
54
54
  "properties": null,
@@ -74,7 +74,7 @@
74
74
  "48x48": "https://secure.gravatar.com/avatar/09110b9326675e772c94aa61fc89195b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRP-6.png"
75
75
  },
76
76
  "displayName": "Raj Popat",
77
- "emailAddress": "raj.popat@wolterskluwer.com",
77
+ "emailAddress": "test.user@example.com",
78
78
  "self": "https://jira-prod-us-26-3.prod.atl-paas.net/rest/api/3/user?accountId=712020%3Adad5df85-a7bd-4337-b858-0faf9012a009",
79
79
  "timeZone": "America/New_York"
80
80
  },
@@ -107,7 +107,7 @@
107
107
  },
108
108
  "fieldsToInclude": null,
109
109
  "id": "155110",
110
- "key": "GHCOS-41175",
110
+ "key": "DEMO-41175",
111
111
  "names": null,
112
112
  "operations": null,
113
113
  "properties": null,
@@ -62,6 +62,7 @@ type NudgeTemplateData struct {
62
62
  Workflow string
63
63
  Repo string
64
64
  Node string
65
+ Mailbox string
65
66
  NextSteps string
66
67
  }
67
68
 
@@ -70,7 +71,7 @@ var namePattern = regexp.MustCompile(`^[a-z][a-zA-Z0-9]*$`)
70
71
  var nudgeVarPattern = regexp.MustCompile(`\{\{([^{}]*)\}\}`)
71
72
 
72
73
  var knownNudgeVars = map[string]bool{
73
- "taskSystem": true, "ticket": true, "workflow": true, "repo": true, "node": true, "nextSteps": true,
74
+ "taskSystem": true, "ticket": true, "workflow": true, "repo": true, "node": true, "mailbox": true, "nextSteps": true,
74
75
  }
75
76
 
76
77
  // Parse strictly decodes a workflow YAML document. Unknown fields and
@@ -326,6 +327,8 @@ func (w *Workflow) RenderNudge(node string, data NudgeTemplateData) (string, err
326
327
  return data.Repo
327
328
  case "node":
328
329
  return data.Node
330
+ case "mailbox":
331
+ return data.Mailbox
329
332
  case "nextSteps":
330
333
  return data.NextSteps
331
334
  }
@@ -350,7 +350,7 @@ func TestValidateGraphReachability(t *testing.T) {
350
350
 
351
351
  func TestValidateNudgeTemplate(t *testing.T) {
352
352
  t.Run("supported variables", func(t *testing.T) {
353
- yaml := strings.Replace(minimalValid, " description: Do the coding work.", " description: Do the coding work.\n nudgePrompt: \"{{taskSystem}} {{ticket}} {{workflow}} {{repo}} {{node}} {{nextSteps}}\"", 1)
353
+ yaml := strings.Replace(minimalValid, " description: Do the coding work.", " description: Do the coding work.\n nudgePrompt: \"{{taskSystem}} {{ticket}} {{workflow}} {{repo}} {{node}} {{mailbox}} {{nextSteps}}\"", 1)
354
354
  wf := parse(t, "basicFlow", yaml)
355
355
  if err := wf.Validate(); err != nil {
356
356
  t.Fatalf("supported nudge variables rejected: %v", err)
@@ -389,16 +389,16 @@ func TestCleanupRunnerOnEndDefaultsFalse(t *testing.T) {
389
389
  }
390
390
 
391
391
  func TestRenderNudge(t *testing.T) {
392
- yaml := strings.Replace(minimalValid, " description: Do the coding work.", " description: Do the coding work.\n nudgePrompt: \"task={{taskSystem}} ticket={{ticket}} wf={{workflow}} repo={{repo}} node={{node}} steps={{nextSteps}}\"", 1)
392
+ yaml := strings.Replace(minimalValid, " description: Do the coding work.", " description: Do the coding work.\n nudgePrompt: \"task={{taskSystem}} ticket={{ticket}} wf={{workflow}} repo={{repo}} node={{node}} mailbox={{mailbox}} steps={{nextSteps}}\"", 1)
393
393
  wf := parse(t, "basicFlow", yaml)
394
394
  for _, taskSystem := range []string{"jira", "linear"} {
395
395
  out, err := wf.RenderNudge("coding", workflow.NudgeTemplateData{
396
- TaskSystem: taskSystem, Ticket: "PAY-101", Workflow: "basicFlow", Repo: "payments", Node: "coding", NextSteps: "end",
396
+ TaskSystem: taskSystem, Ticket: "PAY-101", Workflow: "basicFlow", Repo: "payments", Node: "coding", Mailbox: "PAY-101.1", NextSteps: "end",
397
397
  })
398
398
  if err != nil {
399
399
  t.Fatalf("RenderNudge(%s) failed: %v", taskSystem, err)
400
400
  }
401
- want := "task=" + taskSystem + " ticket=PAY-101 wf=basicFlow repo=payments node=coding steps=end"
401
+ want := "task=" + taskSystem + " ticket=PAY-101 wf=basicFlow repo=payments node=coding mailbox=PAY-101.1 steps=end"
402
402
  if out != want {
403
403
  t.Fatalf("RenderNudge(%s) = %q, want %q", taskSystem, out, want)
404
404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-flow",
3
- "version": "0.2.8-alpha",
3
+ "version": "0.2.10-alpha",
4
4
  "description": "Graph-based agent workflow engine — tracker-agnostic, pluggable runners",
5
5
  "bin": {
6
6
  "relay-flow": "./bin/relay-flow.js",