relay-flow 0.2.0-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.
Files changed (102) hide show
  1. package/README.md +171 -26
  2. package/cmd/relay-flow/beads_composition_test.go +451 -0
  3. package/cmd/relay-flow/commands_test.go +593 -15
  4. package/cmd/relay-flow/main.go +356 -119
  5. package/cmd/relay-flow/scenario_test.go +246 -35
  6. package/cmd/relay-flow/serve.go +5 -2
  7. package/examples/beads-workflow.yaml +74 -0
  8. package/examples/default-story-workflow.yaml +88 -0
  9. package/go.mod +2 -1
  10. package/go.sum +2 -0
  11. package/internal/config/config.go +13 -2
  12. package/internal/config/merge_test.go +18 -0
  13. package/internal/execution/goworkflows/activities.go +136 -87
  14. package/internal/execution/goworkflows/end_feedback_test.go +124 -0
  15. package/internal/execution/goworkflows/engine.go +41 -8
  16. package/internal/execution/goworkflows/engine_test.go +100 -22
  17. package/internal/execution/goworkflows/fakes_test.go +63 -25
  18. package/internal/execution/goworkflows/interpreter.go +45 -30
  19. package/internal/execution/goworkflows/mailbox_test.go +85 -0
  20. package/internal/execution/goworkflows/node_runtime_integration_test.go +12 -6
  21. package/internal/execution/goworkflows/node_runtime_test.go +72 -23
  22. package/internal/execution/goworkflows/recovery_test.go +7 -7
  23. package/internal/execution/goworkflows/report_contract_fixture_test.go +31 -0
  24. package/internal/execution/goworkflows/retry_log_test.go +11 -11
  25. package/internal/harness/contract_test.go +15 -0
  26. package/internal/harness/factory.go +25 -3
  27. package/internal/harness/harness.go +30 -4
  28. package/internal/harness/opencode/opencode.go +125 -10
  29. package/internal/harness/opencode/opencode_test.go +183 -0
  30. package/internal/harness/opencode/repo_setup.go +361 -0
  31. package/internal/harness/plugin_selection_test.go +5 -5
  32. package/internal/paths/paths.go +18 -16
  33. package/internal/recover/recover.go +11 -6
  34. package/internal/repo/repo.go +13 -0
  35. package/internal/repo/service.go +10 -0
  36. package/internal/repo/service_test.go +55 -6
  37. package/internal/router/router.go +3 -2
  38. package/internal/router/router_test.go +87 -0
  39. package/internal/run/manager.go +14 -1
  40. package/internal/run/run.go +6 -4
  41. package/internal/run/run_manager_test.go +21 -1
  42. package/internal/runner/contract_test.go +64 -26
  43. package/internal/runner/orca/orca.go +30 -54
  44. package/internal/runner/orca/orca_test.go +143 -4
  45. package/internal/runner/orca/orcacli/orcacli.go +5 -0
  46. package/internal/runner/orca/orcacli/orcacli_test.go +3 -0
  47. package/internal/runner/orca/orcacli/testdata/strict-orca.sh +2 -0
  48. package/internal/runner/runner.go +15 -8
  49. package/internal/task/auth_test.go +48 -0
  50. package/internal/task/beads/bdcli/bdcli.go +323 -0
  51. package/internal/task/beads/bdcli/bdcli_test.go +297 -0
  52. package/internal/task/beads/bdcli/testdata/array.json +1 -0
  53. package/internal/task/beads/bdcli/testdata/children.json +1 -0
  54. package/internal/task/beads/bdcli/testdata/claimed.json +1 -0
  55. package/internal/task/beads/bdcli/testdata/commented.json +1 -0
  56. package/internal/task/beads/bdcli/testdata/comments.json +1 -0
  57. package/internal/task/beads/bdcli/testdata/created.json +1 -0
  58. package/internal/task/beads/bdcli/testdata/object.json +1 -0
  59. package/internal/task/beads/bdcli/testdata/ready.json +1 -0
  60. package/internal/task/beads/bdcli/testdata/show.json +1 -0
  61. package/internal/task/beads/bdcli/testdata/strict-bd.sh +149 -0
  62. package/internal/task/beads/bdcli/testdata/updated.json +1 -0
  63. package/internal/task/beads/beads.go +840 -0
  64. package/internal/task/beads/beads_test.go +609 -0
  65. package/internal/task/beads/comments_test.go +242 -0
  66. package/internal/task/beads/config_compatibility_test.go +163 -0
  67. package/internal/task/beads/lifecycle_inheritance_test.go +168 -0
  68. package/internal/task/beads/repo_composition_test.go +232 -0
  69. package/internal/task/beads/runtime_config_test.go +81 -0
  70. package/internal/task/beads/status_compatibility_test.go +233 -0
  71. package/internal/task/beads/status_test.go +257 -0
  72. package/internal/task/beads/testdata/strict-bd-repo.sh +27 -0
  73. package/internal/task/beads/validation_test.go +110 -0
  74. package/internal/task/contract_test.go +12 -0
  75. package/internal/task/factory.go +52 -3
  76. package/internal/task/jira/auth.go +209 -0
  77. package/internal/task/jira/auth_test.go +160 -0
  78. package/internal/task/jira/effects_test.go +39 -0
  79. package/internal/task/jira/filters_test.go +96 -16
  80. package/internal/task/jira/helpers_test.go +29 -19
  81. package/internal/task/jira/jira.go +263 -90
  82. package/internal/task/jira/lifecycle_inheritance_test.go +172 -0
  83. package/internal/task/jira/normalize.go +32 -14
  84. package/internal/task/jira/rest/adf.go +165 -0
  85. package/internal/task/jira/rest/adf_test.go +60 -0
  86. package/internal/task/jira/rest/client.go +573 -0
  87. package/internal/task/jira/rest/client_test.go +381 -0
  88. package/internal/task/jira/templates_test.go +118 -0
  89. package/internal/task/jira/transition_defaults_test.go +22 -18
  90. package/internal/task/jira/validation_test.go +1 -1
  91. package/internal/task/task.go +32 -0
  92. package/internal/workflow/report_test.go +45 -0
  93. package/internal/workflow/workflow.go +9 -6
  94. package/internal/workflow/workflow_test.go +14 -12
  95. package/package.json +2 -1
  96. package/internal/task/jira/acli/acli.go +0 -306
  97. package/internal/task/jira/acli/acli_test.go +0 -208
  98. package/internal/task/jira/acli/testdata/acli_comments.json +0 -55
  99. package/internal/task/jira/acli/testdata/search_invalid_assignee.txt +0 -1
  100. package/internal/task/jira/acli/testdata/search_invalid_status.txt +0 -1
  101. /package/internal/task/{jira/acli/testdata/search_success.json → beads/bdcli/testdata/empty.json} +0 -0
  102. /package/internal/task/jira/testdata/{acli_search.json → jira_search_issues.json} +0 -0
@@ -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
+ `