relay-flow 0.3.9-alpha → 0.3.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.
- package/README.md +27 -19
- package/cmd/relay-flow/commands_test.go +440 -21
- package/cmd/relay-flow/main.go +203 -37
- package/cmd/relay-flow/observability_test.go +11 -0
- package/cmd/relay-flow/onboarding.go +1 -1
- package/cmd/relay-flow/onboarding_test.go +1 -1
- package/cmd/relay-flow/scenario_test.go +26 -10
- package/cmd/relay-flow/serve.go +27 -1
- package/internal/execution/goworkflows/fakes_test.go +20 -0
- package/internal/execution/goworkflows/interpreter.go +1 -0
- package/internal/execution/goworkflows/ownership_test.go +259 -0
- package/internal/execution/goworkflows/recovery_test.go +81 -1
- package/internal/execution/temporal/interpreter.go +2 -1
- package/internal/execution/temporal/recovery.go +68 -36
- package/internal/harness/harness.go +1 -0
- package/internal/harness/opencode/opencode.go +5 -0
- package/internal/harness/opencode/opencode_test.go +17 -1
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/harness/pi/pi.go +6 -0
- package/internal/harness/pi/task_env_test.go +16 -0
- package/internal/recover/recover.go +30 -1
- package/internal/repo/binding_test.go +63 -0
- package/internal/repo/repo.go +47 -6
- package/internal/router/router.go +39 -5
- package/internal/router/router_test.go +67 -1
- package/internal/run/manager.go +90 -3
- package/internal/run/run_manager_test.go +74 -0
- package/internal/server/api_test.go +13 -0
- package/internal/server/client.go +7 -0
- package/internal/server/fixture_test.go +5 -0
- package/internal/server/server.go +32 -3
- package/internal/task/beads/beads.go +201 -3
- package/internal/task/beads/beads_test.go +172 -0
- package/internal/task/jira/effects_test.go +86 -0
- package/internal/task/jira/filters_test.go +103 -0
- package/internal/task/jira/helpers_test.go +11 -2
- package/internal/task/jira/jira.go +228 -4
- package/internal/task/jira/normalize.go +30 -15
- package/internal/task/jira/rest/client.go +17 -2
- package/internal/task/jira/transition_defaults_test.go +3 -1
- package/internal/task/task.go +66 -0
- package/internal/workflow/report.go +3 -0
- package/internal/workflow/report_test.go +16 -0
- package/internal/workflow/workflow.go +24 -0
- package/internal/workflow/workflow_test.go +58 -1
- package/package.json +1 -1
package/cmd/relay-flow/main.go
CHANGED
|
@@ -61,13 +61,25 @@ var (
|
|
|
61
61
|
buildDate = "unknown"
|
|
62
62
|
)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
var runningAsMain = false
|
|
65
|
+
|
|
66
|
+
const serveChildEnvironment = "RELAY_FLOW_SERVE_CHILD"
|
|
67
|
+
|
|
68
|
+
func main() {
|
|
69
|
+
runningAsMain = true
|
|
70
|
+
os.Exit(run(os.Args[1:], os.Stdin))
|
|
71
|
+
}
|
|
65
72
|
|
|
66
73
|
// home returns the relay-flow root, honoring RELAY_FLOW_HOME (tests and
|
|
67
|
-
// non-standard installs) and falling back to ~/.relay-flow.
|
|
74
|
+
// non-standard installs) and falling back to ~/.relay-flow. The configured
|
|
75
|
+
// root is made absolute so detached children never resolve it through a
|
|
76
|
+
// caller directory that may disappear after launch.
|
|
68
77
|
func home() (paths.Paths, error) {
|
|
69
78
|
if env := os.Getenv("RELAY_FLOW_HOME"); env != "" {
|
|
70
|
-
root := env
|
|
79
|
+
root, err := filepath.Abs(env)
|
|
80
|
+
if err != nil {
|
|
81
|
+
return paths.Paths{}, fmt.Errorf("resolve RELAY_FLOW_HOME: %w", err)
|
|
82
|
+
}
|
|
71
83
|
return paths.Paths{
|
|
72
84
|
Root: root,
|
|
73
85
|
Config: filepath.Join(root, "config.yaml"),
|
|
@@ -150,6 +162,7 @@ func usage(w io.Writer) {
|
|
|
150
162
|
|
|
151
163
|
Usage:
|
|
152
164
|
relay-flow [command] [flags]
|
|
165
|
+
rf [command] [flags] (equivalent executable name)
|
|
153
166
|
|
|
154
167
|
Setup and server:
|
|
155
168
|
relay-flow init [--force] [--task-plugin <name> --runner-plugin <name> --harness-plugin <name>]
|
|
@@ -157,8 +170,13 @@ Setup and server:
|
|
|
157
170
|
[--temporal-address <host:port>] [--temporal-namespace <name>]
|
|
158
171
|
(interactive first run also authenticates and optionally registers repos)
|
|
159
172
|
relay-flow task auth [task-plugin options]
|
|
160
|
-
relay-flow serve [--recover] [--debug] [--background]
|
|
173
|
+
relay-flow serve [--recover] [--debug] [--foreground | --background]
|
|
161
174
|
relay-flow stop
|
|
175
|
+
|
|
176
|
+
Plain serve starts detached, waits for Unix-socket readiness, and then returns.
|
|
177
|
+
Use serve --foreground for a blocking process-supervisor/development mode.
|
|
178
|
+
Detached startup diagnostics are written to ~/.relay-flow/server.log (or
|
|
179
|
+
$RELAY_FLOW_HOME/server.log when a custom home is configured).
|
|
162
180
|
relay-flow report
|
|
163
181
|
relay-flow version | --version | -v
|
|
164
182
|
|
|
@@ -179,6 +197,7 @@ Run:
|
|
|
179
197
|
relay-flow run get --ticket <key> [--json]
|
|
180
198
|
relay-flow run restart --ticket <key>
|
|
181
199
|
relay-flow run cancel --ticket <key>
|
|
200
|
+
relay-flow run backfill-owner --repo <name> --ticket <key> --workflow <name>
|
|
182
201
|
|
|
183
202
|
Use relay-flow <command> --help for command-specific details.
|
|
184
203
|
|
|
@@ -247,9 +266,12 @@ func printScopedHelp(args []string, w io.Writer) int {
|
|
|
247
266
|
fmt.Fprintln(w, " --force Update plugin selections while preserving existing durable state.")
|
|
248
267
|
fmt.Fprintln(w, "Example: relay-flow init --task-plugin jira --runner-plugin orca --harness-plugin opencode")
|
|
249
268
|
case "serve":
|
|
250
|
-
fmt.Fprintln(w, "Usage: relay-flow serve [--recover] [--debug] [--background]")
|
|
251
|
-
fmt.Fprintln(w, "\nStart
|
|
252
|
-
fmt.Fprintln(w, "
|
|
269
|
+
fmt.Fprintln(w, "Usage: relay-flow serve [--recover] [--debug] [--foreground | --background]")
|
|
270
|
+
fmt.Fprintln(w, "\nStart relay-flow detached and wait for the Unix socket to become ready.")
|
|
271
|
+
fmt.Fprintln(w, " --foreground Run the server in the current process until stop or signal.")
|
|
272
|
+
fmt.Fprintln(w, " --background Compatibility spelling for the default detached mode.")
|
|
273
|
+
fmt.Fprintln(w, "The detached server writes diagnostics to ~/.relay-flow/server.log (or RELAY_FLOW_HOME/server.log).")
|
|
274
|
+
fmt.Fprintln(w, "Examples: rf serve; relay-flow serve --foreground")
|
|
253
275
|
case "stop":
|
|
254
276
|
fmt.Fprintln(w, "Usage: relay-flow stop")
|
|
255
277
|
fmt.Fprintln(w, "\nStop the running relay-flow server.")
|
|
@@ -384,6 +406,9 @@ func printScopedHelp(args []string, w io.Writer) int {
|
|
|
384
406
|
fmt.Fprintln(w, " cancel Permanently cancel the current execution attempt.")
|
|
385
407
|
fmt.Fprintln(w, " Usage: relay-flow run cancel --ticket <key> [--reason <text>]")
|
|
386
408
|
fmt.Fprintln(w, " Example: relay-flow run cancel --ticket PAY-101 --reason "+`"operator request"`)
|
|
409
|
+
fmt.Fprintln(w, " backfill-owner Explicitly add provenance to a legacy workflow claim.")
|
|
410
|
+
fmt.Fprintln(w, " Usage: relay-flow run backfill-owner --repo <name> --ticket <key> --workflow <name>")
|
|
411
|
+
fmt.Fprintln(w, " Example: relay-flow run backfill-owner --repo payments --ticket PAY-101 --workflow basicFlow")
|
|
387
412
|
fmt.Fprintln(w, "Use relay-flow run <subcommand> --help for flag details.")
|
|
388
413
|
case "run list":
|
|
389
414
|
fmt.Fprintln(w, "Usage: relay-flow run list [--repo <name>] [--workflow <name>] [--ticket <key>] [--active] [--json] [--no-color]")
|
|
@@ -418,6 +443,15 @@ func printScopedHelp(args []string, w io.Writer) int {
|
|
|
418
443
|
fmt.Fprintln(w, " --ticket <key> Task ticket key (required).")
|
|
419
444
|
fmt.Fprintln(w, " --reason <text> Optional cancellation reason recorded with the request.")
|
|
420
445
|
fmt.Fprintln(w, "Example: relay-flow run cancel --ticket PAY-101 --reason "+`"operator request"`)
|
|
446
|
+
case "run backfill-owner":
|
|
447
|
+
fmt.Fprintln(w, "Usage: relay-flow run backfill-owner --repo <name> --ticket <key> --workflow <name>")
|
|
448
|
+
fmt.Fprintln(w, "\nExplicitly add adapter-owned provenance to a legacy workflow claim.")
|
|
449
|
+
fmt.Fprintln(w, "This never removes wf: labels, comments, or history and is not automatic routing.")
|
|
450
|
+
fmt.Fprintln(w, "Flags:")
|
|
451
|
+
fmt.Fprintln(w, " --repo <name> Registered repository containing the claim (required).")
|
|
452
|
+
fmt.Fprintln(w, " --ticket <key> Task ticket key (required).")
|
|
453
|
+
fmt.Fprintln(w, " --workflow <name> Claimed workflow name (required).")
|
|
454
|
+
fmt.Fprintln(w, "Example: relay-flow run backfill-owner --repo payments --ticket PAY-101 --workflow basicFlow")
|
|
421
455
|
default:
|
|
422
456
|
fmt.Fprintf(os.Stderr, "unknown help target: %s\n", name)
|
|
423
457
|
return exitUsage
|
|
@@ -689,7 +723,7 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
689
723
|
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
690
724
|
return exitFail
|
|
691
725
|
}
|
|
692
|
-
fmt.Println("Repository setup complete. The server is running; stop it with `
|
|
726
|
+
fmt.Println("Repository setup complete. The server is running; stop it with `rf stop`.")
|
|
693
727
|
}
|
|
694
728
|
} else if err := config.SaveMachine(p.Config, cfg); err != nil {
|
|
695
729
|
fmt.Fprintln(os.Stderr, err)
|
|
@@ -821,11 +855,17 @@ func cmdServe(p paths.Paths, args []string) int {
|
|
|
821
855
|
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
|
|
822
856
|
recover := fs.Bool("recover", false, "treat execution state as lost and rebuild from the task system")
|
|
823
857
|
debug := fs.Bool("debug", false, "enable debug logging (overrides RELAY_FLOW_LOG_LEVEL)")
|
|
824
|
-
|
|
858
|
+
foreground := fs.Bool("foreground", false, "run the server in the current process until stop or signal")
|
|
859
|
+
background := fs.Bool("background", false, "compatibility spelling for the default detached mode")
|
|
825
860
|
if err := fs.Parse(args); err != nil {
|
|
826
861
|
return exitUsage
|
|
827
862
|
}
|
|
828
|
-
|
|
863
|
+
child := os.Getenv(serveChildEnvironment) == "1"
|
|
864
|
+
if *foreground && *background {
|
|
865
|
+
fmt.Fprintln(os.Stderr, "serve: --foreground and --background cannot be used together")
|
|
866
|
+
return exitUsage
|
|
867
|
+
}
|
|
868
|
+
if !*foreground && !child {
|
|
829
869
|
if err := startBackgroundServe(p, *recover, *debug); err != nil {
|
|
830
870
|
fmt.Fprintln(os.Stderr, err)
|
|
831
871
|
return exitFail
|
|
@@ -833,6 +873,12 @@ func cmdServe(p paths.Paths, args []string) int {
|
|
|
833
873
|
fmt.Println("Relay-flow server started")
|
|
834
874
|
return exitOK
|
|
835
875
|
}
|
|
876
|
+
if runningAsMain {
|
|
877
|
+
if err := anchorForegroundServe(p.Root); err != nil {
|
|
878
|
+
fmt.Fprintln(os.Stderr, err)
|
|
879
|
+
return exitFail
|
|
880
|
+
}
|
|
881
|
+
}
|
|
836
882
|
logCloser, err := logging.Setup(p.ServerLog, logging.Options{
|
|
837
883
|
Debug: *debug,
|
|
838
884
|
Env: os.Getenv("RELAY_FLOW_LOG_LEVEL"),
|
|
@@ -858,6 +904,9 @@ var (
|
|
|
858
904
|
)
|
|
859
905
|
|
|
860
906
|
func startBackgroundServe(p paths.Paths, recover, debug bool) error {
|
|
907
|
+
if err := ensureStableServeDirectory(p.Root); err != nil {
|
|
908
|
+
return fmt.Errorf("serve --background: prepare working directory: %w", err)
|
|
909
|
+
}
|
|
861
910
|
client := server.NewClient(p.Socket)
|
|
862
911
|
if serverResponding(client, 200*time.Millisecond) {
|
|
863
912
|
return fmt.Errorf("serve --background: server is already running")
|
|
@@ -869,36 +918,57 @@ func startBackgroundServe(p paths.Paths, recover, debug bool) error {
|
|
|
869
918
|
}
|
|
870
919
|
|
|
871
920
|
var (
|
|
872
|
-
cmd
|
|
873
|
-
wait
|
|
874
|
-
|
|
875
|
-
|
|
921
|
+
cmd *exec.Cmd
|
|
922
|
+
wait chan error
|
|
923
|
+
testServeCancel chan struct{}
|
|
924
|
+
observedOwner = lockHeld
|
|
925
|
+
devNull *os.File
|
|
876
926
|
)
|
|
877
927
|
if !lockHeld {
|
|
878
|
-
executable, resolveErr := os.Executable()
|
|
879
|
-
if resolveErr != nil {
|
|
880
|
-
return fmt.Errorf("serve --background: resolve executable: %w", resolveErr)
|
|
881
|
-
}
|
|
882
|
-
childArgs := []string{"serve"}
|
|
883
|
-
if recover {
|
|
884
|
-
childArgs = append(childArgs, "--recover")
|
|
885
|
-
}
|
|
886
|
-
if debug {
|
|
887
|
-
childArgs = append(childArgs, "--debug")
|
|
888
|
-
}
|
|
889
|
-
devNull, err = os.OpenFile(os.DevNull, os.O_RDWR, 0)
|
|
890
|
-
if err != nil {
|
|
891
|
-
return fmt.Errorf("serve --background: open %s: %w", os.DevNull, err)
|
|
892
|
-
}
|
|
893
|
-
defer devNull.Close()
|
|
894
|
-
cmd = exec.Command(executable, childArgs...)
|
|
895
|
-
cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull
|
|
896
|
-
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
897
|
-
if err := cmd.Start(); err != nil {
|
|
898
|
-
return fmt.Errorf("serve --background: start: %w; see %s", err, p.ServerLog)
|
|
899
|
-
}
|
|
900
928
|
wait = make(chan error, 1)
|
|
901
|
-
|
|
929
|
+
if !runningAsMain {
|
|
930
|
+
// The run(args, stdin) seam is used by in-process tests. A Go test
|
|
931
|
+
// executable is not the relay-flow CLI entrypoint, so spawning
|
|
932
|
+
// os.Executable here would recursively run the test suite. Keep the
|
|
933
|
+
// production path detached while using the same serveRoot path in
|
|
934
|
+
// that seam.
|
|
935
|
+
serveCtx := &inProcessServeContext{Context: context.Background(), done: make(chan struct{})}
|
|
936
|
+
testServeCancel = serveCtx.done
|
|
937
|
+
go func() {
|
|
938
|
+
logCloser, logErr := logging.Setup(p.ServerLog, logging.Options{
|
|
939
|
+
Debug: debug,
|
|
940
|
+
Env: os.Getenv("RELAY_FLOW_LOG_LEVEL"),
|
|
941
|
+
})
|
|
942
|
+
if logErr != nil {
|
|
943
|
+
wait <- logErr
|
|
944
|
+
return
|
|
945
|
+
}
|
|
946
|
+
defer logCloser.Close()
|
|
947
|
+
wait <- serveRoot(serveCtx, p, recover)
|
|
948
|
+
}()
|
|
949
|
+
} else {
|
|
950
|
+
executable, resolveErr := os.Executable()
|
|
951
|
+
if resolveErr != nil {
|
|
952
|
+
return fmt.Errorf("serve --background: resolve executable: %w", resolveErr)
|
|
953
|
+
}
|
|
954
|
+
childArgs := serveChildArgs(recover, debug)
|
|
955
|
+
devNull, err = os.OpenFile(os.DevNull, os.O_RDWR, 0)
|
|
956
|
+
if err != nil {
|
|
957
|
+
return fmt.Errorf("serve --background: open %s: %w", os.DevNull, err)
|
|
958
|
+
}
|
|
959
|
+
defer devNull.Close()
|
|
960
|
+
cmd = exec.Command(executable, childArgs...)
|
|
961
|
+
cmd.Dir = p.Root
|
|
962
|
+
childEnvironment := setEnvironment(os.Environ(), serveChildEnvironment, "1")
|
|
963
|
+
childEnvironment = setEnvironment(childEnvironment, "PWD", p.Root)
|
|
964
|
+
cmd.Env = setEnvironment(childEnvironment, "RELAY_FLOW_HOME", p.Root)
|
|
965
|
+
cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull
|
|
966
|
+
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
967
|
+
if err := cmd.Start(); err != nil {
|
|
968
|
+
return fmt.Errorf("serve --background: start: %w; see %s", err, p.ServerLog)
|
|
969
|
+
}
|
|
970
|
+
go func() { wait <- cmd.Wait() }()
|
|
971
|
+
}
|
|
902
972
|
}
|
|
903
973
|
|
|
904
974
|
ticker := time.NewTicker(backgroundServePollInterval)
|
|
@@ -908,6 +978,10 @@ func startBackgroundServe(p paths.Paths, recover, debug bool) error {
|
|
|
908
978
|
return nil
|
|
909
979
|
}
|
|
910
980
|
if !time.Now().Before(deadline) {
|
|
981
|
+
if testServeCancel != nil {
|
|
982
|
+
close(testServeCancel)
|
|
983
|
+
testServeCancel = nil
|
|
984
|
+
}
|
|
911
985
|
if cmd != nil && wait != nil {
|
|
912
986
|
_ = cmd.Process.Kill()
|
|
913
987
|
<-wait
|
|
@@ -925,6 +999,7 @@ func startBackgroundServe(p paths.Paths, recover, debug bool) error {
|
|
|
925
999
|
}
|
|
926
1000
|
select {
|
|
927
1001
|
case childErr := <-wait:
|
|
1002
|
+
testServeCancel = nil
|
|
928
1003
|
held, lockErr := serverLockHeld(p.Lock)
|
|
929
1004
|
if lockErr != nil {
|
|
930
1005
|
return fmt.Errorf("serve --background: inspect server lock: %w", lockErr)
|
|
@@ -943,6 +1018,80 @@ func startBackgroundServe(p paths.Paths, recover, debug bool) error {
|
|
|
943
1018
|
}
|
|
944
1019
|
}
|
|
945
1020
|
|
|
1021
|
+
func serveChildArgs(recover, debug bool) []string {
|
|
1022
|
+
args := []string{"serve"}
|
|
1023
|
+
if recover {
|
|
1024
|
+
args = append(args, "--recover")
|
|
1025
|
+
}
|
|
1026
|
+
if debug {
|
|
1027
|
+
args = append(args, "--debug")
|
|
1028
|
+
}
|
|
1029
|
+
return args
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
type inProcessServeContext struct {
|
|
1033
|
+
context.Context
|
|
1034
|
+
done chan struct{}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
func (c *inProcessServeContext) Done() <-chan struct{} {
|
|
1038
|
+
return c.done
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
func (c *inProcessServeContext) Err() error {
|
|
1042
|
+
select {
|
|
1043
|
+
case <-c.done:
|
|
1044
|
+
return context.Canceled
|
|
1045
|
+
default:
|
|
1046
|
+
return nil
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
func setEnvironment(environment []string, key, value string) []string {
|
|
1051
|
+
prefix := key + "="
|
|
1052
|
+
out := make([]string, 0, len(environment)+1)
|
|
1053
|
+
for _, entry := range environment {
|
|
1054
|
+
if strings.HasPrefix(entry, prefix) {
|
|
1055
|
+
continue
|
|
1056
|
+
}
|
|
1057
|
+
out = append(out, entry)
|
|
1058
|
+
}
|
|
1059
|
+
return append(out, prefix+value)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
func ensureStableServeDirectory(root string) error {
|
|
1063
|
+
if err := os.MkdirAll(root, 0o700); err != nil {
|
|
1064
|
+
return err
|
|
1065
|
+
}
|
|
1066
|
+
if err := os.Chmod(root, 0o700); err != nil {
|
|
1067
|
+
return err
|
|
1068
|
+
}
|
|
1069
|
+
info, err := os.Stat(root)
|
|
1070
|
+
if err != nil {
|
|
1071
|
+
return err
|
|
1072
|
+
}
|
|
1073
|
+
if !info.IsDir() {
|
|
1074
|
+
return fmt.Errorf("%s is not a directory", root)
|
|
1075
|
+
}
|
|
1076
|
+
return nil
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
func anchorForegroundServe(root string) error {
|
|
1080
|
+
if err := ensureStableServeDirectory(root); err != nil {
|
|
1081
|
+
return err
|
|
1082
|
+
}
|
|
1083
|
+
if err := os.Chdir(root); err != nil {
|
|
1084
|
+
return fmt.Errorf("anchor working directory at %s: %w", root, err)
|
|
1085
|
+
}
|
|
1086
|
+
if err := os.Setenv("PWD", root); err != nil {
|
|
1087
|
+
return fmt.Errorf("set working directory environment at %s: %w", root, err)
|
|
1088
|
+
}
|
|
1089
|
+
if err := os.Setenv("RELAY_FLOW_HOME", root); err != nil {
|
|
1090
|
+
return fmt.Errorf("set relay-flow home environment at %s: %w", root, err)
|
|
1091
|
+
}
|
|
1092
|
+
return nil
|
|
1093
|
+
}
|
|
1094
|
+
|
|
946
1095
|
func serverLockHeld(path string) (bool, error) {
|
|
947
1096
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
|
948
1097
|
if err != nil {
|
|
@@ -1400,6 +1549,23 @@ func cmdRun(c *server.Client, args []string) int {
|
|
|
1400
1549
|
return exitFail
|
|
1401
1550
|
}
|
|
1402
1551
|
return exitOK
|
|
1552
|
+
case "backfill-owner":
|
|
1553
|
+
fs := flag.NewFlagSet("run backfill-owner", flag.ContinueOnError)
|
|
1554
|
+
repoName := fs.String("repo", "", "registered repository name")
|
|
1555
|
+
ticket := fs.String("ticket", "", "ticket key")
|
|
1556
|
+
workflowName := fs.String("workflow", "", "workflow name")
|
|
1557
|
+
if err := fs.Parse(args[1:]); err != nil {
|
|
1558
|
+
return exitUsage
|
|
1559
|
+
}
|
|
1560
|
+
if *repoName == "" || *ticket == "" || *workflowName == "" {
|
|
1561
|
+
fmt.Fprintln(os.Stderr, "run backfill-owner: --repo, --ticket, and --workflow are required")
|
|
1562
|
+
return exitUsage
|
|
1563
|
+
}
|
|
1564
|
+
if err := c.BackfillClaimOwner(context.Background(), *repoName, *ticket, *workflowName); err != nil {
|
|
1565
|
+
fmt.Fprintln(os.Stderr, err)
|
|
1566
|
+
return exitFail
|
|
1567
|
+
}
|
|
1568
|
+
return exitOK
|
|
1403
1569
|
}
|
|
1404
1570
|
usage(os.Stderr)
|
|
1405
1571
|
return exitUsage
|
|
@@ -64,6 +64,17 @@ func TestScopedHelpCoversAllCommandLevels(t *testing.T) {
|
|
|
64
64
|
if !strings.Contains(root, "Use relay-flow <command> --help for command-specific details.") {
|
|
65
65
|
t.Fatalf("root help missing scoped-help guidance:\n%s", root)
|
|
66
66
|
}
|
|
67
|
+
for _, want := range []string{
|
|
68
|
+
"Plain serve starts detached",
|
|
69
|
+
"Unix-socket readiness",
|
|
70
|
+
"serve --foreground",
|
|
71
|
+
"blocking process-supervisor/development mode",
|
|
72
|
+
"server.log",
|
|
73
|
+
} {
|
|
74
|
+
if !strings.Contains(root, want) {
|
|
75
|
+
t.Fatalf("root help missing serve startup guidance %q:\n%s", want, root)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
func TestInitHelpDocumentsTemporalFlags(t *testing.T) {
|
|
@@ -337,7 +337,7 @@ func standaloneRepoServerHint(err error) error {
|
|
|
337
337
|
message := err.Error()
|
|
338
338
|
for _, marker := range []string{"dial unix", "connect: no such file or directory", "connection refused"} {
|
|
339
339
|
if strings.Contains(message, marker) {
|
|
340
|
-
return fmt.Errorf("%w; start the server with `
|
|
340
|
+
return fmt.Errorf("%w; start the server with `rf serve` (or use the guided init path; use `relay-flow serve --foreground` when a blocking process is required)", err)
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
return err
|
|
@@ -496,7 +496,7 @@ func TestRunOnboardingSpinner(t *testing.T) {
|
|
|
496
496
|
|
|
497
497
|
func TestStandaloneRepoServerHint(t *testing.T) {
|
|
498
498
|
err := standaloneRepoServerHint(errors.New("server call GET /repos: dial unix /tmp/server.sock: connect: no such file or directory"))
|
|
499
|
-
if err == nil || !containsAll(err.Error(), "serve
|
|
499
|
+
if err == nil || !containsAll(err.Error(), "rf serve", "guided init") {
|
|
500
500
|
t.Fatalf("server hint = %v", err)
|
|
501
501
|
}
|
|
502
502
|
original := errors.New("server bad request")
|
|
@@ -839,6 +839,18 @@ func (s *scenarioTaskSystem) CompileFilter(config.RawValues) (func(task.Ticket)
|
|
|
839
839
|
return func(t task.Ticket) bool { return t.Key == scenarioTicket }, nil
|
|
840
840
|
}
|
|
841
841
|
|
|
842
|
+
func (s *scenarioTaskSystem) CompileOwnershipFilter(config.RawValues) (func(task.Ticket) bool, error) {
|
|
843
|
+
return func(task.Ticket) bool { return true }, nil
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
func (s *scenarioTaskSystem) ValidateOwnership(context.Context, task.TicketRef, string, config.RawValues) error {
|
|
847
|
+
return nil
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
func (s *scenarioTaskSystem) ClaimIfOwned(ctx context.Context, ref task.TicketRef, workflowName string, _ config.RawValues) error {
|
|
851
|
+
return s.Claim(ctx, ref, workflowName)
|
|
852
|
+
}
|
|
853
|
+
|
|
842
854
|
func (s *scenarioTaskSystem) Claim(_ context.Context, ref task.TicketRef, workflowName string) error {
|
|
843
855
|
s.log.add("claim:" + ref.Key + ":" + workflowName)
|
|
844
856
|
s.mu.Lock()
|
|
@@ -1200,19 +1212,23 @@ func (h *scenarioHarness) BuildCommand(spec harness.LaunchSpec) (runner.Command,
|
|
|
1200
1212
|
h.sessions[spec.Title] = harness.Session{ID: "session-" + spec.Title, Title: spec.Title}
|
|
1201
1213
|
h.mu.Unlock()
|
|
1202
1214
|
h.log.add("harness-launched:" + spec.Title)
|
|
1215
|
+
env := map[string]string{
|
|
1216
|
+
"RELAY_FLOW_RUN_ID": string(spec.RunID),
|
|
1217
|
+
"RELAY_FLOW_WORKFLOW": spec.Workflow,
|
|
1218
|
+
"RELAY_FLOW_REPO": spec.RepoName,
|
|
1219
|
+
"RELAY_FLOW_TICKET": spec.Ticket,
|
|
1220
|
+
"RELAY_FLOW_NODE": spec.Node,
|
|
1221
|
+
"RELAY_FLOW_NODE_TYPE": string(spec.NodeType),
|
|
1222
|
+
"RELAY_FLOW_NUDGE_PROMPT": spec.NudgePrompt,
|
|
1223
|
+
"RELAY_FLOW_NEXT_STEPS_JSON": routesJSON(spec.NextSteps),
|
|
1224
|
+
}
|
|
1225
|
+
if spec.NodeType == workflow.NodeHITL {
|
|
1226
|
+
env["RELAY_FLOW_AUTO_REJECT"] = fmt.Sprintf("%t", spec.AutoReject)
|
|
1227
|
+
}
|
|
1203
1228
|
return runner.Command{
|
|
1204
1229
|
Executable: "fake-harness",
|
|
1205
1230
|
Args: []string{"opaque", spec.Prompt},
|
|
1206
|
-
Env:
|
|
1207
|
-
"RELAY_FLOW_RUN_ID": string(spec.RunID),
|
|
1208
|
-
"RELAY_FLOW_WORKFLOW": spec.Workflow,
|
|
1209
|
-
"RELAY_FLOW_REPO": spec.RepoName,
|
|
1210
|
-
"RELAY_FLOW_TICKET": spec.Ticket,
|
|
1211
|
-
"RELAY_FLOW_NODE": spec.Node,
|
|
1212
|
-
"RELAY_FLOW_NODE_TYPE": string(spec.NodeType),
|
|
1213
|
-
"RELAY_FLOW_NUDGE_PROMPT": spec.NudgePrompt,
|
|
1214
|
-
"RELAY_FLOW_NEXT_STEPS_JSON": routesJSON(spec.NextSteps),
|
|
1215
|
-
},
|
|
1231
|
+
Env: env,
|
|
1216
1232
|
}, nil
|
|
1217
1233
|
}
|
|
1218
1234
|
|
package/cmd/relay-flow/serve.go
CHANGED
|
@@ -541,6 +541,17 @@ func workflowSubmissionValidator(repoReg *repo.Registry, rnr runner.Runner, hrn
|
|
|
541
541
|
if _, err := rp.TaskSystem.CompileFilter(wf.TaskConfig); err != nil {
|
|
542
542
|
return fmt.Errorf("workflow %q repo %q: compile filter: %w", wf.Name, repoName, err)
|
|
543
543
|
}
|
|
544
|
+
capabilities, ok := rp.TaskSystem.(task.OwnershipCapabilities)
|
|
545
|
+
if !ok {
|
|
546
|
+
return fmt.Errorf("workflow %q repo %q: task system lacks required ownership capabilities", wf.Name, repoName)
|
|
547
|
+
}
|
|
548
|
+
ownership, err := capabilities.CompileOwnershipFilter(wf.TaskConfig)
|
|
549
|
+
if err != nil {
|
|
550
|
+
return fmt.Errorf("workflow %q repo %q: compile ownership filter: %w", wf.Name, repoName, err)
|
|
551
|
+
}
|
|
552
|
+
if ownership == nil {
|
|
553
|
+
return fmt.Errorf("workflow %q repo %q: ownership compiler returned a nil matcher", wf.Name, repoName)
|
|
554
|
+
}
|
|
544
555
|
for nodeName, node := range wf.Nodes {
|
|
545
556
|
if (node.Type != workflow.NodeAgent && node.Type != workflow.NodeHITL) || node.Agent == "" {
|
|
546
557
|
continue
|
|
@@ -650,6 +661,15 @@ func (d *serveDeps) RestartRun(ctx context.Context, ticket string) (runsvc.Run,
|
|
|
650
661
|
func (d *serveDeps) CancelRun(ctx context.Context, ticket, reason string) error {
|
|
651
662
|
return d.runManager.CancelByTicket(ctx, ticket, reason)
|
|
652
663
|
}
|
|
664
|
+
func (d *serveDeps) BackfillClaimOwner(ctx context.Context, repoName, ticket, workflowName string) error {
|
|
665
|
+
if err := d.runManager.BackfillClaimOwner(ctx, repoName, ticket, workflowName); err != nil {
|
|
666
|
+
if errors.Is(err, task.ErrOwnershipMismatch) {
|
|
667
|
+
return fmt.Errorf("%w: %v", server.ErrConflict, err)
|
|
668
|
+
}
|
|
669
|
+
return err
|
|
670
|
+
}
|
|
671
|
+
return nil
|
|
672
|
+
}
|
|
653
673
|
|
|
654
674
|
func (d *serveDeps) SubmitReport(ctx context.Context, rep runsvc.ReportRequest) (runsvc.ReportAck, error) {
|
|
655
675
|
return d.engine.SubmitReport(ctx, rep)
|
|
@@ -720,15 +740,21 @@ func handleBatch(runManager *runsvc.RunManager) repo.BatchHandler {
|
|
|
720
740
|
case err != nil:
|
|
721
741
|
var amb *router.AmbiguousError
|
|
722
742
|
var inv *router.InvalidClaimError
|
|
743
|
+
var ownerMismatch *router.ClaimOwnerMismatchError
|
|
723
744
|
outcome := "error"
|
|
745
|
+
workflowName := ""
|
|
724
746
|
switch {
|
|
725
747
|
case errors.As(err, &amb):
|
|
726
748
|
outcome = "ambiguous"
|
|
727
749
|
case errors.As(err, &inv):
|
|
728
750
|
outcome = "invalid-claim"
|
|
751
|
+
workflowName = inv.Workflow
|
|
752
|
+
case errors.As(err, &ownerMismatch):
|
|
753
|
+
outcome = "claim-owner-mismatch"
|
|
754
|
+
workflowName = ownerMismatch.Workflow
|
|
729
755
|
}
|
|
730
756
|
slog.Info("route outcome",
|
|
731
|
-
"repo", rp.Name, "ticket", ticket.Key, "outcome", outcome, "error", err)
|
|
757
|
+
"repo", rp.Name, "ticket", ticket.Key, "workflow", workflowName, "outcome", outcome, "error", err)
|
|
732
758
|
continue
|
|
733
759
|
}
|
|
734
760
|
routeOutcome := "claimed"
|
|
@@ -73,6 +73,7 @@ type fakeTaskSystem struct {
|
|
|
73
73
|
labels map[string][]string // mailbox key -> labels
|
|
74
74
|
specs []task.MailboxSpec
|
|
75
75
|
comments []recordedComment
|
|
76
|
+
hasCommentN int
|
|
76
77
|
resets []string
|
|
77
78
|
renderText func(task.TextKind, task.TextData) (string, error)
|
|
78
79
|
|
|
@@ -115,6 +116,18 @@ func (s *fakeTaskSystem) CompileFilter(config.RawValues) (func(task.Ticket) bool
|
|
|
115
116
|
return func(task.Ticket) bool { return true }, nil
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
func (s *fakeTaskSystem) CompileOwnershipFilter(config.RawValues) (func(task.Ticket) bool, error) {
|
|
120
|
+
return func(task.Ticket) bool { return true }, nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func (s *fakeTaskSystem) ValidateOwnership(context.Context, task.TicketRef, string, config.RawValues) error {
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func (s *fakeTaskSystem) ClaimIfOwned(ctx context.Context, ref task.TicketRef, workflowName string, _ config.RawValues) error {
|
|
128
|
+
return s.Claim(ctx, ref, workflowName)
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
func (s *fakeTaskSystem) Claim(_ context.Context, ref task.TicketRef, wf string) error {
|
|
119
132
|
s.log.add("claim:" + ref.Key + ":" + wf)
|
|
120
133
|
return nil
|
|
@@ -208,6 +221,7 @@ func (s *fakeTaskSystem) CompleteMailbox(_ context.Context, mb task.Mailbox) err
|
|
|
208
221
|
|
|
209
222
|
func (s *fakeTaskSystem) HasComment(_ context.Context, target task.Target, marker string) (bool, error) {
|
|
210
223
|
s.mu.Lock()
|
|
224
|
+
s.hasCommentN++
|
|
211
225
|
defer s.mu.Unlock()
|
|
212
226
|
for _, c := range s.comments {
|
|
213
227
|
if c.Marker == marker {
|
|
@@ -267,6 +281,12 @@ func (s *fakeTaskSystem) ResetForRecovery(_ context.Context, parent task.TicketR
|
|
|
267
281
|
return nil
|
|
268
282
|
}
|
|
269
283
|
|
|
284
|
+
func (s *fakeTaskSystem) hasCommentCount() int {
|
|
285
|
+
s.mu.Lock()
|
|
286
|
+
defer s.mu.Unlock()
|
|
287
|
+
return s.hasCommentN
|
|
288
|
+
}
|
|
289
|
+
|
|
270
290
|
func (s *fakeTaskSystem) commentBodies(key string) []recordedComment {
|
|
271
291
|
s.mu.Lock()
|
|
272
292
|
defer s.mu.Unlock()
|
|
@@ -292,6 +292,7 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
292
292
|
Ticket: start.Ticket.Key,
|
|
293
293
|
Node: current,
|
|
294
294
|
NodeType: node.Type,
|
|
295
|
+
AutoReject: node.AutoReject,
|
|
295
296
|
Agent: node.Agent,
|
|
296
297
|
Title: title,
|
|
297
298
|
NudgePrompt: node.NudgePrompt,
|