relay-flow 0.2.1-alpha → 0.2.2-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 +155 -22
- package/cmd/relay-flow/beads_composition_test.go +451 -0
- package/cmd/relay-flow/commands_test.go +74 -0
- package/cmd/relay-flow/main.go +25 -0
- package/cmd/relay-flow/scenario_test.go +48 -12
- package/cmd/relay-flow/serve.go +4 -2
- package/examples/beads-workflow.yaml +74 -0
- package/examples/default-story-workflow.yaml +6 -6
- package/go.mod +2 -1
- package/go.sum +2 -0
- package/internal/config/config.go +13 -2
- package/internal/config/merge_test.go +18 -0
- package/internal/execution/goworkflows/activities.go +105 -56
- package/internal/execution/goworkflows/end_feedback_test.go +124 -0
- package/internal/execution/goworkflows/engine_test.go +33 -15
- package/internal/execution/goworkflows/fakes_test.go +50 -4
- package/internal/execution/goworkflows/interpreter.go +36 -29
- package/internal/execution/goworkflows/mailbox_test.go +85 -0
- package/internal/execution/goworkflows/node_runtime_test.go +30 -5
- package/internal/execution/goworkflows/recovery_test.go +2 -2
- package/internal/execution/goworkflows/report_contract_fixture_test.go +31 -0
- package/internal/harness/contract_test.go +10 -0
- package/internal/harness/factory.go +25 -3
- package/internal/harness/harness.go +30 -4
- package/internal/harness/opencode/opencode.go +125 -10
- package/internal/harness/opencode/opencode_test.go +183 -0
- package/internal/harness/opencode/repo_setup.go +361 -0
- package/internal/harness/plugin_selection_test.go +5 -5
- package/internal/recover/recover.go +11 -6
- package/internal/repo/service.go +10 -0
- package/internal/repo/service_test.go +51 -2
- package/internal/run/run.go +6 -4
- package/internal/task/beads/bdcli/bdcli.go +323 -0
- package/internal/task/beads/bdcli/bdcli_test.go +297 -0
- package/internal/task/beads/bdcli/testdata/array.json +1 -0
- package/internal/task/beads/bdcli/testdata/children.json +1 -0
- package/internal/task/beads/bdcli/testdata/claimed.json +1 -0
- package/internal/task/beads/bdcli/testdata/commented.json +1 -0
- package/internal/task/beads/bdcli/testdata/comments.json +1 -0
- package/internal/task/beads/bdcli/testdata/created.json +1 -0
- package/internal/task/beads/bdcli/testdata/empty.json +1 -0
- package/internal/task/beads/bdcli/testdata/object.json +1 -0
- package/internal/task/beads/bdcli/testdata/ready.json +1 -0
- package/internal/task/beads/bdcli/testdata/show.json +1 -0
- package/internal/task/beads/bdcli/testdata/strict-bd.sh +149 -0
- package/internal/task/beads/bdcli/testdata/updated.json +1 -0
- package/internal/task/beads/beads.go +840 -0
- package/internal/task/beads/beads_test.go +609 -0
- package/internal/task/beads/comments_test.go +242 -0
- package/internal/task/beads/config_compatibility_test.go +163 -0
- package/internal/task/beads/lifecycle_inheritance_test.go +168 -0
- package/internal/task/beads/repo_composition_test.go +232 -0
- package/internal/task/beads/runtime_config_test.go +81 -0
- package/internal/task/beads/status_compatibility_test.go +233 -0
- package/internal/task/beads/status_test.go +257 -0
- package/internal/task/beads/testdata/strict-bd-repo.sh +27 -0
- package/internal/task/beads/validation_test.go +110 -0
- package/internal/task/contract_test.go +10 -0
- package/internal/task/factory.go +37 -4
- package/internal/task/jira/auth.go +27 -1
- package/internal/task/jira/auth_test.go +54 -1
- package/internal/task/jira/filters_test.go +60 -0
- package/internal/task/jira/jira.go +171 -29
- package/internal/task/jira/lifecycle_inheritance_test.go +172 -0
- package/internal/task/jira/rest/adf.go +119 -82
- package/internal/task/jira/rest/adf_test.go +60 -0
- package/internal/task/jira/rest/client_test.go +1 -1
- package/internal/task/jira/templates_test.go +118 -0
- package/internal/task/jira/transition_defaults_test.go +4 -2
- package/internal/task/task.go +32 -0
- package/internal/workflow/report_test.go +45 -0
- package/package.json +1 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/rajpopat27/relay-flow/internal/config"
|
|
12
|
+
"github.com/rajpopat27/relay-flow/internal/execution/goworkflows"
|
|
13
|
+
"github.com/rajpopat27/relay-flow/internal/repo"
|
|
14
|
+
runsvc "github.com/rajpopat27/relay-flow/internal/run"
|
|
15
|
+
"github.com/rajpopat27/relay-flow/internal/server"
|
|
16
|
+
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// TestBeadsCompositionUsesRealRepoServiceAndDurableRun exercises the real
|
|
20
|
+
// composition path with the production Beads factory and bd subprocess seam.
|
|
21
|
+
// The fake bd intentionally blocks mailbox discovery until the test observes
|
|
22
|
+
// the durable run, making claim -> run creation -> mailbox creation ordering
|
|
23
|
+
// explicit rather than inferred from timing.
|
|
24
|
+
func TestBeadsCompositionUsesRealRepoServiceAndDurableRun(t *testing.T) {
|
|
25
|
+
base := t.TempDir()
|
|
26
|
+
relayHome := filepath.Join(base, "relay-flow")
|
|
27
|
+
codePath := filepath.Join(base, "code", "payments")
|
|
28
|
+
beadsDir := filepath.Join(base, "beads", "payments", ".beads")
|
|
29
|
+
for _, dir := range []string{relayHome, codePath, beadsDir} {
|
|
30
|
+
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
31
|
+
t.Fatal(err)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
t.Setenv("RELAY_FLOW_HOME", relayHome)
|
|
35
|
+
|
|
36
|
+
logPath, releasePath, stateDir := installCompositionFakeBD(t, codePath, beadsDir)
|
|
37
|
+
|
|
38
|
+
p, err := home()
|
|
39
|
+
if err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
if err := config.SaveMachine(p.Config, &config.Machine{
|
|
43
|
+
PollIntervalSeconds: 1,
|
|
44
|
+
CompletedRunRetentionDays: 1,
|
|
45
|
+
TaskPlugin: "beads",
|
|
46
|
+
RunnerPlugin: scenarioRunnerPlugin,
|
|
47
|
+
HarnessPlugin: scenarioHarnessPlugin,
|
|
48
|
+
Repos: map[string]config.Repo{},
|
|
49
|
+
}); err != nil {
|
|
50
|
+
t.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log := newScenarioLog()
|
|
54
|
+
rnr := newScenarioRunner(log)
|
|
55
|
+
hrn := newScenarioHarness(log)
|
|
56
|
+
// The scenario runner and harness are the existing replaceable seams used
|
|
57
|
+
// by the composition tests; only the task system is the real Beads adapter.
|
|
58
|
+
setScenarioFactoryAdapters(nil, rnr, hrn)
|
|
59
|
+
repoService := repo.NewService(repo.ServiceConfig{
|
|
60
|
+
ConfigPath: p.Config,
|
|
61
|
+
TaskPlugin: "beads",
|
|
62
|
+
Runner: rnr,
|
|
63
|
+
Harness: hrn,
|
|
64
|
+
Active: compositionNoActiveRuns{},
|
|
65
|
+
Workflows: compositionNoWorkflowRefs{},
|
|
66
|
+
})
|
|
67
|
+
if _, err := repoService.Register(context.Background(), repo.RegisterInput{
|
|
68
|
+
Name: "payments",
|
|
69
|
+
Path: codePath,
|
|
70
|
+
TaskConfig: config.RawValues{"beadsDir": beadsDir},
|
|
71
|
+
}); err != nil {
|
|
72
|
+
t.Fatalf("register Beads repo: %v", err)
|
|
73
|
+
}
|
|
74
|
+
// Registration probes the real adapter once. Start the composition
|
|
75
|
+
// assertions with a clean command trace and clean fake state.
|
|
76
|
+
if err := os.WriteFile(logPath, nil, 0o600); err != nil {
|
|
77
|
+
t.Fatal(err)
|
|
78
|
+
}
|
|
79
|
+
if err := os.RemoveAll(stateDir); err != nil {
|
|
80
|
+
t.Fatal(err)
|
|
81
|
+
}
|
|
82
|
+
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
|
83
|
+
t.Fatal(err)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if err := os.MkdirAll(p.Workflows, 0o700); err != nil {
|
|
87
|
+
t.Fatal(err)
|
|
88
|
+
}
|
|
89
|
+
store := &workflow.Store{Dir: p.Workflows}
|
|
90
|
+
if err := store.Put("beadsComposition", []byte(beadsCompositionWorkflowYAML)); err != nil {
|
|
91
|
+
t.Fatal(err)
|
|
92
|
+
}
|
|
93
|
+
if err := goworkflows.InitDatabase(p.Database); err != nil {
|
|
94
|
+
t.Fatal(err)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
98
|
+
done := make(chan error, 1)
|
|
99
|
+
go func() { done <- serveRoot(ctx, p, false) }()
|
|
100
|
+
|
|
101
|
+
released := false
|
|
102
|
+
cleanup := func() {
|
|
103
|
+
if !released {
|
|
104
|
+
_ = os.WriteFile(releasePath, []byte("release\n"), 0o600)
|
|
105
|
+
released = true
|
|
106
|
+
}
|
|
107
|
+
cancel()
|
|
108
|
+
select {
|
|
109
|
+
case <-done:
|
|
110
|
+
case <-time.After(15 * time.Second):
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
t.Cleanup(cleanup)
|
|
114
|
+
|
|
115
|
+
client := server.NewClient(p.Socket)
|
|
116
|
+
waitForServer(t, client)
|
|
117
|
+
|
|
118
|
+
var active runsvc.Run
|
|
119
|
+
waitScenario(t, 15*time.Second, func() bool {
|
|
120
|
+
var getErr error
|
|
121
|
+
active, getErr = client.GetRunByTicket(context.Background(), "demo-parent")
|
|
122
|
+
return getErr == nil
|
|
123
|
+
})
|
|
124
|
+
if active.ID == "" {
|
|
125
|
+
t.Fatal("durable run was not created after the Beads claim")
|
|
126
|
+
}
|
|
127
|
+
if err := appendCompositionLog(logPath, "test|run-observed\n"); err != nil {
|
|
128
|
+
t.Fatal(err)
|
|
129
|
+
}
|
|
130
|
+
lines := readCompositionLog(t, logPath)
|
|
131
|
+
readyIndex := compositionLogIndex(lines, "list --ready --no-parent --limit 0 --json")
|
|
132
|
+
claimIndex := compositionLogIndex(lines, "update demo-parent --add-label wf:beadsComposition --json")
|
|
133
|
+
runObservedIndex := compositionLogIndex(lines, "test|run-observed")
|
|
134
|
+
if readyIndex < 0 || claimIndex < 0 || runObservedIndex < 0 {
|
|
135
|
+
t.Fatalf("composition trace missing poll/claim/run events: %v", lines)
|
|
136
|
+
}
|
|
137
|
+
if !(readyIndex < claimIndex && claimIndex < runObservedIndex) {
|
|
138
|
+
t.Fatalf("poll -> claim -> run ordering = ready:%d claim:%d run:%d; trace=%v", readyIndex, claimIndex, runObservedIndex, lines)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if err := os.WriteFile(releasePath, []byte("release\n"), 0o600); err != nil {
|
|
142
|
+
t.Fatal(err)
|
|
143
|
+
}
|
|
144
|
+
released = true
|
|
145
|
+
waitScenario(t, 15*time.Second, func() bool {
|
|
146
|
+
current, getErr := client.GetRunByTicket(context.Background(), "demo-parent")
|
|
147
|
+
return getErr == nil && current.State == runsvc.StateWaiting && current.CurrentNode == "implement"
|
|
148
|
+
})
|
|
149
|
+
lines = readCompositionLog(t, logPath)
|
|
150
|
+
createImplementIndex := compositionLogIndex(lines, "create demo-parent:implement")
|
|
151
|
+
if createImplementIndex <= runObservedIndex {
|
|
152
|
+
t.Fatalf("mailbox creation did not follow run creation: run:%d create:%d; trace=%v", runObservedIndex, createImplementIndex, lines)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
current, err := client.GetRunByTicket(context.Background(), "demo-parent")
|
|
156
|
+
if err != nil {
|
|
157
|
+
t.Fatal(err)
|
|
158
|
+
}
|
|
159
|
+
if ack, err := client.SubmitReport(context.Background(), runsvc.ReportRequest{
|
|
160
|
+
RunID: current.ID,
|
|
161
|
+
Node: current.CurrentNode,
|
|
162
|
+
ReportID: string(current.CurrentNodeVisitID) + ":composition-implement",
|
|
163
|
+
Report: scenarioReport(workflow.OutcomeSuccess, "verify"),
|
|
164
|
+
}); err != nil || !ack.Accepted {
|
|
165
|
+
t.Fatalf("submit implement report: ack=%+v err=%v", ack, err)
|
|
166
|
+
}
|
|
167
|
+
waitScenario(t, 15*time.Second, func() bool {
|
|
168
|
+
current, getErr := client.GetRunByTicket(context.Background(), "demo-parent")
|
|
169
|
+
return getErr == nil && current.State == runsvc.StateWaiting && current.CurrentNode == "verify"
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
current, err = client.GetRunByTicket(context.Background(), "demo-parent")
|
|
173
|
+
if err != nil {
|
|
174
|
+
t.Fatal(err)
|
|
175
|
+
}
|
|
176
|
+
if ack, err := client.SubmitReport(context.Background(), runsvc.ReportRequest{
|
|
177
|
+
RunID: current.ID,
|
|
178
|
+
Node: current.CurrentNode,
|
|
179
|
+
ReportID: string(current.CurrentNodeVisitID) + ":composition-verify",
|
|
180
|
+
Report: scenarioReport(workflow.OutcomeSuccess, "end"),
|
|
181
|
+
}); err != nil || !ack.Accepted {
|
|
182
|
+
t.Fatalf("submit verify report: ack=%+v err=%v", ack, err)
|
|
183
|
+
}
|
|
184
|
+
waitScenario(t, 15*time.Second, func() bool {
|
|
185
|
+
finished, getErr := client.GetRunByTicket(context.Background(), "demo-parent")
|
|
186
|
+
return getErr == nil && finished.State == runsvc.StateCompleted
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
lines = readCompositionLog(t, logPath)
|
|
190
|
+
if compositionLogIndex(lines, "create demo-parent:verify") < 0 {
|
|
191
|
+
t.Fatalf("verify mailbox was not created: %v", lines)
|
|
192
|
+
}
|
|
193
|
+
comments := readCompositionState(t, stateDir, "comments.log")
|
|
194
|
+
for _, want := range []string{
|
|
195
|
+
"comment demo-parent.1",
|
|
196
|
+
"comment demo-parent.2",
|
|
197
|
+
"Summary for implement",
|
|
198
|
+
"Feedback from implement to verify mailbox demo-parent.2",
|
|
199
|
+
"Summary for verify",
|
|
200
|
+
} {
|
|
201
|
+
if !strings.Contains(comments, want) {
|
|
202
|
+
t.Fatalf("comments missing %q: %q", want, comments)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if strings.Contains(comments, "comment demo-parent ") {
|
|
206
|
+
t.Fatalf("summary/feedback was written to the parent: %q", comments)
|
|
207
|
+
}
|
|
208
|
+
if log.count("runner-repo-validated") == 0 || log.count("harness-agent-validated:implementer") == 0 {
|
|
209
|
+
t.Fatalf("existing runner/harness seams were not used: %v", log.all())
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if err := client.Stop(context.Background()); err != nil {
|
|
213
|
+
t.Fatal(err)
|
|
214
|
+
}
|
|
215
|
+
select {
|
|
216
|
+
case serveErr := <-done:
|
|
217
|
+
if serveErr != nil {
|
|
218
|
+
t.Fatalf("serveRoot: %v", serveErr)
|
|
219
|
+
}
|
|
220
|
+
case <-time.After(15 * time.Second):
|
|
221
|
+
t.Fatal("serveRoot did not stop")
|
|
222
|
+
}
|
|
223
|
+
released = true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func installCompositionFakeBD(t *testing.T, repoPath, beadsDir string) (logPath, releasePath, stateDir string) {
|
|
227
|
+
t.Helper()
|
|
228
|
+
binDir := t.TempDir()
|
|
229
|
+
logPath = filepath.Join(t.TempDir(), "bd.log")
|
|
230
|
+
releasePath = filepath.Join(t.TempDir(), "release")
|
|
231
|
+
stateDir = filepath.Join(t.TempDir(), "state")
|
|
232
|
+
if err := os.WriteFile(logPath, nil, 0o600); err != nil {
|
|
233
|
+
t.Fatal(err)
|
|
234
|
+
}
|
|
235
|
+
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
|
236
|
+
t.Fatal(err)
|
|
237
|
+
}
|
|
238
|
+
path := filepath.Join(binDir, "bd")
|
|
239
|
+
if err := os.WriteFile(path, []byte(compositionFakeBDScript), 0o755); err != nil {
|
|
240
|
+
t.Fatal(err)
|
|
241
|
+
}
|
|
242
|
+
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
243
|
+
t.Setenv("BD_LOG", logPath)
|
|
244
|
+
t.Setenv("BD_STATE", stateDir)
|
|
245
|
+
t.Setenv("BD_GATE_RELEASE", releasePath)
|
|
246
|
+
t.Setenv("BD_EXPECT_CWD", repoPath)
|
|
247
|
+
t.Setenv("BD_EXPECT_BEADS_DIR", beadsDir)
|
|
248
|
+
// These ambient selectors must be removed by bdcli.commandEnvironment.
|
|
249
|
+
t.Setenv("BEADS_DIR", "/ambient/workspace")
|
|
250
|
+
t.Setenv("BEADS_DB", "/ambient/beads.db")
|
|
251
|
+
t.Setenv("BD_DB", "/ambient/legacy.db")
|
|
252
|
+
return logPath, releasePath, stateDir
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
func appendCompositionLog(path, text string) error {
|
|
256
|
+
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o600)
|
|
257
|
+
if err != nil {
|
|
258
|
+
return err
|
|
259
|
+
}
|
|
260
|
+
defer f.Close()
|
|
261
|
+
_, err = f.WriteString(text)
|
|
262
|
+
return err
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func readCompositionLog(t *testing.T, path string) []string {
|
|
266
|
+
t.Helper()
|
|
267
|
+
raw, err := os.ReadFile(path)
|
|
268
|
+
if err != nil {
|
|
269
|
+
t.Fatal(err)
|
|
270
|
+
}
|
|
271
|
+
return strings.Split(strings.TrimSpace(string(raw)), "\n")
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func compositionLogIndex(lines []string, want string) int {
|
|
275
|
+
for i, line := range lines {
|
|
276
|
+
if strings.Contains(line, want) {
|
|
277
|
+
return i
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return -1
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func readCompositionState(t *testing.T, dir, name string) string {
|
|
284
|
+
t.Helper()
|
|
285
|
+
raw, err := os.ReadFile(filepath.Join(dir, name))
|
|
286
|
+
if err != nil {
|
|
287
|
+
t.Fatal(err)
|
|
288
|
+
}
|
|
289
|
+
return string(raw)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
type compositionNoActiveRuns struct{}
|
|
293
|
+
|
|
294
|
+
func (compositionNoActiveRuns) HasActiveRepo(context.Context, string) (bool, error) {
|
|
295
|
+
return false, nil
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
type compositionNoWorkflowRefs struct{}
|
|
299
|
+
|
|
300
|
+
func (compositionNoWorkflowRefs) ReferencesRepo(string) bool { return false }
|
|
301
|
+
|
|
302
|
+
const beadsCompositionWorkflowYAML = `name: beadsComposition
|
|
303
|
+
repos: [payments]
|
|
304
|
+
cleanupRunnerOnEnd: true
|
|
305
|
+
nodes:
|
|
306
|
+
start:
|
|
307
|
+
onSuccess: [{target: implement}]
|
|
308
|
+
implement:
|
|
309
|
+
type: agent
|
|
310
|
+
agent: implementer
|
|
311
|
+
description: Implement the requested change.
|
|
312
|
+
onSuccess: [{target: verify}]
|
|
313
|
+
onFailure: [{target: implement, when: implementation cannot proceed}]
|
|
314
|
+
verify:
|
|
315
|
+
type: agent
|
|
316
|
+
agent: verifier
|
|
317
|
+
description: Verify the implementation.
|
|
318
|
+
onSuccess: [{target: end}]
|
|
319
|
+
onFailure: [{target: implement, when: verification fails}]
|
|
320
|
+
end: {}
|
|
321
|
+
`
|
|
322
|
+
|
|
323
|
+
const compositionFakeBDScript = `#!/bin/sh
|
|
324
|
+
set -eu
|
|
325
|
+
|
|
326
|
+
fail() {
|
|
327
|
+
printf 'unsupported or malformed composition bd invocation: %s\n' "$*" >&2
|
|
328
|
+
exit 64
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
[ -n "${BD_LOG-}" ] || fail "BD_LOG is missing"
|
|
332
|
+
[ -n "${BD_STATE-}" ] || fail "BD_STATE is missing"
|
|
333
|
+
[ -n "${BD_GATE_RELEASE-}" ] || fail "BD_GATE_RELEASE is missing"
|
|
334
|
+
[ "$PWD" = "${BD_EXPECT_CWD-}" ] || fail "wrong cwd: $PWD"
|
|
335
|
+
[ "${BEADS_DIR-}" = "${BD_EXPECT_BEADS_DIR-}" ] || fail "wrong BEADS_DIR: ${BEADS_DIR-}"
|
|
336
|
+
[ "${BEADS_DB+x}" != x ] || fail "ambient BEADS_DB leaked"
|
|
337
|
+
[ "${BD_DB+x}" != x ] || fail "ambient BD_DB leaked"
|
|
338
|
+
printf 'bd|%s\n' "$*" >> "$BD_LOG"
|
|
339
|
+
|
|
340
|
+
status_file() {
|
|
341
|
+
case "$1" in
|
|
342
|
+
demo-parent) printf '%s/parent.status' "$BD_STATE" ;;
|
|
343
|
+
demo-parent.1) printf '%s/implement.status' "$BD_STATE" ;;
|
|
344
|
+
demo-parent.2) printf '%s/verify.status' "$BD_STATE" ;;
|
|
345
|
+
*) fail "unknown issue $1" ;;
|
|
346
|
+
esac
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
issue_status() {
|
|
350
|
+
file=$(status_file "$1")
|
|
351
|
+
if [ -f "$file" ]; then
|
|
352
|
+
tr -d '\n' < "$file"
|
|
353
|
+
else
|
|
354
|
+
printf 'open'
|
|
355
|
+
fi
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
issue_json() {
|
|
359
|
+
issue=$1
|
|
360
|
+
status=$(issue_status "$issue")
|
|
361
|
+
case "$issue" in
|
|
362
|
+
demo-parent)
|
|
363
|
+
if [ -f "$BD_STATE/claimed" ]; then
|
|
364
|
+
labels='["wf:beadsComposition"]'
|
|
365
|
+
else
|
|
366
|
+
labels='[]'
|
|
367
|
+
fi
|
|
368
|
+
printf '[{"id":"demo-parent","title":"Composition parent","status":"%s","issue_type":"epic","priority":1,"labels":%s}]\n' "$status" "$labels"
|
|
369
|
+
;;
|
|
370
|
+
demo-parent.1)
|
|
371
|
+
printf '[{"id":"demo-parent.1","title":"demo-parent:implement","status":"%s","issue_type":"task","priority":2,"labels":["wf:beadsComposition"],"parent":"demo-parent"}]\n' "$status"
|
|
372
|
+
;;
|
|
373
|
+
demo-parent.2)
|
|
374
|
+
printf '[{"id":"demo-parent.2","title":"demo-parent:verify","status":"%s","issue_type":"task","priority":2,"labels":["wf:beadsComposition"],"parent":"demo-parent"}]\n' "$status"
|
|
375
|
+
;;
|
|
376
|
+
*) fail "unknown issue $issue" ;;
|
|
377
|
+
esac
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if [ "$#" -eq 6 ] && [ "$1" = list ] && [ "$2" = --ready ] && [ "$3" = --limit ] && [ "$4" = 1 ] && [ "$5" = --no-parent ] && [ "$6" = --json ]; then
|
|
381
|
+
printf '[]\n'
|
|
382
|
+
exit 0
|
|
383
|
+
fi
|
|
384
|
+
if [ "$#" -eq 6 ] && [ "$1" = list ] && [ "$2" = --ready ] && [ "$3" = --no-parent ] && [ "$4" = --limit ] && [ "$5" = 0 ] && [ "$6" = --json ]; then
|
|
385
|
+
if [ -f "$BD_STATE/claimed" ]; then
|
|
386
|
+
printf '[]\n'
|
|
387
|
+
else
|
|
388
|
+
printf '[{"id":"demo-parent","title":"Composition parent","status":"open","issue_type":"epic","priority":1,"labels":[]}]\n'
|
|
389
|
+
fi
|
|
390
|
+
exit 0
|
|
391
|
+
fi
|
|
392
|
+
if [ "$#" -eq 9 ] && [ "$1" = list ] && [ "$2" = --no-parent ] && [ "$3" = --status ] && [ "$4" = open,in_progress,blocked,deferred ] && [ "$5" = --label-pattern ] && [ "$6" = 'wf:*' ] && [ "$7" = --limit ] && [ "$8" = 0 ] && [ "$9" = --json ]; then
|
|
393
|
+
printf '[]\n'
|
|
394
|
+
exit 0
|
|
395
|
+
fi
|
|
396
|
+
if [ "$#" -eq 7 ] && [ "$1" = list ] && [ "$2" = --parent ] && [ "$3" = demo-parent ] && [ "$4" = --all ] && [ "$5" = --limit ] && [ "$6" = 0 ] && [ "$7" = --json ]; then
|
|
397
|
+
while [ ! -f "$BD_GATE_RELEASE" ]; do
|
|
398
|
+
sleep 0.01
|
|
399
|
+
done
|
|
400
|
+
out='['
|
|
401
|
+
sep=
|
|
402
|
+
if [ -f "$BD_STATE/implement" ]; then
|
|
403
|
+
out="${out}${sep}{\"id\":\"demo-parent.1\",\"title\":\"demo-parent:implement\",\"status\":\"$(issue_status demo-parent.1)\",\"issue_type\":\"task\",\"priority\":2,\"labels\":[\"wf:beadsComposition\"],\"parent\":\"demo-parent\"}"
|
|
404
|
+
sep=,
|
|
405
|
+
fi
|
|
406
|
+
if [ -f "$BD_STATE/verify" ]; then
|
|
407
|
+
out="${out}${sep}{\"id\":\"demo-parent.2\",\"title\":\"demo-parent:verify\",\"status\":\"$(issue_status demo-parent.2)\",\"issue_type\":\"task\",\"priority\":2,\"labels\":[\"wf:beadsComposition\"],\"parent\":\"demo-parent\"}"
|
|
408
|
+
fi
|
|
409
|
+
printf '%s]\n' "$out"
|
|
410
|
+
exit 0
|
|
411
|
+
fi
|
|
412
|
+
if [ "$#" -eq 3 ] && [ "$1" = show ] && [ "$3" = --json ]; then
|
|
413
|
+
issue_json "$2"
|
|
414
|
+
exit 0
|
|
415
|
+
fi
|
|
416
|
+
if [ "$#" -eq 11 ] && [ "$1" = create ] && [ "$3" = --type ] && [ "$4" = task ] && [ "$5" = --parent ] && [ "$7" = --no-inherit-labels ] && [ "$8" = --labels ] && [ "$9" = wf:beadsComposition ] && [ "${10}" = --stdin ] && [ "${11}" = --json ]; then
|
|
417
|
+
case "$2" in
|
|
418
|
+
demo-parent:implement) id=demo-parent.1; node=implement ;;
|
|
419
|
+
demo-parent:verify) id=demo-parent.2; node=verify ;;
|
|
420
|
+
*) fail "unknown child title $2" ;;
|
|
421
|
+
esac
|
|
422
|
+
cat > "$BD_STATE/$node.description"
|
|
423
|
+
: > "$BD_STATE/$node"
|
|
424
|
+
printf '%s\n' open > "$BD_STATE/$node.status"
|
|
425
|
+
printf '{"id":"%s","title":"%s","status":"open","issue_type":"task","priority":2,"labels":["wf:beadsComposition"],"parent":"demo-parent"}\n' "$id" "$2"
|
|
426
|
+
exit 0
|
|
427
|
+
fi
|
|
428
|
+
if [ "$#" -eq 5 ] && [ "$1" = update ] && [ "$2" = demo-parent ] && [ "$3" = --add-label ] && [ "$4" = wf:beadsComposition ] && [ "$5" = --json ]; then
|
|
429
|
+
: > "$BD_STATE/claimed"
|
|
430
|
+
printf '{}\n'
|
|
431
|
+
exit 0
|
|
432
|
+
fi
|
|
433
|
+
if [ "$#" -eq 5 ] && [ "$1" = update ] && [ "$3" = --status ] && [ "$5" = --json ]; then
|
|
434
|
+
file=$(status_file "$2")
|
|
435
|
+
printf '%s\n' "$4" > "$file"
|
|
436
|
+
printf '{}\n'
|
|
437
|
+
exit 0
|
|
438
|
+
fi
|
|
439
|
+
if [ "$#" -eq 3 ] && [ "$1" = comments ] && [ "$3" = --json ]; then
|
|
440
|
+
printf '[]\n'
|
|
441
|
+
exit 0
|
|
442
|
+
fi
|
|
443
|
+
if [ "$#" -eq 4 ] && [ "$1" = comment ] && [ "$3" = --stdin ] && [ "$4" = --json ]; then
|
|
444
|
+
printf 'comment %s\n' "$2" >> "$BD_STATE/comments.log"
|
|
445
|
+
cat >> "$BD_STATE/comments.log"
|
|
446
|
+
printf '\n---\n' >> "$BD_STATE/comments.log"
|
|
447
|
+
printf '{}\n'
|
|
448
|
+
exit 0
|
|
449
|
+
fi
|
|
450
|
+
fail "$@"
|
|
451
|
+
`
|
|
@@ -346,6 +346,43 @@ func TestInitPrintsSelectedValuesAndCompletion(t *testing.T) {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
func TestInitWritesHarnessPromptDefaults(t *testing.T) {
|
|
350
|
+
home := t.TempDir()
|
|
351
|
+
if code := cli(t, home, "", initArgs()...); code != 0 {
|
|
352
|
+
t.Fatalf("init exit = %d", code)
|
|
353
|
+
}
|
|
354
|
+
cfg, err := config.LoadMachine(filepath.Join(home, ".relay-flow", "config.yaml"))
|
|
355
|
+
if err != nil {
|
|
356
|
+
t.Fatal(err)
|
|
357
|
+
}
|
|
358
|
+
for _, key := range []string{"initial", "feedback", "hitl"} {
|
|
359
|
+
value, ok := cfg.HarnessConfig[key].(string)
|
|
360
|
+
if !ok || value == "" {
|
|
361
|
+
t.Fatalf("harnessConfig.%s = %#v", key, cfg.HarnessConfig[key])
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func TestInitWritesTaskTextDefaults(t *testing.T) {
|
|
367
|
+
home := t.TempDir()
|
|
368
|
+
if code := cli(t, home, "", initArgs()...); code != 0 {
|
|
369
|
+
t.Fatalf("init exit = %d", code)
|
|
370
|
+
}
|
|
371
|
+
cfg, err := config.LoadMachine(filepath.Join(home, ".relay-flow", "config.yaml"))
|
|
372
|
+
if err != nil {
|
|
373
|
+
t.Fatal(err)
|
|
374
|
+
}
|
|
375
|
+
templates, ok := rawValuesMap(cfg.TaskConfig["templates"])
|
|
376
|
+
if !ok {
|
|
377
|
+
t.Fatalf("taskConfig.templates = %#v", cfg.TaskConfig["templates"])
|
|
378
|
+
}
|
|
379
|
+
for _, key := range []string{"mailboxDescription", "summaryComment", "feedbackComment"} {
|
|
380
|
+
if value, ok := templates[key].(string); !ok || value == "" {
|
|
381
|
+
t.Fatalf("taskConfig.templates.%s = %#v", key, templates[key])
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
349
386
|
func TestInitDoesNotCollectOrWriteTaskCredentials(t *testing.T) {
|
|
350
387
|
home := t.TempDir()
|
|
351
388
|
if code := cli(t, home, "", initArgs()...); code != 0 {
|
|
@@ -413,6 +450,12 @@ func TestInitForcePreservesDurableAndUserState(t *testing.T) {
|
|
|
413
450
|
t.Fatal(err)
|
|
414
451
|
}
|
|
415
452
|
cfg.Repos = map[string]config.Repo{"existing": {Path: "/srv/existing"}}
|
|
453
|
+
cfg.HarnessConfig["initial"] = "custom {{ticket}}"
|
|
454
|
+
taskTemplates, ok := rawValuesMap(cfg.TaskConfig["templates"])
|
|
455
|
+
if !ok {
|
|
456
|
+
t.Fatalf("taskConfig.templates = %#v", cfg.TaskConfig["templates"])
|
|
457
|
+
}
|
|
458
|
+
taskTemplates["mailboxDescription"] = "custom {{nodeDescription}}"
|
|
416
459
|
if err := config.SaveMachine(filepath.Join(root, "config.yaml"), cfg); err != nil {
|
|
417
460
|
t.Fatal(err)
|
|
418
461
|
}
|
|
@@ -453,6 +496,26 @@ func TestInitForcePreservesDurableAndUserState(t *testing.T) {
|
|
|
453
496
|
if _, ok := cfg.Repos["existing"]; !ok {
|
|
454
497
|
t.Fatal("forced init removed existing repo config")
|
|
455
498
|
}
|
|
499
|
+
if got := cfg.HarnessConfig["initial"]; got != "custom {{ticket}}" {
|
|
500
|
+
t.Fatalf("forced init changed prompt override: %#v", got)
|
|
501
|
+
}
|
|
502
|
+
gotTaskTemplates, ok := rawValuesMap(cfg.TaskConfig["templates"])
|
|
503
|
+
if !ok {
|
|
504
|
+
t.Fatalf("taskConfig.templates = %#v", cfg.TaskConfig["templates"])
|
|
505
|
+
}
|
|
506
|
+
if got := gotTaskTemplates["mailboxDescription"]; got != "custom {{nodeDescription}}" {
|
|
507
|
+
t.Fatalf("forced init changed task text override: %#v", got)
|
|
508
|
+
}
|
|
509
|
+
for _, key := range []string{"summaryComment", "feedbackComment"} {
|
|
510
|
+
if got, ok := gotTaskTemplates[key].(string); !ok || got == "" {
|
|
511
|
+
t.Fatalf("forced init omitted task default %s: %#v", key, gotTaskTemplates[key])
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for _, key := range []string{"feedback", "hitl"} {
|
|
515
|
+
if got, ok := cfg.HarnessConfig[key].(string); !ok || got == "" {
|
|
516
|
+
t.Fatalf("forced init omitted harness default %s: %#v", key, cfg.HarnessConfig[key])
|
|
517
|
+
}
|
|
518
|
+
}
|
|
456
519
|
db, err := sql.Open("sqlite", dbPath)
|
|
457
520
|
if err != nil {
|
|
458
521
|
t.Fatal(err)
|
|
@@ -467,6 +530,17 @@ func TestInitForcePreservesDurableAndUserState(t *testing.T) {
|
|
|
467
530
|
}
|
|
468
531
|
}
|
|
469
532
|
|
|
533
|
+
func rawValuesMap(value any) (map[string]any, bool) {
|
|
534
|
+
switch values := value.(type) {
|
|
535
|
+
case map[string]any:
|
|
536
|
+
return values, true
|
|
537
|
+
case config.RawValues:
|
|
538
|
+
return map[string]any(values), true
|
|
539
|
+
default:
|
|
540
|
+
return nil, false
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
470
544
|
// 8.2: --task-plugin/--runner-plugin/--harness-plugin run init without any
|
|
471
545
|
// prompt/stdin and write the same machine config as the stdin path.
|
|
472
546
|
func TestInitFlagsNonInteractive(t *testing.T) {
|
package/cmd/relay-flow/main.go
CHANGED
|
@@ -38,6 +38,7 @@ import (
|
|
|
38
38
|
// name validation at init-time).
|
|
39
39
|
_ "github.com/rajpopat27/relay-flow/internal/harness/opencode"
|
|
40
40
|
_ "github.com/rajpopat27/relay-flow/internal/runner/orca"
|
|
41
|
+
_ "github.com/rajpopat27/relay-flow/internal/task/beads"
|
|
41
42
|
_ "github.com/rajpopat27/relay-flow/internal/task/jira"
|
|
42
43
|
)
|
|
43
44
|
|
|
@@ -269,6 +270,30 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
269
270
|
cfg.TaskPlugin = names[0]
|
|
270
271
|
cfg.RunnerPlugin = names[1]
|
|
271
272
|
cfg.HarnessPlugin = names[2]
|
|
273
|
+
taskDefaults, err := task.Defaults(names[0])
|
|
274
|
+
if err != nil {
|
|
275
|
+
fmt.Fprintln(os.Stderr, "init: task plugin: "+err.Error())
|
|
276
|
+
return exitFail
|
|
277
|
+
}
|
|
278
|
+
if !*force || !configExists {
|
|
279
|
+
cfg.TaskConfig = taskDefaults
|
|
280
|
+
} else {
|
|
281
|
+
cfg.TaskConfig = config.Merge(taskDefaults, cfg.TaskConfig)
|
|
282
|
+
}
|
|
283
|
+
if err := task.ValidateTextConfig(names[0], cfg.TaskConfig); err != nil {
|
|
284
|
+
fmt.Fprintln(os.Stderr, "init: task plugin config: "+err.Error())
|
|
285
|
+
return exitFail
|
|
286
|
+
}
|
|
287
|
+
harnessDefaults, err := harness.Defaults(names[2])
|
|
288
|
+
if err != nil {
|
|
289
|
+
fmt.Fprintln(os.Stderr, "init: harness plugin: "+err.Error())
|
|
290
|
+
return exitFail
|
|
291
|
+
}
|
|
292
|
+
if !*force || !configExists {
|
|
293
|
+
cfg.HarnessConfig = harnessDefaults
|
|
294
|
+
} else {
|
|
295
|
+
cfg.HarnessConfig = config.Merge(harnessDefaults, cfg.HarnessConfig)
|
|
296
|
+
}
|
|
272
297
|
if err := config.SaveMachine(p.Config, cfg); err != nil {
|
|
273
298
|
fmt.Fprintln(os.Stderr, err)
|
|
274
299
|
return exitFail
|