relay-flow 0.0.1 → 0.2.0-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 (116) hide show
  1. package/README.md +148 -143
  2. package/cmd/relay-flow/commands_test.go +464 -0
  3. package/cmd/relay-flow/main.go +670 -180
  4. package/cmd/relay-flow/scenario_test.go +1135 -0
  5. package/cmd/relay-flow/serve.go +609 -0
  6. package/go.mod +69 -2
  7. package/go.sum +185 -0
  8. package/internal/config/config.go +88 -0
  9. package/internal/config/machine.go +99 -48
  10. package/internal/config/machine_test.go +248 -0
  11. package/internal/config/merge_test.go +118 -0
  12. package/internal/config/writeatomic.go +36 -0
  13. package/internal/config/writeatomic_test.go +98 -0
  14. package/internal/execution/goworkflows/activities.go +490 -0
  15. package/internal/execution/goworkflows/engine.go +487 -0
  16. package/internal/execution/goworkflows/engine_test.go +600 -0
  17. package/internal/execution/goworkflows/fakes_test.go +517 -0
  18. package/internal/execution/goworkflows/interpreter.go +605 -0
  19. package/internal/execution/goworkflows/logging_test.go +154 -0
  20. package/internal/execution/goworkflows/mailbox_test.go +423 -0
  21. package/internal/execution/goworkflows/node_runtime_integration_test.go +127 -0
  22. package/internal/execution/goworkflows/node_runtime_test.go +486 -0
  23. package/internal/execution/goworkflows/projection.go +504 -0
  24. package/internal/execution/goworkflows/recovery_test.go +1092 -0
  25. package/internal/execution/goworkflows/retry_log_test.go +59 -0
  26. package/internal/execution/goworkflows/retry_projection_test.go +98 -0
  27. package/internal/harness/contract_test.go +169 -0
  28. package/internal/harness/factory.go +63 -0
  29. package/internal/harness/harness.go +41 -0
  30. package/internal/harness/opencode/opencode.go +166 -0
  31. package/internal/harness/opencode/opencode_test.go +50 -0
  32. package/internal/harness/plugin_selection_test.go +126 -0
  33. package/internal/identity/identity.go +37 -0
  34. package/internal/logging/logging.go +56 -0
  35. package/internal/logging/logging_test.go +116 -0
  36. package/internal/paths/paths.go +67 -0
  37. package/internal/recover/recover.go +115 -0
  38. package/internal/repo/poller.go +186 -0
  39. package/internal/repo/poller_test.go +327 -0
  40. package/internal/repo/repo.go +119 -0
  41. package/internal/repo/service.go +216 -0
  42. package/internal/repo/service_test.go +298 -0
  43. package/internal/retry/retry.go +118 -0
  44. package/internal/router/router.go +83 -0
  45. package/internal/router/router_test.go +144 -0
  46. package/internal/run/manager.go +108 -0
  47. package/internal/run/run.go +140 -0
  48. package/internal/run/run_identity_test.go +52 -0
  49. package/internal/run/run_manager_test.go +266 -0
  50. package/internal/runner/contract_test.go +221 -0
  51. package/internal/runner/factory.go +65 -0
  52. package/internal/runner/orca/orca.go +363 -170
  53. package/internal/runner/orca/orca_test.go +134 -160
  54. package/internal/runner/orca/orcacli/orcacli.go +215 -0
  55. package/internal/runner/orca/orcacli/orcacli_test.go +154 -0
  56. package/internal/runner/orca/orcacli/testdata/repo-list.json +18 -0
  57. package/internal/runner/orca/orcacli/testdata/strict-orca.sh +30 -0
  58. package/internal/runner/orca/orcacli/testdata/terminal-close.json +12 -0
  59. package/internal/runner/orca/orcacli/testdata/terminal-create.json +18 -0
  60. package/internal/runner/orca/orcacli/testdata/terminal-list.json +51 -0
  61. package/internal/runner/orca/orcacli/testdata/terminal-send.json +1 -0
  62. package/internal/runner/orca/orcacli/testdata/terminal-show.json +1 -0
  63. package/internal/runner/orca/orcacli/testdata/worktree-create.json +22 -0
  64. package/internal/runner/orca/orcacli/testdata/worktree-list.json +31 -0
  65. package/internal/runner/orca/orcacli/testdata/worktree-remove.json +6 -0
  66. package/internal/runner/runner.go +47 -64
  67. package/internal/server/api_test.go +300 -0
  68. package/internal/server/client.go +192 -74
  69. package/internal/server/fixture_test.go +248 -0
  70. package/internal/server/server.go +425 -248
  71. package/internal/server/shutdown_test.go +116 -0
  72. package/internal/task/contract_test.go +223 -0
  73. package/internal/task/factory.go +103 -0
  74. package/internal/task/jira/acli/acli.go +306 -0
  75. package/internal/task/jira/acli/acli_test.go +208 -0
  76. package/internal/task/jira/acli/testdata/acli_comments.json +55 -0
  77. package/internal/task/jira/acli/testdata/search_invalid_assignee.txt +1 -0
  78. package/internal/task/jira/acli/testdata/search_invalid_status.txt +1 -0
  79. package/internal/task/jira/acli/testdata/search_success.json +1 -0
  80. package/internal/task/jira/filters_test.go +234 -0
  81. package/internal/task/jira/helpers_test.go +60 -0
  82. package/internal/task/jira/jira.go +507 -0
  83. package/internal/task/jira/normalize.go +101 -0
  84. package/internal/task/jira/testdata/acli_search.json +120 -0
  85. package/internal/task/jira/transition_defaults_test.go +156 -0
  86. package/internal/task/jira/validation_test.go +94 -0
  87. package/internal/task/task.go +84 -0
  88. package/internal/workflow/report.go +85 -0
  89. package/internal/workflow/report_test.go +259 -0
  90. package/internal/workflow/service.go +142 -0
  91. package/internal/workflow/store.go +136 -0
  92. package/internal/workflow/store_test.go +282 -0
  93. package/internal/workflow/workflow.go +342 -0
  94. package/internal/workflow/workflow_test.go +410 -0
  95. package/package.json +1 -1
  96. package/internal/acli/acli.go +0 -229
  97. package/internal/config/demo_test.go +0 -17
  98. package/internal/config/schema.go +0 -193
  99. package/internal/config/schema_test.go +0 -162
  100. package/internal/daemon/daemon.go +0 -218
  101. package/internal/daemon/daemon_test.go +0 -204
  102. package/internal/discovery/discovery.go +0 -122
  103. package/internal/discovery/discovery_test.go +0 -62
  104. package/internal/opencode/opencode.go +0 -26
  105. package/internal/orcacli/orcacli.go +0 -264
  106. package/internal/runner/orca/README.md +0 -64
  107. package/internal/runner/runner_test.go +0 -64
  108. package/internal/server/server_test.go +0 -195
  109. package/internal/tasks/jira/README.md +0 -69
  110. package/internal/tasks/jira/component_test.go +0 -16
  111. package/internal/tasks/jira/decode.go +0 -24
  112. package/internal/tasks/jira/jira.go +0 -231
  113. package/internal/tasks/jira/jira_test.go +0 -259
  114. package/internal/tasks/jira/jql_test.go +0 -16
  115. package/internal/tasks/tasks.go +0 -90
  116. package/internal/tasks/tasks_test.go +0 -91
@@ -0,0 +1,154 @@
1
+ package orcacli
2
+
3
+ import (
4
+ "context"
5
+ "os"
6
+ "os/exec"
7
+ "path/filepath"
8
+ "strings"
9
+ "testing"
10
+ )
11
+
12
+ func TestCLIContractsAgainstCapturedRealOutput(t *testing.T) {
13
+ installStrictFakeOrca(t)
14
+ ctx := context.Background()
15
+ cli := New()
16
+
17
+ repos, err := cli.ListRepos(ctx)
18
+ if err != nil {
19
+ t.Fatal(err)
20
+ }
21
+ if len(repos) != 1 || repos[0].ID != "repo-1" || repos[0].DisplayName != "app" || repos[0].Path != "/srv/app" {
22
+ t.Fatalf("ListRepos = %+v", repos)
23
+ }
24
+
25
+ worktrees, err := cli.ListWorktrees(ctx)
26
+ if err != nil {
27
+ t.Fatal(err)
28
+ }
29
+ if len(worktrees) != 1 || worktrees[0].ID != "wt-main" || worktrees[0].Branch != "refs/heads/master" || !worktrees[0].IsMainWorktree {
30
+ t.Fatalf("ListWorktrees = %+v", worktrees)
31
+ }
32
+
33
+ if err := cli.CreateWorktree(ctx, "PAY-101", "repo-1", "wt-main", "origin/alice/PAY-101"); err != nil {
34
+ t.Fatal(err)
35
+ }
36
+ if err := cli.DeleteWorktree(ctx, "wt-PAY-101"); err != nil {
37
+ t.Fatal(err)
38
+ }
39
+
40
+ terminals, err := cli.ListTerminals(ctx, "id:wt-PAY-101")
41
+ if err != nil {
42
+ t.Fatal(err)
43
+ }
44
+ if len(terminals) != 1 {
45
+ t.Fatalf("ListTerminals = %+v", terminals)
46
+ }
47
+ if got := terminals[0]; got.Handle != "term-1" || got.Title != "PAY-101:implement" || !got.Connected {
48
+ t.Fatalf("ListTerminals[0] = %+v", got)
49
+ }
50
+ if terminals[0].Title == "opencode" {
51
+ t.Fatal("ListTerminals used mutable pane title instead of stable visualLayouts tab title")
52
+ }
53
+ direct, err := cli.ShowTerminal(ctx, "term-1")
54
+ if err != nil || direct.Handle != "term-1" || !direct.Connected {
55
+ t.Fatalf("ShowTerminal = %+v, %v", direct, err)
56
+ }
57
+ if err := cli.SendTerminal(ctx, "term-1", "hello"); err != nil {
58
+ t.Fatal(err)
59
+ }
60
+
61
+ handle, err := cli.CreateTerminal(ctx, "PAY-101", "PAY-101:implement", "echo hello")
62
+ if err != nil {
63
+ t.Fatal(err)
64
+ }
65
+ if handle != "term-1" {
66
+ t.Fatalf("CreateTerminal handle = %q", handle)
67
+ }
68
+ if err := cli.CloseTerminal(ctx, handle); err != nil {
69
+ t.Fatal(err)
70
+ }
71
+ }
72
+
73
+ func TestStrictFakeOrcaRejectsMissingFlags(t *testing.T) {
74
+ fake := installStrictFakeOrca(t)
75
+ if err := exec.Command(fake, "terminal", "list", "--worktree", "id:wt-PAY-101", "--json").Run(); err == nil {
76
+ t.Fatal("strict fake accepted terminal list without --include-visual-layouts")
77
+ }
78
+ }
79
+
80
+ func TestFindExistingBranchLocalOnly(t *testing.T) {
81
+ repo := newGitRepo(t)
82
+ runGit(t, repo, "branch", "alice/PAY-101")
83
+
84
+ got, ok, err := FindExistingBranch(repo, "PAY-101")
85
+ if err != nil {
86
+ t.Fatal(err)
87
+ }
88
+ if !ok || got != "alice/PAY-101" {
89
+ t.Fatalf("FindExistingBranch = %q, %v; want alice/PAY-101, true", got, ok)
90
+ }
91
+ }
92
+
93
+ func TestFindExistingBranchRemoteOnly(t *testing.T) {
94
+ repo := newGitRepo(t)
95
+ runGit(t, repo, "update-ref", "refs/remotes/origin/alice/PAY-102", "HEAD")
96
+
97
+ got, ok, err := FindExistingBranch(repo, "PAY-102")
98
+ if err != nil {
99
+ t.Fatal(err)
100
+ }
101
+ if !ok || got != "origin/alice/PAY-102" {
102
+ t.Fatalf("FindExistingBranch = %q, %v; want origin/alice/PAY-102, true", got, ok)
103
+ }
104
+ }
105
+
106
+ func TestFindExistingBranchMissing(t *testing.T) {
107
+ repo := newGitRepo(t)
108
+ runGit(t, repo, "branch", "alice/OTHER-1")
109
+
110
+ got, ok, err := FindExistingBranch(repo, "PAY-103")
111
+ if err != nil {
112
+ t.Fatal(err)
113
+ }
114
+ if ok || got != "" {
115
+ t.Fatalf("FindExistingBranch = %q, %v; want empty, false", got, ok)
116
+ }
117
+ }
118
+
119
+ func newGitRepo(t *testing.T) string {
120
+ t.Helper()
121
+ repo := t.TempDir()
122
+ runGit(t, repo, "init", "--quiet")
123
+ runGit(t, repo, "-c", "user.name=Relay Flow", "-c", "user.email=relay-flow@example.invalid", "commit", "--allow-empty", "--quiet", "-m", "initial")
124
+ return repo
125
+ }
126
+
127
+ func runGit(t *testing.T, repo string, args ...string) {
128
+ t.Helper()
129
+ cmdArgs := append([]string{"-C", repo}, args...)
130
+ out, err := exec.Command("git", cmdArgs...).CombinedOutput()
131
+ if err != nil {
132
+ t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, out)
133
+ }
134
+ }
135
+
136
+ func installStrictFakeOrca(t *testing.T) string {
137
+ t.Helper()
138
+ binDir := t.TempDir()
139
+ fake := filepath.Join(binDir, "orca")
140
+ script, err := os.ReadFile(filepath.Join("testdata", "strict-orca.sh"))
141
+ if err != nil {
142
+ t.Fatal(err)
143
+ }
144
+ if err := os.WriteFile(fake, script, 0o755); err != nil {
145
+ t.Fatal(err)
146
+ }
147
+ fixtureDir, err := filepath.Abs("testdata")
148
+ if err != nil {
149
+ t.Fatal(err)
150
+ }
151
+ t.Setenv("ORCA_FIXTURE_DIR", fixtureDir)
152
+ t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
153
+ return fake
154
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "captured-repo-list",
3
+ "ok": true,
4
+ "result": {
5
+ "repos": [
6
+ {
7
+ "id": "repo-1",
8
+ "path": "/srv/app",
9
+ "displayName": "app",
10
+ "kind": "git",
11
+ "externalWorktreeVisibility": "hide",
12
+ "upstream": null,
13
+ "gitRemoteIdentity": null
14
+ }
15
+ ]
16
+ },
17
+ "_meta": {"runtimeId": "runtime-1"}
18
+ }
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ fixture=
5
+ if [ "$#" -eq 3 ] && [ "$1" = repo ] && [ "$2" = list ] && [ "$3" = --json ]; then
6
+ fixture=repo-list.json
7
+ elif [ "$#" -eq 3 ] && [ "$1" = worktree ] && [ "$2" = list ] && [ "$3" = --json ]; then
8
+ fixture=worktree-list.json
9
+ elif [ "$#" -eq 11 ] && [ "$1" = worktree ] && [ "$2" = create ] && [ "$3" = --name ] && [ "$4" = PAY-101 ] && [ "$5" = --repo ] && [ "$6" = id:repo-1 ] && [ "$7" = --parent-worktree ] && [ "$8" = worktree:wt-main ] && [ "$9" = --base-branch ] && [ "${10}" = origin/alice/PAY-101 ] && [ "${11}" = --json ]; then
10
+ fixture=worktree-create.json
11
+ elif [ "$#" -eq 5 ] && [ "$1" = worktree ] && [ "$2" = rm ] && [ "$3" = --worktree ] && [ "$4" = id:wt-PAY-101 ] && [ "$5" = --json ]; then
12
+ fixture=worktree-remove.json
13
+ elif [ "$#" -eq 6 ] && [ "$1" = terminal ] && [ "$2" = list ] && [ "$3" = --worktree ] && [ "$4" = id:wt-PAY-101 ] && [ "$5" = --include-visual-layouts ] && [ "$6" = --json ]; then
14
+ fixture=terminal-list.json
15
+ elif [ "$#" -eq 5 ] && [ "$1" = terminal ] && [ "$2" = show ] && [ "$3" = --terminal ] && [ "$4" = term-1 ] && [ "$5" = --json ]; then
16
+ fixture=terminal-show.json
17
+ elif [ "$#" -eq 8 ] && [ "$1" = terminal ] && [ "$2" = send ] && [ "$3" = --terminal ] && [ "$4" = term-1 ] && [ "$5" = --text ] && [ "$6" = 'hello' ] && [ "$7" = --enter ] && [ "$8" = --json ]; then
18
+ fixture=terminal-send.json
19
+ elif [ "$#" -eq 9 ] && [ "$1" = terminal ] && [ "$2" = create ] && [ "$3" = --worktree ] && [ "$4" = name:PAY-101 ] && [ "$5" = --title ] && [ "$6" = PAY-101:implement ] && [ "$7" = --command ] && [ "$8" = 'echo hello' ] && [ "$9" = --json ]; then
20
+ fixture=terminal-create.json
21
+ elif [ "$#" -eq 5 ] && [ "$1" = terminal ] && [ "$2" = close ] && [ "$3" = --terminal ] && [ "$4" = term-1 ] && [ "$5" = --json ]; then
22
+ fixture=terminal-close.json
23
+ else
24
+ printf 'unsupported or malformed orca invocation:' >&2
25
+ printf ' <%s>' "$@" >&2
26
+ printf '\n' >&2
27
+ exit 64
28
+ fi
29
+
30
+ cat "$ORCA_FIXTURE_DIR/$fixture"
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "captured-terminal-close",
3
+ "ok": true,
4
+ "result": {
5
+ "close": {
6
+ "handle": "term-1",
7
+ "tabId": "tab-1",
8
+ "ptyKilled": true
9
+ }
10
+ },
11
+ "_meta": {"runtimeId": "runtime-1"}
12
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "captured-terminal-create",
3
+ "ok": true,
4
+ "result": {
5
+ "terminal": {
6
+ "handle": "term-1",
7
+ "tabId": "tab-1",
8
+ "paneKey": "tab-1:leaf-1",
9
+ "ptyId": "wt-PAY-101@@pty-1",
10
+ "worktreeId": "wt-PAY-101",
11
+ "title": "PAY-101:implement",
12
+ "executionHostId": "local",
13
+ "hostPlatform": "linux",
14
+ "surface": "visible"
15
+ }
16
+ },
17
+ "_meta": {"runtimeId": "runtime-1"}
18
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "id": "captured-terminal-list",
3
+ "ok": true,
4
+ "result": {
5
+ "terminals": [
6
+ {
7
+ "handle": "term-1",
8
+ "ptyId": "wt-PAY-101@@pty-1",
9
+ "worktreeId": "wt-PAY-101",
10
+ "worktreePath": "/srv/worktrees/PAY-101",
11
+ "branch": "refs/heads/alice/PAY-101",
12
+ "tabId": "tab-1",
13
+ "leafId": "leaf-1",
14
+ "title": "opencode",
15
+ "connected": true,
16
+ "writable": true,
17
+ "preview": ""
18
+ }
19
+ ],
20
+ "visualLayouts": [
21
+ {
22
+ "worktreeId": "wt-PAY-101",
23
+ "worktreePath": "/srv/worktrees/PAY-101",
24
+ "root": {
25
+ "type": "group",
26
+ "groupId": "group-1",
27
+ "activeTabId": "tab-1",
28
+ "tabs": [
29
+ {
30
+ "tabId": "tab-1",
31
+ "title": "PAY-101:implement",
32
+ "activeLeafId": "leaf-1",
33
+ "panes": {
34
+ "type": "terminal",
35
+ "handle": "term-1",
36
+ "tabId": "tab-1",
37
+ "leafId": "leaf-1",
38
+ "title": "opencode",
39
+ "connected": true,
40
+ "active": true
41
+ }
42
+ }
43
+ ]
44
+ }
45
+ }
46
+ ],
47
+ "totalCount": 1,
48
+ "truncated": false
49
+ },
50
+ "_meta": {"runtimeId": "runtime-1"}
51
+ }
@@ -0,0 +1 @@
1
+ {"ok":true,"result":{}}
@@ -0,0 +1 @@
1
+ {"ok":true,"result":{"terminal":{"handle":"term-1","title":"PAY-101:implement","connected":true,"writable":true}}}
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "captured-worktree-create",
3
+ "ok": true,
4
+ "result": {
5
+ "worktree": {
6
+ "id": "wt-PAY-101",
7
+ "instanceId": "instance-PAY-101",
8
+ "repoId": "repo-1",
9
+ "path": "/srv/worktrees/PAY-101",
10
+ "head": "0123456789abcdef",
11
+ "branch": "refs/heads/alice/PAY-101",
12
+ "isBare": false,
13
+ "isMainWorktree": false,
14
+ "displayName": "PAY-101",
15
+ "baseRef": "origin/alice/PAY-101",
16
+ "parentWorktreeId": "wt-main",
17
+ "childWorktreeIds": []
18
+ },
19
+ "warnings": []
20
+ },
21
+ "_meta": {"runtimeId": "runtime-1"}
22
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "id": "captured-worktree-list",
3
+ "ok": true,
4
+ "result": {
5
+ "worktrees": [
6
+ {
7
+ "id": "wt-main",
8
+ "instanceId": "instance-main",
9
+ "repoId": "repo-1",
10
+ "path": "/srv/app",
11
+ "head": "0123456789abcdef",
12
+ "branch": "refs/heads/master",
13
+ "isBare": false,
14
+ "isMainWorktree": true,
15
+ "displayName": "master",
16
+ "parentWorktreeId": null,
17
+ "childWorktreeIds": [],
18
+ "git": {
19
+ "path": "/srv/app",
20
+ "head": "0123456789abcdef",
21
+ "branch": "refs/heads/master",
22
+ "isBare": false,
23
+ "isMainWorktree": true
24
+ }
25
+ }
26
+ ],
27
+ "totalCount": 1,
28
+ "truncated": false
29
+ },
30
+ "_meta": {"runtimeId": "runtime-1"}
31
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "captured-worktree-remove",
3
+ "ok": true,
4
+ "result": {"removed": true},
5
+ "_meta": {"runtimeId": "runtime-1"}
6
+ }
@@ -1,81 +1,64 @@
1
- // Package runner defines the execution-backend plug point. Orca ships
2
- // built in (internal/runner/orca); tmux or others register the same way.
3
- // A Runner knows how to spawn agent sessions, re-find them (bounce),
4
- // nudge them, and tear them down — nothing about trackers.
1
+ // Package runner defines the runner contract: workspaces/environments,
2
+ // terminals, liveness, and process handles. The runner does not know
3
+ // task-system fields, workflow routes, report contents, or agent command
4
+ // syntax.
5
5
  package runner
6
6
 
7
7
  import (
8
- "fmt"
9
- "sort"
10
- "sync"
8
+ "context"
9
+ "errors"
11
10
 
12
- "github.com/rajpopat27/relay-flow/internal/tasks"
11
+ "github.com/rajpopat27/relay-flow/internal/identity"
13
12
  )
14
13
 
15
- // Session is a live agent terminal/process handle.
16
- type Session struct {
17
- ID string
18
- Title string
19
- }
14
+ var ErrSessionUnavailable = errors.New("stored session unavailable")
20
15
 
21
- // Runner is the execution-backend interface.
22
- type Runner interface {
23
- // Spawn creates (or ensures) the ticket's worktree/session titled
24
- // "<key>:<agent>:<node>", injects env, and sends the initial prompt.
25
- Spawn(t tasks.Ticket, node, agent, prompt string, env map[string]string) error
26
- // Find locates the existing session for this ticket+node (bounce).
27
- Find(t tasks.Ticket, node string) (Session, bool, error)
28
- // Nudge sends a prompt into an existing session.
29
- Nudge(s Session, prompt string) error
30
- // Close tears down all of the ticket's sessions (terminal node).
31
- Close(t tasks.Ticket) error
16
+ type RepoCandidate struct {
17
+ Name string `json:"name"`
18
+ Path string `json:"path"`
32
19
  }
33
20
 
34
- // Factory builds a runner instance. runner.config is decoded by
35
- // UnmarshalConfig (strict), then passed to New.
36
- type Factory struct {
37
- UnmarshalConfig func(map[string]any) (any, error)
38
- New func(cfg any) (Runner, error)
21
+ type Environment struct {
22
+ ID string `json:"id"`
23
+ Path string `json:"path"`
39
24
  }
40
25
 
41
- var (
42
- mu sync.RWMutex
43
- factories = map[string]Factory{}
44
- )
26
+ type Terminal struct {
27
+ ID string `json:"id"`
28
+ Title string `json:"title"`
29
+ }
45
30
 
46
- // Register makes a runner available under `type: <name>`. Panics on
47
- // duplicate registration (programmer error).
48
- func Register(name string, f Factory) {
49
- mu.Lock()
50
- defer mu.Unlock()
51
- if _, dup := factories[name]; dup {
52
- panic("runner: duplicate registration " + name)
53
- }
54
- factories[name] = f
31
+ type Command struct {
32
+ Executable string `json:"executable"`
33
+ Args []string `json:"args,omitempty"`
34
+ Env map[string]string `json:"env,omitempty"`
55
35
  }
56
36
 
57
- // New resolves a runner by type name and builds an instance.
58
- func New(typeName string, rawCfg map[string]any) (Runner, error) {
59
- mu.RLock()
60
- f, ok := factories[typeName]
61
- mu.RUnlock()
62
- if !ok {
63
- return nil, fmt.Errorf("unknown runner type %q (registered: %v)", typeName, registered())
64
- }
65
- cfg, err := f.UnmarshalConfig(rawCfg)
66
- if err != nil {
67
- return nil, fmt.Errorf("runner type %q config: %w", typeName, err)
68
- }
69
- return f.New(cfg)
37
+ type RunSpec struct {
38
+ RunID identity.RunID
39
+ RepoName string
40
+ RepoPath string
41
+ TicketKey string
70
42
  }
71
43
 
72
- func registered() []string {
73
- mu.RLock()
74
- defer mu.RUnlock()
75
- names := make([]string, 0, len(factories))
76
- for n := range factories {
77
- names = append(names, n)
78
- }
79
- sort.Strings(names)
80
- return names
44
+ // Runner owns ticket-scoped environments, terminals, and liveness.
45
+ // FindTerminal returns only a live usable terminal. CloseTerminals closes
46
+ // agent terminals while preserving the environment/workspace; CleanupRun
47
+ // removes all runner-owned run resources at end. Both reconstruct identity
48
+ // from RunSpec without SQLite IDs.
49
+ //
50
+ // Terminal titles are stable and minimal: only "<ticket>:<node>" — never
51
+ // nodeVisitID, workflow name, agent name, or other changing metadata.
52
+ type Runner interface {
53
+ DiscoverRepos(ctx context.Context) ([]RepoCandidate, error)
54
+ ValidateRepo(ctx context.Context, name, path string) error
55
+ EnsureEnvironment(ctx context.Context, spec RunSpec) (Environment, error)
56
+ InspectTerminal(ctx context.Context, terminal Terminal) (Terminal, bool, error)
57
+ SendTerminal(ctx context.Context, terminal Terminal, text string) error
58
+ CreateTerminal(ctx context.Context, env Environment, title string, command Command) (Terminal, error)
59
+ FindTerminal(ctx context.Context, env Environment, title string) (Terminal, bool, error)
60
+ CloseTerminal(ctx context.Context, terminal Terminal) error
61
+ EnsureTerminal(ctx context.Context, env Environment, title string, command Command) (Terminal, error)
62
+ CloseTerminals(ctx context.Context, spec RunSpec) error
63
+ CleanupRun(ctx context.Context, spec RunSpec) error
81
64
  }