relay-flow 0.2.1-alpha → 0.2.3-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 +193 -25
- package/cmd/relay-flow/beads_composition_test.go +451 -0
- package/cmd/relay-flow/commands_test.go +79 -1
- package/cmd/relay-flow/main.go +50 -1
- package/cmd/relay-flow/scenario_test.go +48 -12
- package/cmd/relay-flow/serve.go +16 -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 +124 -56
- package/internal/execution/goworkflows/end_feedback_test.go +124 -0
- package/internal/execution/goworkflows/engine.go +20 -0
- package/internal/execution/goworkflows/engine_test.go +33 -15
- package/internal/execution/goworkflows/fakes_test.go +84 -10
- package/internal/execution/goworkflows/interpreter.go +84 -30
- package/internal/execution/goworkflows/mailbox_test.go +85 -0
- package/internal/execution/goworkflows/node_runtime_test.go +30 -5
- package/internal/execution/goworkflows/projection.go +52 -11
- package/internal/execution/goworkflows/recovery_test.go +131 -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 +238 -0
- package/internal/harness/opencode/repo_setup.go +401 -0
- package/internal/harness/plugin_selection_test.go +5 -5
- package/internal/identity/identity.go +28 -1
- package/internal/identity/identity_test.go +40 -0
- 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/manager.go +193 -25
- package/internal/run/run.go +30 -10
- package/internal/run/run_manager_test.go +100 -0
- package/internal/server/api_test.go +54 -0
- package/internal/server/client.go +11 -0
- package/internal/server/fixture_test.go +18 -0
- package/internal/server/server.go +16 -0
- 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 +856 -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 +276 -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/helpers_test.go +3 -0
- package/internal/task/jira/jira.go +187 -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 +47 -2
- package/internal/task/task.go +40 -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
|
|
|
@@ -248,6 +249,8 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
248
249
|
// the SAME registry by pointer (via replaceInternal), so the engine,
|
|
249
250
|
// pollers, and handlers observe one in-memory repo set.
|
|
250
251
|
wfSvc := workflow.NewService(store, engine, repoExists{repoReg})
|
|
252
|
+
runManager.Repos = repoReg
|
|
253
|
+
runManager.Workflows = wfSvc.Registry()
|
|
251
254
|
wfSvc.Gate = lifecycleGate
|
|
252
255
|
wfSvc.ValidateTaskConfig = workflowConfigValidator(repoReg)
|
|
253
256
|
// Submit/Remove must also rebuild repo bindings under the gate (spec
|
|
@@ -263,6 +266,7 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
263
266
|
ConfigPath: p.Config,
|
|
264
267
|
TaskPlugin: cfg.TaskPlugin,
|
|
265
268
|
Runner: rnr,
|
|
269
|
+
Harness: hrn,
|
|
266
270
|
Active: engine,
|
|
267
271
|
Workflows: wfSvc.Registry(),
|
|
268
272
|
}, repoReg)
|
|
@@ -273,8 +277,8 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
273
277
|
// specs come from the engine so the recover path builds the same
|
|
274
278
|
// description content as normal run execution.
|
|
275
279
|
if recover {
|
|
276
|
-
specsFor := func(wf *workflow.Workflow
|
|
277
|
-
return goworkflows.
|
|
280
|
+
specsFor := func(sys task.System, work runsvc.Work, wf *workflow.Workflow) ([]task.MailboxSpec, error) {
|
|
281
|
+
return goworkflows.RenderMailboxSpecs(sys, work, wf)
|
|
278
282
|
}
|
|
279
283
|
if err := recoverpkg.FromTaskSystem(ctx, repoReg, rnr, runManager, specsFor); err != nil {
|
|
280
284
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
@@ -453,6 +457,16 @@ func (d *serveDeps) ListRuns(ctx context.Context, filter runsvc.Filter) ([]runsv
|
|
|
453
457
|
func (d *serveDeps) GetRunByTicket(ctx context.Context, ticket string) (runsvc.Run, error) {
|
|
454
458
|
return d.engine.FindRunByTicket(ctx, ticket)
|
|
455
459
|
}
|
|
460
|
+
func (d *serveDeps) RestartRun(ctx context.Context, ticket string) (runsvc.Run, error) {
|
|
461
|
+
rn, err := d.runManager.RestartByTicket(ctx, ticket)
|
|
462
|
+
if err != nil {
|
|
463
|
+
if errors.Is(err, runsvc.ErrRestartConflict) {
|
|
464
|
+
return runsvc.Run{}, fmt.Errorf("%w: %v", server.ErrConflict, err)
|
|
465
|
+
}
|
|
466
|
+
return runsvc.Run{}, err
|
|
467
|
+
}
|
|
468
|
+
return rn, nil
|
|
469
|
+
}
|
|
456
470
|
func (d *serveDeps) CancelRun(ctx context.Context, ticket, reason string) error {
|
|
457
471
|
return d.runManager.CancelByTicket(ctx, ticket, reason)
|
|
458
472
|
}
|
|
@@ -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,9 +51,38 @@ 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
|
|
|
67
|
+
// PrepareRestart reopens mailbox state through the optional task-system
|
|
68
|
+
// capability, then closes any surviving run-owned terminals while preserving
|
|
69
|
+
// the ticket worktree. Both operations are idempotent/retryable and remain
|
|
70
|
+
// behind their respective task and runner interfaces.
|
|
71
|
+
func (a *Activities) PrepareRestart(ctx context.Context, w run.Work, repoPath string, mailboxes []task.Mailbox) error {
|
|
72
|
+
sys, err := a.taskSystem(w.Repo)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return err
|
|
75
|
+
}
|
|
76
|
+
if preparer, ok := sys.(task.RestartPreparer); ok {
|
|
77
|
+
if err := preparer.PrepareRestart(ctx, w.Parent, mailboxes); err != nil {
|
|
78
|
+
return err
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
spec := a.runSpec(w)
|
|
82
|
+
spec.RepoPath = repoPath
|
|
83
|
+
return a.Runner.CloseTerminals(ctx, spec)
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
// ValidateAgents validates every referenced agent on the repo.
|
|
58
87
|
func (a *Activities) ValidateAgents(ctx context.Context, repoPath string, agents []string) error {
|
|
59
88
|
for _, agent := range agents {
|
|
@@ -147,60 +176,65 @@ func (a *Activities) EnsureNodeRuntime(ctx context.Context, nw run.NodeWork, rep
|
|
|
147
176
|
if err := a.Runner.SetEnvironmentStatus(ctx, env, status); err != nil {
|
|
148
177
|
return err
|
|
149
178
|
}
|
|
150
|
-
hadRuntime := currentRuntime.TerminalID != "" || currentRuntime.SessionID != ""
|
|
151
179
|
// IDs come from the guarded current row; the activity input's prior visit
|
|
152
180
|
// is used only to decide whether a live process needs rebinding.
|
|
153
181
|
rt.TerminalID = currentRuntime.TerminalID
|
|
154
182
|
rt.SessionID = currentRuntime.SessionID
|
|
155
183
|
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
184
|
stored := runner.Terminal{ID: rt.TerminalID, Title: spec.Title}
|
|
165
|
-
terminal, err := a.Runner.
|
|
185
|
+
terminal, live, err := a.Runner.FindTerminal(ctx, stored)
|
|
166
186
|
if err != nil {
|
|
167
187
|
return err
|
|
168
188
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
if live {
|
|
190
|
+
if err := a.Runs.replaceNodeRuntime(ctx, nw.RunID, nw.Node, nw.NodeVisitID,
|
|
191
|
+
terminal.ID, rt.SessionID, rt.SessionID); err != nil {
|
|
192
|
+
return err
|
|
193
|
+
}
|
|
194
|
+
if !revisit {
|
|
195
|
+
// Same-visit retry/restart is silent: do not render, build, or send.
|
|
196
|
+
return nil
|
|
197
|
+
}
|
|
198
|
+
prompt, err := a.Harness.RenderPrompt(harness.PromptFeedback, spec.PromptData, spec.NudgePrompt)
|
|
199
|
+
if err != nil {
|
|
200
|
+
return err
|
|
201
|
+
}
|
|
202
|
+
if err := a.Runner.SendTerminal(ctx, terminal, prompt); err == nil {
|
|
203
|
+
return nil
|
|
204
|
+
}
|
|
205
|
+
// Direct use failed. Close the known live terminal before replacing it
|
|
206
|
+
// so a second agent process cannot be left running.
|
|
207
|
+
if err := a.Runner.CloseTerminal(ctx, terminal); err != nil {
|
|
208
|
+
return err
|
|
177
209
|
}
|
|
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
210
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
211
|
+
|
|
212
|
+
// An initial or replacement terminal resumes the stored session and
|
|
213
|
+
// receives the rendered initial prompt. Same-visit replacements omit the
|
|
214
|
+
// node nudge; a new visit includes it.
|
|
215
|
+
nudge := ""
|
|
216
|
+
if rt.NodeVisitID == "" || revisit {
|
|
217
|
+
nudge = spec.NudgePrompt
|
|
188
218
|
}
|
|
189
|
-
|
|
190
|
-
|
|
219
|
+
spec.Prompt, err = a.Harness.RenderPrompt(harness.PromptInitial, spec.PromptData, nudge)
|
|
220
|
+
if err != nil {
|
|
221
|
+
return err
|
|
191
222
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if err := a.Runner.CloseTerminal(ctx, terminal); err != nil {
|
|
223
|
+
cmd, err := a.Harness.BuildCommand(spec)
|
|
224
|
+
if err != nil {
|
|
195
225
|
return err
|
|
196
226
|
}
|
|
197
|
-
replacement, err := a.Runner.EnsureTerminal(ctx, env,
|
|
227
|
+
replacement, err := a.Runner.EnsureTerminal(ctx, env, stored, spec.Title, cmd)
|
|
198
228
|
if err != nil {
|
|
199
229
|
return err
|
|
200
230
|
}
|
|
231
|
+
// Persist a newly created/replacement handle before any later external
|
|
232
|
+
// effect. Runtime session registration may update SessionID independently.
|
|
201
233
|
if err := a.Runs.replaceNodeRuntime(ctx, nw.RunID, nw.Node, nw.NodeVisitID,
|
|
202
234
|
replacement.ID, rt.SessionID, rt.SessionID); err != nil {
|
|
203
|
-
|
|
235
|
+
if replacement.ID != rt.TerminalID {
|
|
236
|
+
_ = a.Runner.CloseTerminal(ctx, replacement)
|
|
237
|
+
}
|
|
204
238
|
return err
|
|
205
239
|
}
|
|
206
240
|
return nil
|
|
@@ -276,7 +310,14 @@ func (a *Activities) Comment(ctx context.Context, repoName string, cw run.Commen
|
|
|
276
310
|
if err != nil {
|
|
277
311
|
return err
|
|
278
312
|
}
|
|
279
|
-
|
|
313
|
+
body := cw.Body
|
|
314
|
+
if cw.TextKind != "" {
|
|
315
|
+
body, err = sys.RenderText(cw.TextKind, cw.TextData)
|
|
316
|
+
if err != nil {
|
|
317
|
+
return fmt.Errorf("render %s: %w", cw.TextKind, err)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if err := sys.Comment(ctx, cw.Item, body, cw.Marker); err != nil {
|
|
280
321
|
return err
|
|
281
322
|
}
|
|
282
323
|
// Log AFTER the write succeeds so the line is a true effect record.
|
|
@@ -395,17 +436,7 @@ func (a *Activities) ProjectionUpdateRetry(ctx context.Context, id run.ID, statu
|
|
|
395
436
|
// description, and every legal route with its when explanation.
|
|
396
437
|
func MailboxSpecForNode(wf *workflow.Workflow, ticketKey, name string, n workflow.Node) task.MailboxSpec {
|
|
397
438
|
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:
|
|
439
|
+
b.WriteString(`Required report format:
|
|
409
440
|
|
|
410
441
|
STATUS: success | failure
|
|
411
442
|
NEXT STEP: <one valid node name>
|
|
@@ -424,7 +455,9 @@ REQUIRED ACTIONS:
|
|
|
424
455
|
RELEVANT CONTEXT:
|
|
425
456
|
EXPECTED RESULT:
|
|
426
457
|
|
|
427
|
-
Every field is required; use None for an intentionally empty section. COMMITS must contain the relevant commit IDs or None.
|
|
458
|
+
Every field is required; use None for an intentionally empty section. COMMITS must contain the relevant commit IDs or None.
|
|
459
|
+
|
|
460
|
+
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
461
|
writeRoutes := func(label string, routes []workflow.Route) {
|
|
429
462
|
if len(routes) == 0 {
|
|
430
463
|
return
|
|
@@ -440,11 +473,20 @@ Every field is required; use None for an intentionally empty section. COMMITS mu
|
|
|
440
473
|
}
|
|
441
474
|
writeRoutes("On success", n.OnSuccess)
|
|
442
475
|
writeRoutes("On failure", n.OnFailure)
|
|
476
|
+
successRoutes := routesText(n.OnSuccess)
|
|
477
|
+
failureRoutes := routesText(n.OnFailure)
|
|
443
478
|
return task.MailboxSpec{
|
|
444
479
|
Node: name,
|
|
445
480
|
Title: ticketKey + ":" + name,
|
|
446
481
|
Description: b.String(),
|
|
447
482
|
TaskConfig: n.TaskConfig,
|
|
483
|
+
TextData: task.TextData{
|
|
484
|
+
Ticket: ticketKey, Workflow: wf.Name, Node: name, NodeType: string(n.Type),
|
|
485
|
+
Agent: n.Agent, NodeDescription: n.Description,
|
|
486
|
+
NextSteps: nextStepsText(append(append([]workflow.Route{}, n.OnSuccess...), n.OnFailure...)),
|
|
487
|
+
SuccessRoutes: successRoutes, FailureRoutes: failureRoutes,
|
|
488
|
+
Mailbox: ticketKey + ":" + name,
|
|
489
|
+
},
|
|
448
490
|
}
|
|
449
491
|
}
|
|
450
492
|
|
|
@@ -464,20 +506,46 @@ func MailboxSpecs(wf *workflow.Workflow, ticketKey string) []task.MailboxSpec {
|
|
|
464
506
|
return out
|
|
465
507
|
}
|
|
466
508
|
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
509
|
+
// RenderMailboxSpecs asks the selected task system to render customizable
|
|
510
|
+
// mailbox text, then appends the fixed report contract and legal routes.
|
|
511
|
+
// It is shared by normal execution and explicit database-loss recovery.
|
|
512
|
+
func RenderMailboxSpecs(sys task.System, w run.Work, wf *workflow.Workflow) ([]task.MailboxSpec, error) {
|
|
513
|
+
specs := MailboxSpecs(wf, w.Parent.Key)
|
|
514
|
+
for i := range specs {
|
|
515
|
+
data := specs[i].TextData
|
|
516
|
+
data.RunID = string(w.RunID)
|
|
517
|
+
data.Repo = w.Repo
|
|
518
|
+
custom, err := sys.RenderText(task.TextMailboxDescription, data)
|
|
519
|
+
if err != nil {
|
|
520
|
+
return nil, fmt.Errorf("render mailbox %q description: %w", specs[i].Node, err)
|
|
521
|
+
}
|
|
522
|
+
specs[i].Description = appendText(custom, specs[i].Description)
|
|
523
|
+
}
|
|
524
|
+
return specs, nil
|
|
470
525
|
}
|
|
471
526
|
|
|
472
|
-
func
|
|
473
|
-
|
|
527
|
+
func routesText(routes []workflow.Route) string {
|
|
528
|
+
var b strings.Builder
|
|
529
|
+
for i, route := range routes {
|
|
530
|
+
if i > 0 {
|
|
531
|
+
b.WriteString("\n")
|
|
532
|
+
}
|
|
533
|
+
b.WriteString(route.Target)
|
|
534
|
+
if route.When != "" {
|
|
535
|
+
b.WriteString(" — when: " + route.When)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return b.String()
|
|
474
539
|
}
|
|
475
540
|
|
|
476
|
-
func
|
|
477
|
-
if
|
|
478
|
-
return
|
|
541
|
+
func appendText(first, second string) string {
|
|
542
|
+
if first == "" {
|
|
543
|
+
return second
|
|
544
|
+
}
|
|
545
|
+
if second == "" {
|
|
546
|
+
return first
|
|
479
547
|
}
|
|
480
|
-
return
|
|
548
|
+
return first + "\n\n" + second
|
|
481
549
|
}
|
|
482
550
|
|
|
483
551
|
// mergeTaskConfig overlays node task config onto workflow task config using
|