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
|
@@ -41,6 +41,9 @@ var (
|
|
|
41
41
|
func init() {
|
|
42
42
|
task.Register(scenarioTaskPlugin, task.Factory{
|
|
43
43
|
RequiredRepoKeys: func() []string { return nil },
|
|
44
|
+
DefaultConfig: func() config.RawValues {
|
|
45
|
+
return config.RawValues{"templates": map[string]any{"format": "alternate"}}
|
|
46
|
+
},
|
|
44
47
|
TaskScopeKey: func(config.RawValues, config.RawValues) (string, error) {
|
|
45
48
|
return "scenario-scope", nil
|
|
46
49
|
},
|
|
@@ -67,16 +70,19 @@ func init() {
|
|
|
67
70
|
}
|
|
68
71
|
return scenarioFactoryRunner, nil
|
|
69
72
|
})
|
|
70
|
-
harness.Register(scenarioHarnessPlugin,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
harness.Register(scenarioHarnessPlugin, harness.Factory{
|
|
74
|
+
DefaultConfig: func() config.RawValues { return config.RawValues{"runtime": "alternate"} },
|
|
75
|
+
New: func(config.RawValues) (harness.Harness, error) {
|
|
76
|
+
scenarioFactoryMu.Lock()
|
|
77
|
+
defer scenarioFactoryMu.Unlock()
|
|
78
|
+
if scenarioFactoryHarness == nil {
|
|
79
|
+
return nil, errors.New("scenario harness not configured")
|
|
80
|
+
}
|
|
81
|
+
if fake, ok := scenarioFactoryHarness.(*scenarioHarness); ok {
|
|
82
|
+
fake.log.add("factory:harness:" + scenarioHarnessPlugin)
|
|
83
|
+
}
|
|
84
|
+
return scenarioFactoryHarness, nil
|
|
85
|
+
},
|
|
80
86
|
})
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -194,7 +200,7 @@ func TestCompositionRootSelectsAlternatePluginsForDurableRun(t *testing.T) {
|
|
|
194
200
|
})
|
|
195
201
|
|
|
196
202
|
launch := hrn.launch("implement")
|
|
197
|
-
wantPrompt := "
|
|
203
|
+
wantPrompt := "initial taskSystem=" + scenarioTaskPlugin
|
|
198
204
|
if !strings.Contains(launch.Prompt, wantPrompt) {
|
|
199
205
|
t.Fatalf("selected task system did not reach launch prompt: %q", launch.Prompt)
|
|
200
206
|
}
|
|
@@ -416,7 +422,7 @@ func TestScenarioLateRegisterAndLateSubmit(t *testing.T) {
|
|
|
416
422
|
}
|
|
417
423
|
repoService := repo.NewServiceWithRegistry(repo.ServiceConfig{
|
|
418
424
|
ConfigPath: configPath, TaskPlugin: scenarioTaskPlugin, Runner: fr,
|
|
419
|
-
Active: engine, Workflows: wfService.Registry(),
|
|
425
|
+
Harness: fh, Active: engine, Workflows: wfService.Registry(),
|
|
420
426
|
}, reg)
|
|
421
427
|
deps := &serveDeps{
|
|
422
428
|
repos: repoService,
|
|
@@ -785,6 +791,7 @@ type scenarioTaskSystem struct {
|
|
|
785
791
|
mu sync.Mutex
|
|
786
792
|
|
|
787
793
|
claimed bool
|
|
794
|
+
renderText func(task.TextKind, task.TextData) (string, error)
|
|
788
795
|
mailboxes map[string]task.Mailbox
|
|
789
796
|
specs map[string]task.MailboxSpec
|
|
790
797
|
mailboxStatus map[string]string
|
|
@@ -833,6 +840,22 @@ func (s *scenarioTaskSystem) ValidateConfig(context.Context, config.RawValues, m
|
|
|
833
840
|
return nil
|
|
834
841
|
}
|
|
835
842
|
|
|
843
|
+
func (s *scenarioTaskSystem) RenderText(kind task.TextKind, data task.TextData) (string, error) {
|
|
844
|
+
if s.renderText != nil {
|
|
845
|
+
return s.renderText(kind, data)
|
|
846
|
+
}
|
|
847
|
+
switch kind {
|
|
848
|
+
case task.TextMailboxDescription:
|
|
849
|
+
return "mailbox " + data.Node + ": " + data.NodeDescription, nil
|
|
850
|
+
case task.TextSummaryComment:
|
|
851
|
+
return "SUMMARY\n" + data.SummaryReport, nil
|
|
852
|
+
case task.TextFeedbackComment:
|
|
853
|
+
return "Feedback from " + data.SourceNode + " to " + data.TargetNode + " mailbox " + data.Mailbox + "\n" + data.FeedbackReport, nil
|
|
854
|
+
default:
|
|
855
|
+
return "", fmt.Errorf("unknown task text kind %q", kind)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
836
859
|
func (s *scenarioTaskSystem) EnsureMailboxes(_ context.Context, parent task.TicketRef, _ string, specs []task.MailboxSpec) (map[string]task.Mailbox, error) {
|
|
837
860
|
s.log.add("mailboxes:ensure")
|
|
838
861
|
s.mu.Lock()
|
|
@@ -1121,6 +1144,8 @@ type scenarioHarness struct {
|
|
|
1121
1144
|
launches map[string][]harness.LaunchSpec
|
|
1122
1145
|
}
|
|
1123
1146
|
|
|
1147
|
+
func (*scenarioHarness) SetupRepo(context.Context, string) error { return nil }
|
|
1148
|
+
|
|
1124
1149
|
var _ harness.Harness = (*scenarioHarness)(nil)
|
|
1125
1150
|
|
|
1126
1151
|
func newScenarioHarness(log *scenarioLog) *scenarioHarness {
|
|
@@ -1139,6 +1164,17 @@ func (h *scenarioHarness) FindSession(_ context.Context, _ string, title string)
|
|
|
1139
1164
|
return session, ok, nil
|
|
1140
1165
|
}
|
|
1141
1166
|
|
|
1167
|
+
func (h *scenarioHarness) RenderPrompt(kind harness.PromptKind, data harness.PromptData, nudge string) (string, error) {
|
|
1168
|
+
prompt := string(kind) + " taskSystem=" + data.TaskSystem + " mailbox=" + data.Mailbox
|
|
1169
|
+
if data.NodeType == workflow.NodeHITL {
|
|
1170
|
+
prompt += " hitl"
|
|
1171
|
+
}
|
|
1172
|
+
if nudge != "" {
|
|
1173
|
+
prompt += " " + nudge
|
|
1174
|
+
}
|
|
1175
|
+
return prompt, nil
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1142
1178
|
func (h *scenarioHarness) BuildCommand(spec harness.LaunchSpec) (runner.Command, error) {
|
|
1143
1179
|
h.mu.Lock()
|
|
1144
1180
|
h.launches[spec.Node] = append(h.launches[spec.Node], spec)
|
package/cmd/relay-flow/serve.go
CHANGED
|
@@ -35,6 +35,7 @@ import (
|
|
|
35
35
|
// Adapter registrations (factories are registered by name via init).
|
|
36
36
|
_ "github.com/rajpopat27/relay-flow/internal/harness/opencode"
|
|
37
37
|
_ "github.com/rajpopat27/relay-flow/internal/runner/orca"
|
|
38
|
+
_ "github.com/rajpopat27/relay-flow/internal/task/beads"
|
|
38
39
|
_ "github.com/rajpopat27/relay-flow/internal/task/jira"
|
|
39
40
|
)
|
|
40
41
|
|
|
@@ -263,6 +264,7 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
263
264
|
ConfigPath: p.Config,
|
|
264
265
|
TaskPlugin: cfg.TaskPlugin,
|
|
265
266
|
Runner: rnr,
|
|
267
|
+
Harness: hrn,
|
|
266
268
|
Active: engine,
|
|
267
269
|
Workflows: wfSvc.Registry(),
|
|
268
270
|
}, repoReg)
|
|
@@ -273,8 +275,8 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
273
275
|
// specs come from the engine so the recover path builds the same
|
|
274
276
|
// description content as normal run execution.
|
|
275
277
|
if recover {
|
|
276
|
-
specsFor := func(wf *workflow.Workflow
|
|
277
|
-
return goworkflows.
|
|
278
|
+
specsFor := func(sys task.System, work runsvc.Work, wf *workflow.Workflow) ([]task.MailboxSpec, error) {
|
|
279
|
+
return goworkflows.RenderMailboxSpecs(sys, work, wf)
|
|
278
280
|
}
|
|
279
281
|
if err := recoverpkg.FromTaskSystem(ctx, repoReg, rnr, runManager, specsFor); err != nil {
|
|
280
282
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Beads workflow example. Register the code repository with a required
|
|
2
|
+
# repo-scoped taskConfig.beadsDir before submitting this workflow, for example:
|
|
3
|
+
#
|
|
4
|
+
# relay-flow repo register --name payments --path /work/payments \
|
|
5
|
+
# --set beadsDir=/work/payments/.beads
|
|
6
|
+
#
|
|
7
|
+
# The Beads prefix is optional and is not a workspace/component selector.
|
|
8
|
+
name: beadsStoryFlow
|
|
9
|
+
repos:
|
|
10
|
+
- payments
|
|
11
|
+
cleanupRunnerOnEnd: true
|
|
12
|
+
|
|
13
|
+
taskConfig:
|
|
14
|
+
filters:
|
|
15
|
+
# Beads statuses are open, in_progress, blocked, deferred, hooked, closed.
|
|
16
|
+
parentStatuses:
|
|
17
|
+
- open
|
|
18
|
+
issueTypes:
|
|
19
|
+
- epic
|
|
20
|
+
labels:
|
|
21
|
+
- relay-ready
|
|
22
|
+
# assignees:
|
|
23
|
+
# - owner@example.com
|
|
24
|
+
# Optional shared default; filters.assignees overrides it when provided.
|
|
25
|
+
# An assignee in effect for a node also assigns that node's mailbox.
|
|
26
|
+
# assignee: owner@example.com
|
|
27
|
+
|
|
28
|
+
# transitionTo describes one lifecycle point, so configure it on a node.
|
|
29
|
+
# Omitting it entirely already gives the Jira-equivalent lifecycle:
|
|
30
|
+
# start parent -> in_progress, work mailbox -> in_progress, end parent -> closed.
|
|
31
|
+
# Values are Beads-native, not Jira values such as "In Progress" or "Done".
|
|
32
|
+
nodes:
|
|
33
|
+
start:
|
|
34
|
+
taskConfig:
|
|
35
|
+
transitionTo:
|
|
36
|
+
parentStatus: in_progress
|
|
37
|
+
onSuccess:
|
|
38
|
+
- target: implement
|
|
39
|
+
when: The Beads parent is ready for implementation
|
|
40
|
+
|
|
41
|
+
implement:
|
|
42
|
+
type: agent
|
|
43
|
+
agent: build
|
|
44
|
+
description: Implement the Beads parent and verify the changes.
|
|
45
|
+
taskConfig:
|
|
46
|
+
# assignee: developer@example.com
|
|
47
|
+
transitionTo:
|
|
48
|
+
taskStatus: in_progress
|
|
49
|
+
onSuccess:
|
|
50
|
+
- target: review
|
|
51
|
+
when: Implementation and verification are complete
|
|
52
|
+
onFailure:
|
|
53
|
+
- target: implement
|
|
54
|
+
when: Implementation needs another pass
|
|
55
|
+
|
|
56
|
+
review:
|
|
57
|
+
type: hitl
|
|
58
|
+
agent: plan
|
|
59
|
+
description: Review the implementation and obtain approval.
|
|
60
|
+
taskConfig:
|
|
61
|
+
# assignee: reviewer@example.com
|
|
62
|
+
transitionTo:
|
|
63
|
+
taskStatus: in_progress
|
|
64
|
+
onSuccess:
|
|
65
|
+
- target: end
|
|
66
|
+
when: The review is approved
|
|
67
|
+
onFailure:
|
|
68
|
+
- target: implement
|
|
69
|
+
when: Changes are requested
|
|
70
|
+
|
|
71
|
+
end:
|
|
72
|
+
taskConfig:
|
|
73
|
+
transitionTo:
|
|
74
|
+
parentStatus: closed
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Optional fields are shown commented; uncomment only the behavior you need.
|
|
3
3
|
name: defaultStoryFlow
|
|
4
4
|
repos:
|
|
5
|
-
-
|
|
5
|
+
- test-relay-flow
|
|
6
6
|
cleanupRunnerOnEnd: true
|
|
7
7
|
|
|
8
8
|
taskConfig:
|
|
@@ -33,7 +33,7 @@ nodes:
|
|
|
33
33
|
|
|
34
34
|
implement:
|
|
35
35
|
type: agent
|
|
36
|
-
agent:
|
|
36
|
+
agent: build
|
|
37
37
|
description: Implement the parent story and verify the changes.
|
|
38
38
|
# nudgePrompt: "Finish {{node}} for {{ticket}} and choose one of: {{nextSteps}}"
|
|
39
39
|
taskConfig:
|
|
@@ -50,13 +50,13 @@ nodes:
|
|
|
50
50
|
|
|
51
51
|
review:
|
|
52
52
|
type: agent
|
|
53
|
-
agent:
|
|
53
|
+
agent: plan
|
|
54
54
|
description: Review the implementation for correctness, regressions, and missing tests.
|
|
55
55
|
# nudgePrompt: "Complete the review for {{ticket}}. Valid routes: {{nextSteps}}"
|
|
56
56
|
taskConfig:
|
|
57
57
|
# assignee: reviewer@example.com
|
|
58
58
|
transitionTo:
|
|
59
|
-
taskStatus: In
|
|
59
|
+
taskStatus: In Progress
|
|
60
60
|
# parentStatus: In Review
|
|
61
61
|
onSuccess:
|
|
62
62
|
- target: prReview
|
|
@@ -67,13 +67,13 @@ nodes:
|
|
|
67
67
|
|
|
68
68
|
prReview:
|
|
69
69
|
type: hitl
|
|
70
|
-
agent:
|
|
70
|
+
agent: plan
|
|
71
71
|
description: Discuss the pull request with the human and obtain approval.
|
|
72
72
|
# nudgePrompt: "Review {{ticket}} with the human. Valid routes: {{nextSteps}}"
|
|
73
73
|
taskConfig:
|
|
74
74
|
# assignee: human-reviewer@example.com
|
|
75
75
|
transitionTo:
|
|
76
|
-
taskStatus: In
|
|
76
|
+
taskStatus: In Progress
|
|
77
77
|
# parentStatus: In Review
|
|
78
78
|
onSuccess:
|
|
79
79
|
- target: end
|
package/go.mod
CHANGED
|
@@ -2,10 +2,11 @@ module github.com/rajpopat27/relay-flow
|
|
|
2
2
|
|
|
3
3
|
go 1.24.6
|
|
4
4
|
|
|
5
|
-
require (
|
|
5
|
+
require (
|
|
6
6
|
github.com/cschleiden/go-workflows v1.4.2
|
|
7
7
|
github.com/google/renameio/v2 v2.0.1
|
|
8
8
|
github.com/google/uuid v1.6.0
|
|
9
|
+
github.com/yuin/goldmark v1.7.13
|
|
9
10
|
gopkg.in/yaml.v3 v3.0.1
|
|
10
11
|
)
|
|
11
12
|
|
package/go.sum
CHANGED
|
@@ -122,6 +122,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|
|
122
122
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
123
123
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
124
124
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
125
|
+
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
|
126
|
+
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
|
125
127
|
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
|
126
128
|
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
|
127
129
|
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
|
@@ -26,9 +26,9 @@ func Merge(values ...RawValues) RawValues {
|
|
|
26
26
|
|
|
27
27
|
func mergeInto(dst, src map[string]any) map[string]any {
|
|
28
28
|
for k, sv := range src {
|
|
29
|
-
if sm, ok := sv
|
|
29
|
+
if sm, ok := rawMap(sv); ok {
|
|
30
30
|
fresh := map[string]any{}
|
|
31
|
-
if dm, ok := dst[k]
|
|
31
|
+
if dm, ok := rawMap(dst[k]); ok {
|
|
32
32
|
mergeInto(fresh, dm)
|
|
33
33
|
}
|
|
34
34
|
dst[k] = mergeInto(fresh, sm)
|
|
@@ -39,6 +39,17 @@ func mergeInto(dst, src map[string]any) map[string]any {
|
|
|
39
39
|
return dst
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
func rawMap(value any) (map[string]any, bool) {
|
|
43
|
+
switch values := value.(type) {
|
|
44
|
+
case map[string]any:
|
|
45
|
+
return values, true
|
|
46
|
+
case RawValues:
|
|
47
|
+
return map[string]any(values), true
|
|
48
|
+
default:
|
|
49
|
+
return nil, false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
// DecodeStrict decodes raw values into dst, rejecting unknown fields and
|
|
43
54
|
// explicit null values.
|
|
44
55
|
func DecodeStrict(values RawValues, dst any) error {
|
|
@@ -49,6 +49,24 @@ func TestMergeMapsRecursively(t *testing.T) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
func TestMergeRawValuesMapsRecursively(t *testing.T) {
|
|
53
|
+
defaults := config.RawValues{"templates": map[string]any{
|
|
54
|
+
"mailboxDescription": "default mailbox",
|
|
55
|
+
"summaryComment": "default summary",
|
|
56
|
+
}}
|
|
57
|
+
override := config.RawValues{"templates": config.RawValues{
|
|
58
|
+
"mailboxDescription": "custom mailbox",
|
|
59
|
+
}}
|
|
60
|
+
got := config.Merge(defaults, override)
|
|
61
|
+
templates, ok := got["templates"].(map[string]any)
|
|
62
|
+
if !ok {
|
|
63
|
+
t.Fatalf("templates = %#v", got["templates"])
|
|
64
|
+
}
|
|
65
|
+
if templates["mailboxDescription"] != "custom mailbox" || templates["summaryComment"] != "default summary" {
|
|
66
|
+
t.Fatalf("templates = %#v", templates)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
func TestMergeListReplaces(t *testing.T) {
|
|
53
71
|
root := config.RawValues{"labels": []any{"a", "b"}}
|
|
54
72
|
wf := config.RawValues{"labels": []any{"c"}}
|
|
@@ -51,6 +51,16 @@ func (a *Activities) EnsureMailboxes(ctx context.Context, w run.Work, specs []ta
|
|
|
51
51
|
if err != nil {
|
|
52
52
|
return nil, err
|
|
53
53
|
}
|
|
54
|
+
for i := range specs {
|
|
55
|
+
data := specs[i].TextData
|
|
56
|
+
data.RunID = string(w.RunID)
|
|
57
|
+
data.Repo = w.Repo
|
|
58
|
+
custom, err := sys.RenderText(task.TextMailboxDescription, data)
|
|
59
|
+
if err != nil {
|
|
60
|
+
return nil, fmt.Errorf("render mailbox %q description: %w", specs[i].Node, err)
|
|
61
|
+
}
|
|
62
|
+
specs[i].Description = appendText(custom, specs[i].Description)
|
|
63
|
+
}
|
|
54
64
|
return sys.EnsureMailboxes(ctx, w.Parent, w.Workflow, specs)
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -147,60 +157,65 @@ func (a *Activities) EnsureNodeRuntime(ctx context.Context, nw run.NodeWork, rep
|
|
|
147
157
|
if err := a.Runner.SetEnvironmentStatus(ctx, env, status); err != nil {
|
|
148
158
|
return err
|
|
149
159
|
}
|
|
150
|
-
hadRuntime := currentRuntime.TerminalID != "" || currentRuntime.SessionID != ""
|
|
151
160
|
// IDs come from the guarded current row; the activity input's prior visit
|
|
152
161
|
// is used only to decide whether a live process needs rebinding.
|
|
153
162
|
rt.TerminalID = currentRuntime.TerminalID
|
|
154
163
|
rt.SessionID = currentRuntime.SessionID
|
|
155
164
|
spec.ResumeID = rt.SessionID
|
|
156
|
-
// Custom instructions belong to a node entry, not same-visit recovery.
|
|
157
|
-
if !hadRuntime || revisit {
|
|
158
|
-
spec.Prompt = appendPrompt(spec.Prompt, spec.NudgePrompt)
|
|
159
|
-
}
|
|
160
|
-
cmd, err := a.Harness.BuildCommand(spec)
|
|
161
|
-
if err != nil {
|
|
162
|
-
return err
|
|
163
|
-
}
|
|
164
165
|
stored := runner.Terminal{ID: rt.TerminalID, Title: spec.Title}
|
|
165
|
-
terminal, err := a.Runner.
|
|
166
|
+
terminal, live, err := a.Runner.FindTerminal(ctx, stored)
|
|
166
167
|
if err != nil {
|
|
167
168
|
return err
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
if live {
|
|
171
|
+
if err := a.Runs.replaceNodeRuntime(ctx, nw.RunID, nw.Node, nw.NodeVisitID,
|
|
172
|
+
terminal.ID, rt.SessionID, rt.SessionID); err != nil {
|
|
173
|
+
return err
|
|
174
|
+
}
|
|
175
|
+
if !revisit {
|
|
176
|
+
// Same-visit retry/restart is silent: do not render, build, or send.
|
|
177
|
+
return nil
|
|
178
|
+
}
|
|
179
|
+
prompt, err := a.Harness.RenderPrompt(harness.PromptFeedback, spec.PromptData, spec.NudgePrompt)
|
|
180
|
+
if err != nil {
|
|
181
|
+
return err
|
|
182
|
+
}
|
|
183
|
+
if err := a.Runner.SendTerminal(ctx, terminal, prompt); err == nil {
|
|
184
|
+
return nil
|
|
185
|
+
}
|
|
186
|
+
// Direct use failed. Close the known live terminal before replacing it
|
|
187
|
+
// so a second agent process cannot be left running.
|
|
188
|
+
if err := a.Runner.CloseTerminal(ctx, terminal); err != nil {
|
|
189
|
+
return err
|
|
177
190
|
}
|
|
178
|
-
return err
|
|
179
|
-
}
|
|
180
|
-
if terminal.ID != rt.TerminalID || !revisit {
|
|
181
|
-
// A newly created terminal receives its prompt in the launch command;
|
|
182
|
-
// same-visit reuse leaves the running turn untouched.
|
|
183
|
-
return nil
|
|
184
191
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
192
|
+
|
|
193
|
+
// An initial or replacement terminal resumes the stored session and
|
|
194
|
+
// receives the rendered initial prompt. Same-visit replacements omit the
|
|
195
|
+
// node nudge; a new visit includes it.
|
|
196
|
+
nudge := ""
|
|
197
|
+
if rt.NodeVisitID == "" || revisit {
|
|
198
|
+
nudge = spec.NudgePrompt
|
|
188
199
|
}
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
spec.Prompt, err = a.Harness.RenderPrompt(harness.PromptInitial, spec.PromptData, nudge)
|
|
201
|
+
if err != nil {
|
|
202
|
+
return err
|
|
191
203
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if err := a.Runner.CloseTerminal(ctx, terminal); err != nil {
|
|
204
|
+
cmd, err := a.Harness.BuildCommand(spec)
|
|
205
|
+
if err != nil {
|
|
195
206
|
return err
|
|
196
207
|
}
|
|
197
|
-
replacement, err := a.Runner.EnsureTerminal(ctx, env,
|
|
208
|
+
replacement, err := a.Runner.EnsureTerminal(ctx, env, stored, spec.Title, cmd)
|
|
198
209
|
if err != nil {
|
|
199
210
|
return err
|
|
200
211
|
}
|
|
212
|
+
// Persist a newly created/replacement handle before any later external
|
|
213
|
+
// effect. Runtime session registration may update SessionID independently.
|
|
201
214
|
if err := a.Runs.replaceNodeRuntime(ctx, nw.RunID, nw.Node, nw.NodeVisitID,
|
|
202
215
|
replacement.ID, rt.SessionID, rt.SessionID); err != nil {
|
|
203
|
-
|
|
216
|
+
if replacement.ID != rt.TerminalID {
|
|
217
|
+
_ = a.Runner.CloseTerminal(ctx, replacement)
|
|
218
|
+
}
|
|
204
219
|
return err
|
|
205
220
|
}
|
|
206
221
|
return nil
|
|
@@ -276,7 +291,14 @@ func (a *Activities) Comment(ctx context.Context, repoName string, cw run.Commen
|
|
|
276
291
|
if err != nil {
|
|
277
292
|
return err
|
|
278
293
|
}
|
|
279
|
-
|
|
294
|
+
body := cw.Body
|
|
295
|
+
if cw.TextKind != "" {
|
|
296
|
+
body, err = sys.RenderText(cw.TextKind, cw.TextData)
|
|
297
|
+
if err != nil {
|
|
298
|
+
return fmt.Errorf("render %s: %w", cw.TextKind, err)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if err := sys.Comment(ctx, cw.Item, body, cw.Marker); err != nil {
|
|
280
302
|
return err
|
|
281
303
|
}
|
|
282
304
|
// Log AFTER the write succeeds so the line is a true effect record.
|
|
@@ -395,17 +417,7 @@ func (a *Activities) ProjectionUpdateRetry(ctx context.Context, id run.ID, statu
|
|
|
395
417
|
// description, and every legal route with its when explanation.
|
|
396
418
|
func MailboxSpecForNode(wf *workflow.Workflow, ticketKey, name string, n workflow.Node) task.MailboxSpec {
|
|
397
419
|
var b strings.Builder
|
|
398
|
-
|
|
399
|
-
ticketKey, name, n.Type, n.Agent, n.Description)
|
|
400
|
-
if n.Type == workflow.NodeHITL {
|
|
401
|
-
b.WriteString(`
|
|
402
|
-
|
|
403
|
-
1. Discuss the task with the human, request the PR link or any missing context, and review the changes. Do not make code changes.
|
|
404
|
-
2. Resolve questions and requested review updates through normal conversation until the human is satisfied with the review.`)
|
|
405
|
-
}
|
|
406
|
-
b.WriteString(`
|
|
407
|
-
|
|
408
|
-
Required report format:
|
|
420
|
+
b.WriteString(`Required report format:
|
|
409
421
|
|
|
410
422
|
STATUS: success | failure
|
|
411
423
|
NEXT STEP: <one valid node name>
|
|
@@ -424,7 +436,9 @@ REQUIRED ACTIONS:
|
|
|
424
436
|
RELEVANT CONTEXT:
|
|
425
437
|
EXPECTED RESULT:
|
|
426
438
|
|
|
427
|
-
Every field is required; use None for an intentionally empty section. COMMITS must contain the relevant commit IDs or None.
|
|
439
|
+
Every field is required; use None for an intentionally empty section. COMMITS must contain the relevant commit IDs or None.
|
|
440
|
+
|
|
441
|
+
Node names identify workflow stages; they are not task-system statuses. STATUS describes whether the work at this node succeeded or failed, not the status of the parent or mailbox. NEXT STEP must name exactly one target listed below for that STATUS. Submit one report only: its SUMMARY is written to this current mailbox, while its FEEDBACK is written only to the selected next node's mailbox. For review nodes, put requested changes in FEEDBACK and select the node responsible for acting on them. Relay-flow and the task system own parent and mailbox status changes. When NEXT STEP is end, every FEEDBACK field must be None.`)
|
|
428
442
|
writeRoutes := func(label string, routes []workflow.Route) {
|
|
429
443
|
if len(routes) == 0 {
|
|
430
444
|
return
|
|
@@ -440,11 +454,20 @@ Every field is required; use None for an intentionally empty section. COMMITS mu
|
|
|
440
454
|
}
|
|
441
455
|
writeRoutes("On success", n.OnSuccess)
|
|
442
456
|
writeRoutes("On failure", n.OnFailure)
|
|
457
|
+
successRoutes := routesText(n.OnSuccess)
|
|
458
|
+
failureRoutes := routesText(n.OnFailure)
|
|
443
459
|
return task.MailboxSpec{
|
|
444
460
|
Node: name,
|
|
445
461
|
Title: ticketKey + ":" + name,
|
|
446
462
|
Description: b.String(),
|
|
447
463
|
TaskConfig: n.TaskConfig,
|
|
464
|
+
TextData: task.TextData{
|
|
465
|
+
Ticket: ticketKey, Workflow: wf.Name, Node: name, NodeType: string(n.Type),
|
|
466
|
+
Agent: n.Agent, NodeDescription: n.Description,
|
|
467
|
+
NextSteps: nextStepsText(append(append([]workflow.Route{}, n.OnSuccess...), n.OnFailure...)),
|
|
468
|
+
SuccessRoutes: successRoutes, FailureRoutes: failureRoutes,
|
|
469
|
+
Mailbox: ticketKey + ":" + name,
|
|
470
|
+
},
|
|
448
471
|
}
|
|
449
472
|
}
|
|
450
473
|
|
|
@@ -464,20 +487,46 @@ func MailboxSpecs(wf *workflow.Workflow, ticketKey string) []task.MailboxSpec {
|
|
|
464
487
|
return out
|
|
465
488
|
}
|
|
466
489
|
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
490
|
+
// RenderMailboxSpecs asks the selected task system to render customizable
|
|
491
|
+
// mailbox text, then appends the fixed report contract and legal routes.
|
|
492
|
+
// It is shared by normal execution and explicit database-loss recovery.
|
|
493
|
+
func RenderMailboxSpecs(sys task.System, w run.Work, wf *workflow.Workflow) ([]task.MailboxSpec, error) {
|
|
494
|
+
specs := MailboxSpecs(wf, w.Parent.Key)
|
|
495
|
+
for i := range specs {
|
|
496
|
+
data := specs[i].TextData
|
|
497
|
+
data.RunID = string(w.RunID)
|
|
498
|
+
data.Repo = w.Repo
|
|
499
|
+
custom, err := sys.RenderText(task.TextMailboxDescription, data)
|
|
500
|
+
if err != nil {
|
|
501
|
+
return nil, fmt.Errorf("render mailbox %q description: %w", specs[i].Node, err)
|
|
502
|
+
}
|
|
503
|
+
specs[i].Description = appendText(custom, specs[i].Description)
|
|
504
|
+
}
|
|
505
|
+
return specs, nil
|
|
470
506
|
}
|
|
471
507
|
|
|
472
|
-
func
|
|
473
|
-
|
|
508
|
+
func routesText(routes []workflow.Route) string {
|
|
509
|
+
var b strings.Builder
|
|
510
|
+
for i, route := range routes {
|
|
511
|
+
if i > 0 {
|
|
512
|
+
b.WriteString("\n")
|
|
513
|
+
}
|
|
514
|
+
b.WriteString(route.Target)
|
|
515
|
+
if route.When != "" {
|
|
516
|
+
b.WriteString(" — when: " + route.When)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return b.String()
|
|
474
520
|
}
|
|
475
521
|
|
|
476
|
-
func
|
|
477
|
-
if
|
|
478
|
-
return
|
|
522
|
+
func appendText(first, second string) string {
|
|
523
|
+
if first == "" {
|
|
524
|
+
return second
|
|
525
|
+
}
|
|
526
|
+
if second == "" {
|
|
527
|
+
return first
|
|
479
528
|
}
|
|
480
|
-
return
|
|
529
|
+
return first + "\n\n" + second
|
|
481
530
|
}
|
|
482
531
|
|
|
483
532
|
// mergeTaskConfig overlays node task config onto workflow task config using
|