relay-flow 0.2.2-alpha → 0.2.4-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 (77) hide show
  1. package/README.md +154 -13
  2. package/cmd/relay-flow/commands_test.go +5 -1
  3. package/cmd/relay-flow/main.go +27 -1
  4. package/cmd/relay-flow/pi_wiring_test.go +64 -0
  5. package/cmd/relay-flow/serve.go +14 -0
  6. package/examples/beads-workflow.yaml +3 -3
  7. package/examples/config-reference.yaml +144 -0
  8. package/examples/minimal-beads-task-workflow.yaml +34 -0
  9. package/examples/minimal-jira-task-workflow.yaml +68 -0
  10. package/examples/workflow-reference.yaml +111 -0
  11. package/internal/execution/goworkflows/activities.go +19 -0
  12. package/internal/execution/goworkflows/engine.go +20 -0
  13. package/internal/execution/goworkflows/engine_test.go +1 -1
  14. package/internal/execution/goworkflows/fakes_test.go +34 -6
  15. package/internal/execution/goworkflows/interpreter.go +48 -1
  16. package/internal/execution/goworkflows/projection.go +52 -11
  17. package/internal/execution/goworkflows/recovery_test.go +129 -0
  18. package/internal/harness/opencode/opencode.go +4 -4
  19. package/internal/harness/opencode/opencode_test.go +59 -4
  20. package/internal/harness/opencode/repo_setup.go +42 -2
  21. package/internal/harness/pi/config_test.go +46 -0
  22. package/internal/harness/pi/lifecycle_test.go +69 -0
  23. package/internal/harness/pi/pi.go +261 -0
  24. package/internal/harness/pi/pi_test.go +322 -0
  25. package/internal/harness/pi/prompt_test.go +121 -0
  26. package/internal/harness/pi/testdata/pi-0.84.1/capture.json +126 -0
  27. package/internal/harness/pi/testdata/pi-0.84.1/noninteractive-output.txt +11 -0
  28. package/internal/harness/pi/testdata/pi-0.84.1/tui-output-sanitized.txt +17 -0
  29. package/internal/harness/pi/validation_test.go +168 -0
  30. package/internal/identity/identity.go +28 -1
  31. package/internal/identity/identity_test.go +40 -0
  32. package/internal/run/manager.go +193 -25
  33. package/internal/run/run.go +24 -6
  34. package/internal/run/run_manager_test.go +100 -0
  35. package/internal/runner/herdr/baseref.go +60 -0
  36. package/internal/runner/herdr/herdr.go +578 -0
  37. package/internal/runner/herdr/herdr_test.go +688 -0
  38. package/internal/runner/herdr/herdrcli/contract.go +128 -0
  39. package/internal/runner/herdr/herdrcli/exec.go +68 -0
  40. package/internal/runner/herdr/herdrcli/herdrcli_test.go +274 -0
  41. package/internal/runner/herdr/herdrcli/live_test.go +132 -0
  42. package/internal/runner/herdr/herdrcli/operations.go +281 -0
  43. package/internal/runner/herdr/herdrcli/response.go +116 -0
  44. package/internal/runner/herdr/herdrcli/testdata/empty-panes.json +1 -0
  45. package/internal/runner/herdr/herdrcli/testdata/empty-tabs.json +1 -0
  46. package/internal/runner/herdr/herdrcli/testdata/error-not-git-worktree.json +1 -0
  47. package/internal/runner/herdr/herdrcli/testdata/error-pane-not-found.json +1 -0
  48. package/internal/runner/herdr/herdrcli/testdata/error-workspace-not-found.json +1 -0
  49. package/internal/runner/herdr/herdrcli/testdata/error-worktree-not-found.json +1 -0
  50. package/internal/runner/herdr/herdrcli/testdata/malformed.json +1 -0
  51. package/internal/runner/herdr/herdrcli/testdata/pane-close.json +6 -0
  52. package/internal/runner/herdr/herdrcli/testdata/pane-get.json +25 -0
  53. package/internal/runner/herdr/herdrcli/testdata/pane-list.json +45 -0
  54. package/internal/runner/herdr/herdrcli/testdata/pane-process-info-shell.json +22 -0
  55. package/internal/runner/herdr/herdrcli/testdata/pane-process-info.json +23 -0
  56. package/internal/runner/herdr/herdrcli/testdata/pane-rename.json +23 -0
  57. package/internal/runner/herdr/herdrcli/testdata/snapshot.json +213 -0
  58. package/internal/runner/herdr/herdrcli/testdata/strict-herdr.sh +175 -0
  59. package/internal/runner/herdr/herdrcli/testdata/tab-create.json +31 -0
  60. package/internal/runner/herdr/herdrcli/testdata/tab-list.json +26 -0
  61. package/internal/runner/herdr/herdrcli/testdata/workspace-close.json +6 -0
  62. package/internal/runner/herdr/herdrcli/testdata/worktree-create.json +58 -0
  63. package/internal/runner/herdr/herdrcli/testdata/worktree-list.json +35 -0
  64. package/internal/runner/herdr/herdrcli/testdata/worktree-open.json +59 -0
  65. package/internal/server/api_test.go +54 -0
  66. package/internal/server/client.go +11 -0
  67. package/internal/server/fixture_test.go +18 -0
  68. package/internal/server/server.go +16 -0
  69. package/internal/task/beads/beads.go +16 -16
  70. package/internal/task/beads/config_compatibility_test.go +7 -8
  71. package/internal/task/beads/status_compatibility_test.go +43 -0
  72. package/internal/task/jira/filters_test.go +32 -6
  73. package/internal/task/jira/helpers_test.go +3 -0
  74. package/internal/task/jira/jira.go +43 -17
  75. package/internal/task/jira/transition_defaults_test.go +43 -0
  76. package/internal/task/task.go +8 -0
  77. package/package.json +1 -1
@@ -0,0 +1,213 @@
1
+ {
2
+ "id": "cli:api:snapshot",
3
+ "result": {
4
+ "snapshot": {
5
+ "agents": [],
6
+ "focused_pane_id": "w1:p1",
7
+ "focused_tab_id": "w1:t1",
8
+ "focused_workspace_id": "w1",
9
+ "layouts": [
10
+ {
11
+ "area": {
12
+ "height": 39,
13
+ "width": 94,
14
+ "x": 26,
15
+ "y": 1
16
+ },
17
+ "focused_pane_id": "w1:p1",
18
+ "panes": [
19
+ {
20
+ "focused": true,
21
+ "pane_id": "w1:p1",
22
+ "rect": {
23
+ "height": 39,
24
+ "width": 94,
25
+ "x": 26,
26
+ "y": 1
27
+ }
28
+ }
29
+ ],
30
+ "splits": [],
31
+ "tab_id": "w1:t1",
32
+ "workspace_id": "w1",
33
+ "zoomed": false
34
+ },
35
+ {
36
+ "area": {
37
+ "height": 39,
38
+ "width": 94,
39
+ "x": 26,
40
+ "y": 1
41
+ },
42
+ "focused_pane_id": "w2:p1",
43
+ "panes": [
44
+ {
45
+ "focused": true,
46
+ "pane_id": "w2:p1",
47
+ "rect": {
48
+ "height": 39,
49
+ "width": 94,
50
+ "x": 26,
51
+ "y": 1
52
+ }
53
+ }
54
+ ],
55
+ "splits": [],
56
+ "tab_id": "w2:t1",
57
+ "workspace_id": "w2",
58
+ "zoomed": false
59
+ },
60
+ {
61
+ "area": {
62
+ "height": 39,
63
+ "width": 94,
64
+ "x": 26,
65
+ "y": 1
66
+ },
67
+ "focused_pane_id": "w2:p2",
68
+ "panes": [
69
+ {
70
+ "focused": true,
71
+ "pane_id": "w2:p2",
72
+ "rect": {
73
+ "height": 39,
74
+ "width": 94,
75
+ "x": 26,
76
+ "y": 1
77
+ }
78
+ }
79
+ ],
80
+ "splits": [],
81
+ "tab_id": "w2:t2",
82
+ "workspace_id": "w2",
83
+ "zoomed": false
84
+ }
85
+ ],
86
+ "panes": [
87
+ {
88
+ "agent_status": "unknown",
89
+ "cwd": "/work/payments",
90
+ "focused": true,
91
+ "foreground_cwd": "/work/payments",
92
+ "pane_id": "w1:p1",
93
+ "revision": 1,
94
+ "scroll": {
95
+ "max_offset_from_bottom": 0,
96
+ "offset_from_bottom": 0,
97
+ "viewport_rows": 39
98
+ },
99
+ "tab_id": "w1:t1",
100
+ "terminal_id": "term_65a5cfcc8c6361",
101
+ "terminal_title": "relay@relay-host:/work/payments",
102
+ "terminal_title_stripped": "relay@relay-host:/work/payments",
103
+ "workspace_id": "w1"
104
+ },
105
+ {
106
+ "agent_status": "unknown",
107
+ "cwd": "/work/worktrees/payments/pay-101",
108
+ "focused": false,
109
+ "foreground_cwd": "/work/worktrees/payments/pay-101",
110
+ "pane_id": "w2:p1",
111
+ "revision": 1,
112
+ "scroll": {
113
+ "max_offset_from_bottom": 0,
114
+ "offset_from_bottom": 0,
115
+ "viewport_rows": 39
116
+ },
117
+ "tab_id": "w2:t1",
118
+ "terminal_id": "term_65a5cfcc8cd9e2",
119
+ "terminal_title": "relay@relay-host:~/.herdr/worktrees/repo/pay-101",
120
+ "terminal_title_stripped": "relay@relay-host:~/.herdr/worktrees/repo/pay-101",
121
+ "workspace_id": "w2"
122
+ },
123
+ {
124
+ "agent_status": "unknown",
125
+ "cwd": "/work/worktrees/payments/pay-101",
126
+ "focused": false,
127
+ "foreground_cwd": "/work/worktrees/payments/pay-101",
128
+ "label": "PAY-101:coding",
129
+ "pane_id": "w2:p2",
130
+ "revision": 2,
131
+ "scroll": {
132
+ "max_offset_from_bottom": 0,
133
+ "offset_from_bottom": 0,
134
+ "viewport_rows": 39
135
+ },
136
+ "tab_id": "w2:t2",
137
+ "terminal_id": "term_65a5cfcc913e03",
138
+ "terminal_title": "sleep 300",
139
+ "terminal_title_stripped": "sleep 300",
140
+ "workspace_id": "w2"
141
+ }
142
+ ],
143
+ "protocol": 20,
144
+ "tabs": [
145
+ {
146
+ "agent_status": "unknown",
147
+ "focused": true,
148
+ "label": "1",
149
+ "number": 1,
150
+ "pane_count": 1,
151
+ "tab_id": "w1:t1",
152
+ "workspace_id": "w1"
153
+ },
154
+ {
155
+ "agent_status": "unknown",
156
+ "focused": false,
157
+ "label": "1",
158
+ "number": 1,
159
+ "pane_count": 1,
160
+ "tab_id": "w2:t1",
161
+ "workspace_id": "w2"
162
+ },
163
+ {
164
+ "agent_status": "unknown",
165
+ "focused": false,
166
+ "label": "PAY-101:coding",
167
+ "number": 2,
168
+ "pane_count": 1,
169
+ "tab_id": "w2:t2",
170
+ "workspace_id": "w2"
171
+ }
172
+ ],
173
+ "version": "0.8.2",
174
+ "workspaces": [
175
+ {
176
+ "active_tab_id": "w1:t1",
177
+ "agent_status": "unknown",
178
+ "focused": true,
179
+ "label": "repo",
180
+ "number": 1,
181
+ "pane_count": 1,
182
+ "tab_count": 1,
183
+ "workspace_id": "w1",
184
+ "worktree": {
185
+ "checkout_path": "/work/payments",
186
+ "is_linked_worktree": false,
187
+ "repo_key": "/work/payments/.git",
188
+ "repo_name": "repo",
189
+ "repo_root": "/work/payments"
190
+ }
191
+ },
192
+ {
193
+ "active_tab_id": "w2:t1",
194
+ "agent_status": "unknown",
195
+ "focused": false,
196
+ "label": "PAY-101",
197
+ "number": 2,
198
+ "pane_count": 2,
199
+ "tab_count": 2,
200
+ "workspace_id": "w2",
201
+ "worktree": {
202
+ "checkout_path": "/work/worktrees/payments/pay-101",
203
+ "is_linked_worktree": true,
204
+ "repo_key": "/work/payments/.git",
205
+ "repo_name": "repo",
206
+ "repo_root": "/work/payments"
207
+ }
208
+ }
209
+ ]
210
+ },
211
+ "type": "session_snapshot"
212
+ }
213
+ }
@@ -0,0 +1,175 @@
1
+ #!/bin/sh
2
+ # Strict fake `herdr`. It accepts only the exact production command shapes and
3
+ # reproduces the observed transport contract of herdr 0.8.2:
4
+ # success -> result envelope on stdout, exit 0 (pane run prints nothing)
5
+ # failure -> error envelope on stderr, exit 1, empty stdout
6
+ set -eu
7
+
8
+ fail() {
9
+ printf 'unsupported or malformed herdr invocation:' >&2
10
+ for arg in "$@"; do
11
+ printf ' <%s>' "$arg" >&2
12
+ done
13
+ printf '\n' >&2
14
+ exit 64
15
+ }
16
+
17
+ # The wrapper supplies selector configuration through the environment rather
18
+ # than adding session/socket flags to every Herdr command.
19
+ check_selector() {
20
+ actual_name=$1
21
+ expected_name=$2
22
+ eval "expected_set=\${${expected_name}+x}"
23
+ if [ "$expected_set" = x ]; then
24
+ eval "expected=\${${expected_name}-}"
25
+ eval "actual=\${${actual_name}-}"
26
+ [ "$actual" = "$expected" ] || fail "wrong $actual_name: $actual"
27
+ else
28
+ eval "actual_set=\${${actual_name}+x}"
29
+ [ "$actual_set" != x ] || fail "ambient $actual_name leaked"
30
+ fi
31
+ }
32
+
33
+ check_selector HERDR_SESSION HERDR_EXPECT_SESSION
34
+ check_selector HERDR_SOCKET_PATH HERDR_EXPECT_SOCKET_PATH
35
+
36
+ fixture_path() {
37
+ fixture_dir=${HERDR_FIXTURE_DIR-}
38
+ [ -n "$fixture_dir" ] || fail "HERDR_FIXTURE_DIR is unset"
39
+ [ -f "$fixture_dir/$1" ] || fail "missing fixture: $1"
40
+ printf '%s' "$fixture_dir/$1"
41
+ }
42
+
43
+ emit_result() { cat "$(fixture_path "$1")"; exit 0; }
44
+
45
+ # Herdr writes error envelopes to stderr and exits 1.
46
+ emit_error() { cat "$(fixture_path "$1")" >&2; exit 1; }
47
+
48
+ check_absolute_cwd() {
49
+ case "$1" in
50
+ /*) ;;
51
+ *) fail "--cwd must be absolute: $1" ;;
52
+ esac
53
+ }
54
+
55
+ check_value() {
56
+ [ -n "$1" ] || fail "empty positional value"
57
+ case "$1" in
58
+ -*) fail "unexpected flag in positional value: $1" ;;
59
+ esac
60
+ }
61
+
62
+ case "$#:${1-}:${2-}" in
63
+ 2:api:snapshot)
64
+ emit_result snapshot.json
65
+ ;;
66
+ 4:worktree:list)
67
+ [ "$3" = --cwd ] || fail "$@"
68
+ check_absolute_cwd "$4"
69
+ case "$4" in
70
+ *not-a-repo) emit_error error-not-git-worktree.json ;;
71
+ *) emit_result worktree-list.json ;;
72
+ esac
73
+ ;;
74
+ 11:worktree:create)
75
+ [ "$3" = --cwd ] || fail "$@"
76
+ check_absolute_cwd "$4"
77
+ [ "$5" = --branch ] || fail "$@"
78
+ check_value "$6"
79
+ [ "$7" = --base ] || fail "$@"
80
+ check_value "$8"
81
+ [ "$9" = --label ] || fail "$@"
82
+ check_value "${10}"
83
+ [ "${11}" = --no-focus ] || fail "$@"
84
+ emit_result worktree-create.json
85
+ ;;
86
+ 9:worktree:open)
87
+ [ "$3" = --cwd ] || fail "$@"
88
+ check_absolute_cwd "$4"
89
+ [ "$5" = --branch ] || fail "$@"
90
+ check_value "$6"
91
+ [ "$7" = --label ] || fail "$@"
92
+ check_value "$8"
93
+ [ "$9" = --no-focus ] || fail "$@"
94
+ case "$6" in
95
+ MISSING-*) emit_error error-worktree-not-found.json ;;
96
+ esac
97
+ emit_result worktree-open.json
98
+ ;;
99
+ 9:tab:create)
100
+ [ "$3" = --workspace ] || fail "$@"
101
+ check_value "$4"
102
+ [ "$5" = --cwd ] || fail "$@"
103
+ check_absolute_cwd "$6"
104
+ [ "$7" = --label ] || fail "$@"
105
+ check_value "$8"
106
+ [ "$9" = --no-focus ] || fail "$@"
107
+ emit_result tab-create.json
108
+ ;;
109
+ 4:tab:list)
110
+ [ "$3" = --workspace ] || fail "$@"
111
+ check_value "$4"
112
+ case "$4" in
113
+ empty) emit_result empty-tabs.json ;;
114
+ missing) emit_error error-workspace-not-found.json ;;
115
+ *) emit_result tab-list.json ;;
116
+ esac
117
+ ;;
118
+ 4:pane:list)
119
+ [ "$3" = --workspace ] || fail "$@"
120
+ check_value "$4"
121
+ case "$4" in
122
+ empty) emit_result empty-panes.json ;;
123
+ missing) emit_error error-workspace-not-found.json ;;
124
+ *) emit_result pane-list.json ;;
125
+ esac
126
+ ;;
127
+ 3:pane:get)
128
+ check_value "$3"
129
+ case "$3" in
130
+ malformed) emit_result malformed.json ;;
131
+ warning) printf 'captured warning\n' >&2; emit_result pane-get.json ;;
132
+ missing) emit_error error-pane-not-found.json ;;
133
+ *) emit_result pane-get.json ;;
134
+ esac
135
+ ;;
136
+ 4:pane:process-info)
137
+ [ "$3" = --pane ] || fail "$@"
138
+ check_value "$4"
139
+ case "$4" in
140
+ shell) emit_result pane-process-info-shell.json ;;
141
+ missing) emit_error error-pane-not-found.json ;;
142
+ *) emit_result pane-process-info.json ;;
143
+ esac
144
+ ;;
145
+ 4:pane:rename)
146
+ check_value "$3"
147
+ check_value "$4"
148
+ emit_result pane-rename.json
149
+ ;;
150
+ 4:pane:run)
151
+ # COMMAND is deliberately one logical argument: a command containing
152
+ # spaces or newlines must arrive as the same argument the wrapper passed
153
+ # to exec.CommandContext. Herdr prints nothing on success.
154
+ check_value "$3"
155
+ [ -n "$4" ] || fail "empty command"
156
+ exit 0
157
+ ;;
158
+ 3:pane:close)
159
+ check_value "$3"
160
+ case "$3" in
161
+ missing) emit_error error-pane-not-found.json ;;
162
+ *) emit_result pane-close.json ;;
163
+ esac
164
+ ;;
165
+ 3:workspace:close)
166
+ check_value "$3"
167
+ case "$3" in
168
+ missing) emit_error error-workspace-not-found.json ;;
169
+ *) emit_result workspace-close.json ;;
170
+ esac
171
+ ;;
172
+ *)
173
+ fail "$@"
174
+ ;;
175
+ esac
@@ -0,0 +1,31 @@
1
+ {
2
+ "id": "cli:tab:create",
3
+ "result": {
4
+ "root_pane": {
5
+ "agent_status": "unknown",
6
+ "cwd": "/work/worktrees/payments/pay-101",
7
+ "focused": false,
8
+ "foreground_cwd": "/work/worktrees/payments/pay-101",
9
+ "pane_id": "w2:p2",
10
+ "revision": 0,
11
+ "scroll": {
12
+ "max_offset_from_bottom": 0,
13
+ "offset_from_bottom": 0,
14
+ "viewport_rows": 39
15
+ },
16
+ "tab_id": "w2:t2",
17
+ "terminal_id": "term_65a5cfcc913e03",
18
+ "workspace_id": "w2"
19
+ },
20
+ "tab": {
21
+ "agent_status": "unknown",
22
+ "focused": false,
23
+ "label": "PAY-101:coding",
24
+ "number": 2,
25
+ "pane_count": 1,
26
+ "tab_id": "w2:t2",
27
+ "workspace_id": "w2"
28
+ },
29
+ "type": "tab_created"
30
+ }
31
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "cli:tab:list",
3
+ "result": {
4
+ "tabs": [
5
+ {
6
+ "agent_status": "unknown",
7
+ "focused": false,
8
+ "label": "1",
9
+ "number": 1,
10
+ "pane_count": 1,
11
+ "tab_id": "w2:t1",
12
+ "workspace_id": "w2"
13
+ },
14
+ {
15
+ "agent_status": "unknown",
16
+ "focused": false,
17
+ "label": "PAY-101:coding",
18
+ "number": 2,
19
+ "pane_count": 1,
20
+ "tab_id": "w2:t2",
21
+ "workspace_id": "w2"
22
+ }
23
+ ],
24
+ "type": "tab_list"
25
+ }
26
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "cli:workspace:close",
3
+ "result": {
4
+ "type": "ok"
5
+ }
6
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "id": "cli:worktree:create",
3
+ "result": {
4
+ "root_pane": {
5
+ "agent_status": "unknown",
6
+ "cwd": "/work/worktrees/payments/pay-101",
7
+ "focused": false,
8
+ "foreground_cwd": "/work/worktrees/payments/pay-101",
9
+ "pane_id": "w2:p1",
10
+ "revision": 0,
11
+ "scroll": {
12
+ "max_offset_from_bottom": 0,
13
+ "offset_from_bottom": 0,
14
+ "viewport_rows": 40
15
+ },
16
+ "tab_id": "w2:t1",
17
+ "terminal_id": "term_65a5cfcc8cd9e2",
18
+ "workspace_id": "w2"
19
+ },
20
+ "tab": {
21
+ "agent_status": "unknown",
22
+ "focused": false,
23
+ "label": "1",
24
+ "number": 1,
25
+ "pane_count": 1,
26
+ "tab_id": "w2:t1",
27
+ "workspace_id": "w2"
28
+ },
29
+ "type": "worktree_created",
30
+ "workspace": {
31
+ "active_tab_id": "w2:t1",
32
+ "agent_status": "unknown",
33
+ "focused": false,
34
+ "label": "PAY-101",
35
+ "number": 2,
36
+ "pane_count": 1,
37
+ "tab_count": 1,
38
+ "workspace_id": "w2",
39
+ "worktree": {
40
+ "checkout_path": "/work/worktrees/payments/pay-101",
41
+ "is_linked_worktree": true,
42
+ "repo_key": "/work/payments/.git",
43
+ "repo_name": "repo",
44
+ "repo_root": "/work/payments"
45
+ }
46
+ },
47
+ "worktree": {
48
+ "branch": "PAY-101",
49
+ "is_bare": false,
50
+ "is_detached": false,
51
+ "is_linked_worktree": true,
52
+ "is_prunable": false,
53
+ "label": "repo",
54
+ "open_workspace_id": "w2",
55
+ "path": "/work/worktrees/payments/pay-101"
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "id": "cli:worktree:list",
3
+ "result": {
4
+ "source": {
5
+ "repo_key": "/work/payments/.git",
6
+ "repo_name": "repo",
7
+ "repo_root": "/work/payments",
8
+ "source_checkout_path": "/work/payments",
9
+ "source_workspace_id": "w1"
10
+ },
11
+ "type": "worktree_list",
12
+ "worktrees": [
13
+ {
14
+ "branch": "main",
15
+ "is_bare": false,
16
+ "is_detached": false,
17
+ "is_linked_worktree": false,
18
+ "is_prunable": false,
19
+ "label": "repo",
20
+ "open_workspace_id": "w1",
21
+ "path": "/work/payments"
22
+ },
23
+ {
24
+ "branch": "PAY-101",
25
+ "is_bare": false,
26
+ "is_detached": false,
27
+ "is_linked_worktree": true,
28
+ "is_prunable": false,
29
+ "label": "repo",
30
+ "open_workspace_id": "w2",
31
+ "path": "/work/worktrees/payments/pay-101"
32
+ }
33
+ ]
34
+ }
35
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "id": "cli:worktree:open",
3
+ "result": {
4
+ "already_open": true,
5
+ "root_pane": {
6
+ "agent_status": "unknown",
7
+ "cwd": "/work/worktrees/payments/pay-101",
8
+ "focused": false,
9
+ "foreground_cwd": "/work/worktrees/payments/pay-101",
10
+ "pane_id": "w2:p1",
11
+ "revision": 0,
12
+ "scroll": {
13
+ "max_offset_from_bottom": 0,
14
+ "offset_from_bottom": 0,
15
+ "viewport_rows": 39
16
+ },
17
+ "tab_id": "w2:t1",
18
+ "terminal_id": "term_65a5cfcc8cd9e2",
19
+ "workspace_id": "w2"
20
+ },
21
+ "tab": {
22
+ "agent_status": "unknown",
23
+ "focused": false,
24
+ "label": "1",
25
+ "number": 1,
26
+ "pane_count": 1,
27
+ "tab_id": "w2:t1",
28
+ "workspace_id": "w2"
29
+ },
30
+ "type": "worktree_opened",
31
+ "workspace": {
32
+ "active_tab_id": "w2:t1",
33
+ "agent_status": "unknown",
34
+ "focused": false,
35
+ "label": "PAY-101",
36
+ "number": 2,
37
+ "pane_count": 1,
38
+ "tab_count": 1,
39
+ "workspace_id": "w2",
40
+ "worktree": {
41
+ "checkout_path": "/work/worktrees/payments/pay-101",
42
+ "is_linked_worktree": true,
43
+ "repo_key": "/work/payments/.git",
44
+ "repo_name": "repo",
45
+ "repo_root": "/work/payments"
46
+ }
47
+ },
48
+ "worktree": {
49
+ "branch": "PAY-101",
50
+ "is_bare": false,
51
+ "is_detached": false,
52
+ "is_linked_worktree": true,
53
+ "is_prunable": false,
54
+ "label": "repo",
55
+ "open_workspace_id": "w2",
56
+ "path": "/work/worktrees/payments/pay-101"
57
+ }
58
+ }
59
+ }
@@ -3,8 +3,10 @@ package server_test
3
3
  import (
4
4
  "bytes"
5
5
  "encoding/json"
6
+ "fmt"
6
7
  "io"
7
8
  "net/http"
9
+ "strings"
8
10
  "testing"
9
11
  "time"
10
12
 
@@ -211,6 +213,58 @@ func TestRepoAndRunEndpoints(t *testing.T) {
211
213
  }
212
214
  }
213
215
 
216
+ func TestRunRestartEndpointReturnsFreshAttempt(t *testing.T) {
217
+ want := run.Run{
218
+ ID: "payments/basicFlow/PAY-101~attempt~2", LogicalID: "payments/basicFlow/PAY-101", AttemptID: 2,
219
+ Repo: "payments", Workflow: "basicFlow", Ticket: task.TicketRef{Key: "PAY-101"}, State: run.StateStarting,
220
+ }
221
+ fake := &fakeServices{restartRun: want}
222
+ c, cleanup := startHandler(t, fake)
223
+ defer cleanup()
224
+
225
+ code, env := do(t, c, http.MethodPost, "http://relay/runs/by-ticket/PAY-101/restart", nil)
226
+ if code != http.StatusOK || !env.OK {
227
+ t.Fatalf("POST restart: code=%d env=%+v", code, env)
228
+ }
229
+ if !bytes.Contains(env.Data, []byte(`"attemptId":2`)) ||
230
+ !bytes.Contains(env.Data, []byte(`"logicalRunId":"payments/basicFlow/PAY-101"`)) {
231
+ t.Fatalf("restart response omitted attempt identity: %s", env.Data)
232
+ }
233
+ if len(fake.restarts) != 1 || fake.restarts[0] != "PAY-101" {
234
+ t.Fatalf("restart calls = %v", fake.restarts)
235
+ }
236
+ }
237
+
238
+ func TestRunRestartConflictMapsTo409(t *testing.T) {
239
+ fake := &fakeServices{restartErr: fmt.Errorf("%w: ticket is still canceling", run.ErrRestartConflict)}
240
+ c, cleanup := startHandler(t, fake)
241
+ defer cleanup()
242
+ code, env := do(t, c, http.MethodPost, "http://relay/runs/by-ticket/PAY-101/restart", nil)
243
+ if code != http.StatusConflict || env.OK || env.Error == nil || !strings.Contains(env.Error.Message, "ticket is still canceling") {
244
+ t.Fatalf("restart conflict: code=%d env=%+v", code, env)
245
+ }
246
+ }
247
+
248
+ func TestBlockedRunGetShowsStatusAction(t *testing.T) {
249
+ blocked := run.Run{
250
+ ID: "payments/basicFlow/PAY-101~attempt~2", LogicalID: "payments/basicFlow/PAY-101", AttemptID: 2,
251
+ State: run.StateBlocked, CurrentNode: "start",
252
+ Ticket: task.TicketRef{Key: "PAY-101"},
253
+ LastError: "Human-owned ticket status \"Blocked\" conflicts with the workflow start transition. Move ticket PAY-101 to an allowed active start status; relay-flow will retry automatically",
254
+ }
255
+ c, cleanup := startHandler(t, &fakeServices{runs: []run.Run{blocked}})
256
+ defer cleanup()
257
+ code, env := do(t, c, http.MethodGet, "http://relay/runs/by-ticket/PAY-101", nil)
258
+ if code != http.StatusOK || !env.OK {
259
+ t.Fatalf("GET blocked run: code=%d env=%+v", code, env)
260
+ }
261
+ for _, want := range []string{`"state":"blocked"`, `"currentNode":"start"`, `allowed active start status`, `automatically`} {
262
+ if !bytes.Contains(env.Data, []byte(want)) {
263
+ t.Fatalf("blocked run response missing %q: %s", want, env.Data)
264
+ }
265
+ }
266
+ }
267
+
214
268
  func TestRunEndpointsExposeRetryDetails(t *testing.T) {
215
269
  next := time.Now().UTC().Add(time.Minute)
216
270
  retrying := run.Run{