relay-flow 0.3.10-alpha → 0.3.11-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 +10 -22
- package/cmd/relay-flow/commands_test.go +9 -2
- package/cmd/relay-flow/main.go +4 -0
- package/examples/config-reference.yaml +23 -19
- package/examples/workflow-reference.yaml +3 -0
- package/internal/execution/goworkflows/activities.go +13 -22
- package/internal/execution/goworkflows/engine_test.go +17 -3
- package/internal/execution/goworkflows/interpreter.go +24 -11
- package/internal/execution/goworkflows/mailbox_test.go +28 -1
- package/internal/execution/temporal/activities.go +13 -22
- package/internal/execution/temporal/compact_mailbox_test.go +56 -0
- package/internal/execution/temporal/interpreter.go +14 -2
- package/internal/harness/harness.go +22 -10
- package/internal/harness/opencode/opencode.go +17 -4
- package/internal/harness/opencode/opencode_test.go +25 -1
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/harness/opencode/task_env_test.go +2 -2
- package/internal/harness/pi/pi.go +15 -2
- package/internal/harness/pi/pi_test.go +7 -4
- package/internal/harness/pi/prompt_test.go +13 -5
- package/internal/harness/pi/task_env_test.go +2 -2
- package/internal/task/jira/jira.go +16 -11
- package/internal/task/jira/templates_test.go +38 -2
- package/internal/task/task.go +1 -0
- package/internal/workflow/report.go +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,13 +28,13 @@ same `relay-flow-plugin` package with host-specific entrypoints.
|
|
|
28
28
|
**OpenCode**
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
|
-
opencode plugin relay-flow-plugin@0.3.
|
|
31
|
+
opencode plugin relay-flow-plugin@0.3.11-alpha
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
**Pi**
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
pi install npm:relay-flow-plugin@0.3.
|
|
37
|
+
pi install npm:relay-flow-plugin@0.3.11-alpha
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Pi loads the package's `pi.ts` extension from its manifest. Do not add
|
|
@@ -164,7 +164,7 @@ OpenCode plugin configuration uses both entrypoints. The server entrypoint is li
|
|
|
164
164
|
```json
|
|
165
165
|
{
|
|
166
166
|
"$schema": "https://opencode.ai/config.json",
|
|
167
|
-
"plugin": ["relay-flow-plugin@0.3.
|
|
167
|
+
"plugin": ["relay-flow-plugin@0.3.11-alpha"]
|
|
168
168
|
}
|
|
169
169
|
```
|
|
170
170
|
|
|
@@ -173,7 +173,7 @@ The native HITL approval entrypoint is listed in `.opencode/tui.json`:
|
|
|
173
173
|
```json
|
|
174
174
|
{
|
|
175
175
|
"$schema": "https://opencode.ai/tui.json",
|
|
176
|
-
"plugin": ["relay-flow-plugin@0.3.
|
|
176
|
+
"plugin": ["relay-flow-plugin@0.3.11-alpha"]
|
|
177
177
|
}
|
|
178
178
|
```
|
|
179
179
|
|
|
@@ -189,7 +189,7 @@ Pi plugin: install the same published package manually in Pi's global package
|
|
|
189
189
|
settings before starting a Pi harness session:
|
|
190
190
|
|
|
191
191
|
```sh
|
|
192
|
-
pi install npm:relay-flow-plugin@0.3.
|
|
192
|
+
pi install npm:relay-flow-plugin@0.3.11-alpha
|
|
193
193
|
```
|
|
194
194
|
|
|
195
195
|
Relay-flow does not install or configure the package automatically. Pi resolves
|
|
@@ -614,28 +614,16 @@ durable run waiting; Pi does not require an LLM Question tool for this step.
|
|
|
614
614
|
|
|
615
615
|
## Structured node report
|
|
616
616
|
|
|
617
|
-
Every visit (agent or HITL) submits one report with
|
|
617
|
+
Every visit (agent or HITL) submits one report with four fixed fields:
|
|
618
618
|
|
|
619
619
|
```
|
|
620
620
|
STATUS: success | failure
|
|
621
|
-
NEXT STEP: <one
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
COMPLETED: ...
|
|
625
|
-
COMMITS: <commit IDs or None>
|
|
626
|
-
NOT COMPLETED: ... | None
|
|
627
|
-
ISSUES DISCOVERED: ... | None
|
|
628
|
-
VERIFICATION: ...
|
|
629
|
-
NOTES: ... | None
|
|
630
|
-
|
|
631
|
-
FEEDBACK:
|
|
632
|
-
REASON FOR NEXT STEP: ...
|
|
633
|
-
REQUIRED ACTIONS: ...
|
|
634
|
-
RELEVANT CONTEXT: ...
|
|
635
|
-
EXPECTED RESULT: ...
|
|
621
|
+
NEXT STEP: <one valid route>
|
|
622
|
+
SUMMARY: <concise result>
|
|
623
|
+
FEEDBACK: <concise handoff, or None when NEXT STEP is end>
|
|
636
624
|
```
|
|
637
625
|
|
|
638
|
-
The
|
|
626
|
+
The report format is hardcoded, exposed as `{{report}}` in Jira mailbox templates and passed to the runtime plugin through `RELAY_FLOW_REPORT_FORMAT`; configurable templates do not change it. The plugin maps `SUMMARY` to `report.summary.completed` and `FEEDBACK` to `report.feedback.requiredActions`, filling the remaining internal JSON fields with `None`. Relay-flow validates the structured report and writes the current summary to its mailbox and only the selected feedback to the next mailbox. When `NEXT STEP` is `end`, `FEEDBACK` must be exactly `None` and no feedback comment is written.
|
|
639
627
|
|
|
640
628
|
The plugin delivers `{runId, node, reportId, report}` as one JSON object via `relay-flow report` stdin with the shared backoff (initial 2s, factor 2, jitter 0.2, max 5m) until acknowledged. It derives `reportId` from the harness session/message identity. Duplicate/stale reports are acked safely with no repeated graph effects. Invalid agent output is nudged; ordinary, missing, or empty HITL output stays silent, while partial report-shaped HITL output is corrected and a valid HITL report opens the native TUI approval dialog. Relay-flow HITL approval does not use OpenCode's Question tool.
|
|
641
629
|
|
|
@@ -23,6 +23,7 @@ import (
|
|
|
23
23
|
|
|
24
24
|
"github.com/charmbracelet/huh"
|
|
25
25
|
"github.com/rajpopat27/relay-flow/internal/config"
|
|
26
|
+
"github.com/rajpopat27/relay-flow/internal/harness"
|
|
26
27
|
"github.com/rajpopat27/relay-flow/internal/repo"
|
|
27
28
|
runsvc "github.com/rajpopat27/relay-flow/internal/run"
|
|
28
29
|
"github.com/rajpopat27/relay-flow/internal/runner"
|
|
@@ -386,6 +387,9 @@ func TestInitWritesHarnessPromptDefaults(t *testing.T) {
|
|
|
386
387
|
t.Fatalf("harnessConfig.%s = %#v", key, cfg.HarnessConfig[key])
|
|
387
388
|
}
|
|
388
389
|
}
|
|
390
|
+
if cfg.HarnessConfig["initial"] != harness.JiraInitialPrompt || cfg.HarnessConfig["feedback"] != harness.JiraFeedbackPrompt {
|
|
391
|
+
t.Fatalf("Jira prompt defaults = %#v", cfg.HarnessConfig)
|
|
392
|
+
}
|
|
389
393
|
}
|
|
390
394
|
|
|
391
395
|
func TestInitWritesTaskTextDefaults(t *testing.T) {
|
|
@@ -406,6 +410,9 @@ func TestInitWritesTaskTextDefaults(t *testing.T) {
|
|
|
406
410
|
t.Fatalf("taskConfig.templates.%s = %#v", key, templates[key])
|
|
407
411
|
}
|
|
408
412
|
}
|
|
413
|
+
if !strings.Contains(templates["mailboxDescription"].(string), "{{report}}") {
|
|
414
|
+
t.Fatal("Jira mailbox description must expose the canonical report")
|
|
415
|
+
}
|
|
409
416
|
}
|
|
410
417
|
|
|
411
418
|
func TestInitDoesNotCollectOrWriteTaskCredentials(t *testing.T) {
|
|
@@ -480,7 +487,7 @@ func TestInitForcePreservesDurableAndUserState(t *testing.T) {
|
|
|
480
487
|
if !ok {
|
|
481
488
|
t.Fatalf("taskConfig.templates = %#v", cfg.TaskConfig["templates"])
|
|
482
489
|
}
|
|
483
|
-
taskTemplates["mailboxDescription"] = "custom {{nodeDescription}}"
|
|
490
|
+
taskTemplates["mailboxDescription"] = "custom {{ticket}} / {{node}} — {{nodeType}} — {{agent}}\n{{nodeDescription}}\n{{successRoutes}}\n{{failureRoutes}}\n{{report}}"
|
|
484
491
|
if err := config.SaveMachine(filepath.Join(root, "config.yaml"), cfg); err != nil {
|
|
485
492
|
t.Fatal(err)
|
|
486
493
|
}
|
|
@@ -528,7 +535,7 @@ func TestInitForcePreservesDurableAndUserState(t *testing.T) {
|
|
|
528
535
|
if !ok {
|
|
529
536
|
t.Fatalf("taskConfig.templates = %#v", cfg.TaskConfig["templates"])
|
|
530
537
|
}
|
|
531
|
-
if got := gotTaskTemplates["mailboxDescription"]; got != "custom {{nodeDescription}}" {
|
|
538
|
+
if got := gotTaskTemplates["mailboxDescription"]; got != "custom {{ticket}} / {{node}} — {{nodeType}} — {{agent}}\n{{nodeDescription}}\n{{successRoutes}}\n{{failureRoutes}}\n{{report}}" {
|
|
532
539
|
t.Fatalf("forced init changed task text override: %#v", got)
|
|
533
540
|
}
|
|
534
541
|
for _, key := range []string{"summaryComment", "feedbackComment"} {
|
package/cmd/relay-flow/main.go
CHANGED
|
@@ -664,6 +664,10 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
664
664
|
fmt.Fprintln(os.Stderr, "init: harness plugin: "+err.Error())
|
|
665
665
|
return exitFail
|
|
666
666
|
}
|
|
667
|
+
if names[0] == "jira" {
|
|
668
|
+
harnessDefaults["initial"] = harness.JiraInitialPrompt
|
|
669
|
+
harnessDefaults["feedback"] = harness.JiraFeedbackPrompt
|
|
670
|
+
}
|
|
667
671
|
if !*force || !configExists {
|
|
668
672
|
cfg.HarnessConfig = harnessDefaults
|
|
669
673
|
} else {
|
|
@@ -42,21 +42,19 @@ taskConfig:
|
|
|
42
42
|
# parentStatus: In Progress
|
|
43
43
|
# taskStatus: In Progress
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
# relay-flow and is not changed by these templates.
|
|
45
|
+
# Jira mailbox descriptions include the canonical report through {{report}}.
|
|
47
46
|
templates:
|
|
48
47
|
mailboxDescription: |-
|
|
49
|
-
|
|
50
|
-
Workflow: {{workflow}}
|
|
51
|
-
Node: {{node}}
|
|
52
|
-
Node type: {{nodeType}}
|
|
53
|
-
Agent: {{agent}}
|
|
54
|
-
Mailbox: {{mailbox}}
|
|
55
|
-
|
|
56
|
-
Node work:
|
|
48
|
+
{{ticket}} / {{node}} — {{nodeType}} — {{agent}}
|
|
57
49
|
{{nodeDescription}}
|
|
58
50
|
|
|
59
|
-
|
|
51
|
+
Success routes:
|
|
52
|
+
{{successRoutes}}
|
|
53
|
+
|
|
54
|
+
Failure routes:
|
|
55
|
+
{{failureRoutes}}
|
|
56
|
+
|
|
57
|
+
{{report}}
|
|
60
58
|
|
|
61
59
|
summaryComment: |-
|
|
62
60
|
Summary for {{node}}
|
|
@@ -80,14 +78,19 @@ harnessPlugin: opencode
|
|
|
80
78
|
harnessConfig:
|
|
81
79
|
# OpenCode supports initial, feedback, and hitl templates.
|
|
82
80
|
initial: |-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
Read the description of your assigned Jira task:
|
|
82
|
+
acli jira workitem view "{{mailbox}}" --fields "summary,description" --json
|
|
85
83
|
|
|
86
|
-
|
|
84
|
+
Follow the work instructions, valid routes, and report format in that description.
|
|
87
85
|
|
|
88
86
|
feedback: |-
|
|
89
|
-
|
|
87
|
+
Continue {{node}} from the latest update on your assigned Jira task:
|
|
88
|
+
acli jira workitem comment list --key "{{mailbox}}" --limit 1 --order "-created" --json
|
|
90
89
|
|
|
90
|
+
# A node needing parent details can request this additional instruction
|
|
91
|
+
# in its node description (it is not included by default):
|
|
92
|
+
# Read the parent Jira ticket for this step:
|
|
93
|
+
# acli jira workitem view "{{ticket}}" --fields "summary,description" --json
|
|
91
94
|
hitl: |-
|
|
92
95
|
Return the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.
|
|
93
96
|
|
|
@@ -135,12 +138,13 @@ repos:
|
|
|
135
138
|
# harnessPlugin: pi
|
|
136
139
|
# harnessConfig:
|
|
137
140
|
# initial: |
|
|
138
|
-
#
|
|
139
|
-
#
|
|
141
|
+
# Read the description of your assigned Jira task:
|
|
142
|
+
# acli jira workitem view "{{mailbox}}" --fields "summary,description" --json
|
|
140
143
|
#
|
|
141
|
-
#
|
|
144
|
+
# Follow the work instructions, valid routes, and report format in that description.
|
|
142
145
|
# feedback: |
|
|
143
|
-
#
|
|
146
|
+
# Continue {{node}} from the latest update on your assigned Jira task:
|
|
147
|
+
# acli jira workitem comment list --key "{{mailbox}}" --limit 1 --order "-created" --json
|
|
144
148
|
#
|
|
145
149
|
# Pi accepts only initial and feedback harness templates. Pi workflow nodes
|
|
146
150
|
# may use agent: default or a native project prompt template in
|
|
@@ -56,6 +56,9 @@ nodes:
|
|
|
56
56
|
type: agent
|
|
57
57
|
agent: build
|
|
58
58
|
description: |
|
|
59
|
+
Read the parent Jira ticket for this step:
|
|
60
|
+
acli jira workitem view "{{ticket}}" --fields "summary,description" --json
|
|
61
|
+
|
|
59
62
|
Implement the parent ticket in the ticket worktree.
|
|
60
63
|
Run the relevant tests and verification commands.
|
|
61
64
|
nudgePrompt: |
|
|
@@ -80,7 +80,11 @@ func (a *Activities) EnsureMailboxes(ctx context.Context, w run.Work, specs []ta
|
|
|
80
80
|
if err != nil {
|
|
81
81
|
return nil, fmt.Errorf("render mailbox %q description: %w", specs[i].Node, err)
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
if data.Report != "" && strings.Contains(custom, data.Report) {
|
|
84
|
+
specs[i].Description = custom
|
|
85
|
+
} else {
|
|
86
|
+
specs[i].Description = appendText(custom, specs[i].Description)
|
|
87
|
+
}
|
|
84
88
|
}
|
|
85
89
|
return sys.EnsureMailboxes(ctx, w.Parent, w.Workflow, specs)
|
|
86
90
|
}
|
|
@@ -488,26 +492,9 @@ func MailboxSpecForNode(wf *workflow.Workflow, ticketKey, name string, n workflo
|
|
|
488
492
|
var b strings.Builder
|
|
489
493
|
b.WriteString(`Required report format:
|
|
490
494
|
|
|
491
|
-
|
|
492
|
-
NEXT STEP: <one valid node name>
|
|
493
|
-
|
|
494
|
-
SUMMARY:
|
|
495
|
-
COMPLETED:
|
|
496
|
-
COMMITS:
|
|
497
|
-
NOT COMPLETED:
|
|
498
|
-
ISSUES DISCOVERED:
|
|
499
|
-
VERIFICATION:
|
|
500
|
-
NOTES:
|
|
501
|
-
|
|
502
|
-
FEEDBACK:
|
|
503
|
-
REASON FOR NEXT STEP:
|
|
504
|
-
REQUIRED ACTIONS:
|
|
505
|
-
RELEVANT CONTEXT:
|
|
506
|
-
EXPECTED RESULT:
|
|
495
|
+
` + workflow.ReportFormat + `
|
|
507
496
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
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.`)
|
|
497
|
+
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, FEEDBACK must be None.`)
|
|
511
498
|
writeRoutes := func(label string, routes []workflow.Route) {
|
|
512
499
|
if len(routes) == 0 {
|
|
513
500
|
return
|
|
@@ -535,7 +522,7 @@ Node names identify workflow stages; they are not task-system statuses. STATUS d
|
|
|
535
522
|
Agent: n.Agent, NodeDescription: n.Description,
|
|
536
523
|
NextSteps: nextStepsText(append(append([]workflow.Route{}, n.OnSuccess...), n.OnFailure...)),
|
|
537
524
|
SuccessRoutes: successRoutes, FailureRoutes: failureRoutes,
|
|
538
|
-
Mailbox: ticketKey + ":" + name,
|
|
525
|
+
Mailbox: ticketKey + ":" + name, Report: workflow.ReportFormat,
|
|
539
526
|
},
|
|
540
527
|
}
|
|
541
528
|
}
|
|
@@ -569,7 +556,11 @@ func RenderMailboxSpecs(sys task.System, w run.Work, wf *workflow.Workflow) ([]t
|
|
|
569
556
|
if err != nil {
|
|
570
557
|
return nil, fmt.Errorf("render mailbox %q description: %w", specs[i].Node, err)
|
|
571
558
|
}
|
|
572
|
-
|
|
559
|
+
if data.Report != "" && strings.Contains(custom, data.Report) {
|
|
560
|
+
specs[i].Description = custom
|
|
561
|
+
} else {
|
|
562
|
+
specs[i].Description = appendText(custom, specs[i].Description)
|
|
563
|
+
}
|
|
573
564
|
}
|
|
574
565
|
return specs, nil
|
|
575
566
|
}
|
|
@@ -72,13 +72,16 @@ func TestMailboxDescriptionAndLaunchPromptAreTaskSystemNeutral(t *testing.T) {
|
|
|
72
72
|
t.Fatal(err)
|
|
73
73
|
}
|
|
74
74
|
want := "Task system: " + taskSystem + "\nUse the " + taskSystem + " tools to read the parent ticket PAY-101.\n\nYour mailbox is PAY-234. Read its description and comments for node instructions and feedback.\n\nKeep the summary brief, and make the feedback as detailed and actionable as possible for the next agent.\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval."
|
|
75
|
+
if taskSystem == "jira" {
|
|
76
|
+
want = "Read the description of your assigned Jira task:\nacli jira workitem view \"PAY-234\" --fields \"summary,description\" --json\n\nFollow the work instructions, valid routes, and report format in that description."
|
|
77
|
+
}
|
|
75
78
|
if prompt != want {
|
|
76
79
|
t.Fatalf("RenderPrompt(%s) = %q, want %q", taskSystem, prompt, want)
|
|
77
80
|
}
|
|
78
|
-
if strings.Contains(prompt, "Jira") || strings.Contains(prompt, "subtask") {
|
|
81
|
+
if taskSystem != "jira" && (strings.Contains(prompt, "Jira") || strings.Contains(prompt, "subtask")) {
|
|
79
82
|
t.Fatalf("launch prompt contains task-system-specific mailbox wording: %q", prompt)
|
|
80
83
|
}
|
|
81
|
-
if strings.Contains(prompt, "STATUS:") || strings.Contains(prompt, node.Description) {
|
|
84
|
+
if strings.Contains(prompt, "STATUS:") || (taskSystem != "jira" && strings.Contains(prompt, node.Description)) {
|
|
82
85
|
t.Fatalf("launch prompt duplicates mailbox instructions: %q", prompt)
|
|
83
86
|
}
|
|
84
87
|
}
|
|
@@ -467,6 +470,7 @@ func TestTransitionOrdering(t *testing.T) {
|
|
|
467
470
|
log := newEventLog()
|
|
468
471
|
sys := newFakeTaskSystem(log)
|
|
469
472
|
fr := newFakeRunner(log)
|
|
473
|
+
fh := newFakeHarness(log)
|
|
470
474
|
wf := workflow.Workflow{
|
|
471
475
|
Name: "reviewFlow", Repos: []string{"payments"},
|
|
472
476
|
Nodes: map[string]workflow.Node{
|
|
@@ -485,7 +489,7 @@ func TestTransitionOrdering(t *testing.T) {
|
|
|
485
489
|
},
|
|
486
490
|
}
|
|
487
491
|
engine := newEngine(t, goworkflows.Dependencies{
|
|
488
|
-
Repos: repoRegistryWith("payments", sys), Runner: fr, Harness:
|
|
492
|
+
Repos: repoRegistryWith("payments", sys), Runner: fr, Harness: fh,
|
|
489
493
|
})
|
|
490
494
|
rid, _ := startRun(engine, wf)
|
|
491
495
|
waitFor(t, 10*time.Second, func() bool {
|
|
@@ -510,6 +514,16 @@ func TestTransitionOrdering(t *testing.T) {
|
|
|
510
514
|
t.Fatalf("HITL status was not set before terminal start; events=%v", events)
|
|
511
515
|
}
|
|
512
516
|
|
|
517
|
+
var sawReviewFeedback bool
|
|
518
|
+
for _, call := range fh.promptCalls() {
|
|
519
|
+
if call.Data.Node == "review" && call.Data.PreviousFeedback == "review it" {
|
|
520
|
+
sawReviewFeedback = true
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if !sawReviewFeedback {
|
|
524
|
+
t.Fatalf("next node prompt did not receive selected feedback: %+v", fh.promptCalls())
|
|
525
|
+
}
|
|
526
|
+
|
|
513
527
|
// Exact cross-primitive order, observed through the fake-adapter and fake-
|
|
514
528
|
// runner call logs (the settled observation seam):
|
|
515
529
|
// summary(current) -> feedback(selected next) -> CompleteMailbox(current)
|
|
@@ -205,6 +205,7 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
current := target
|
|
208
|
+
previousFeedback := ""
|
|
208
209
|
seenReportIDs := map[string]bool{}
|
|
209
210
|
lastStepByNode := map[string]int64{}
|
|
210
211
|
lastDepthByNode := map[string]int{}
|
|
@@ -297,16 +298,17 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
297
298
|
Title: title,
|
|
298
299
|
NudgePrompt: node.NudgePrompt,
|
|
299
300
|
PromptData: harness.PromptData{
|
|
300
|
-
TaskSystem:
|
|
301
|
-
Ticket:
|
|
302
|
-
Workflow:
|
|
303
|
-
Repo:
|
|
304
|
-
Node:
|
|
305
|
-
NodeType:
|
|
306
|
-
Agent:
|
|
307
|
-
NodeDescription:
|
|
308
|
-
NextSteps:
|
|
309
|
-
Mailbox:
|
|
301
|
+
TaskSystem: a.TaskSystem,
|
|
302
|
+
Ticket: start.Ticket.Key,
|
|
303
|
+
Workflow: wf.Name,
|
|
304
|
+
Repo: start.Repo,
|
|
305
|
+
Node: current,
|
|
306
|
+
NodeType: node.Type,
|
|
307
|
+
Agent: node.Agent,
|
|
308
|
+
NodeDescription: node.Description,
|
|
309
|
+
NextSteps: nextStepsText(nextSteps),
|
|
310
|
+
Mailbox: mb.Key,
|
|
311
|
+
PreviousFeedback: previousFeedback,
|
|
310
312
|
},
|
|
311
313
|
NextSteps: nextSteps,
|
|
312
314
|
}
|
|
@@ -395,7 +397,9 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
395
397
|
stepMessage := report.Summary.Completed
|
|
396
398
|
if report.Status == workflow.OutcomeFailure {
|
|
397
399
|
stepStatus = run.StepFailed
|
|
398
|
-
|
|
400
|
+
if report.Summary.IssuesDiscovered != workflow.None {
|
|
401
|
+
stepMessage = report.Summary.IssuesDiscovered
|
|
402
|
+
}
|
|
399
403
|
}
|
|
400
404
|
stepFinished := goworkflow.Now(ctx).UTC()
|
|
401
405
|
if _, err := retryLoop(ctx, start.ID, a, work, current,
|
|
@@ -465,6 +469,7 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
465
469
|
return err
|
|
466
470
|
}
|
|
467
471
|
|
|
472
|
+
previousFeedback = report.Feedback.RequiredActions
|
|
468
473
|
current = next
|
|
469
474
|
}
|
|
470
475
|
|
|
@@ -761,11 +766,19 @@ func nextStepsText(routes []workflow.Route) string {
|
|
|
761
766
|
}
|
|
762
767
|
|
|
763
768
|
func renderSummaryReport(r workflow.Report) string {
|
|
769
|
+
if r.Summary.Commits == workflow.None && r.Summary.NotCompleted == workflow.None &&
|
|
770
|
+
r.Summary.IssuesDiscovered == workflow.None && r.Summary.Verification == workflow.None && r.Summary.Notes == workflow.None {
|
|
771
|
+
return r.Summary.Completed
|
|
772
|
+
}
|
|
764
773
|
return fmt.Sprintf("COMPLETED:\n%s\n\nCOMMITS:\n%s\n\nNOT COMPLETED:\n%s\n\nISSUES DISCOVERED:\n%s\n\nVERIFICATION:\n%s\n\nNOTES:\n%s",
|
|
765
774
|
r.Summary.Completed, r.Summary.Commits, r.Summary.NotCompleted, r.Summary.IssuesDiscovered, r.Summary.Verification, r.Summary.Notes)
|
|
766
775
|
}
|
|
767
776
|
|
|
768
777
|
func renderFeedbackReport(r workflow.Report) string {
|
|
778
|
+
if r.Summary.Commits == workflow.None && r.Feedback.ReasonForNextStep == workflow.None &&
|
|
779
|
+
r.Feedback.RelevantContext == workflow.None && r.Feedback.ExpectedResult == workflow.None {
|
|
780
|
+
return r.Feedback.RequiredActions
|
|
781
|
+
}
|
|
769
782
|
return fmt.Sprintf("COMMITS:\n%s\n\nREASON FOR NEXT STEP:\n%s\n\nREQUIRED ACTIONS:\n%s\n\nRELEVANT CONTEXT:\n%s\n\nEXPECTED RESULT:\n%s",
|
|
770
783
|
r.Summary.Commits, r.Feedback.ReasonForNextStep, r.Feedback.RequiredActions, r.Feedback.RelevantContext, r.Feedback.ExpectedResult)
|
|
771
784
|
}
|
|
@@ -41,6 +41,33 @@ func threeNodeWorkflow() workflow.Workflow {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
func TestCompactMailboxTemplateDoesNotAppendGenericInstructions(t *testing.T) {
|
|
45
|
+
wf := threeNodeWorkflow()
|
|
46
|
+
sys := newFakeTaskSystem(newEventLog())
|
|
47
|
+
sys.renderText = func(_ task.TextKind, data task.TextData) (string, error) {
|
|
48
|
+
return data.Ticket + " / " + data.Node + " — " + data.NodeType + " — " + data.Agent +
|
|
49
|
+
"\n" + data.NodeDescription + "\nSuccess routes:\n" + data.SuccessRoutes +
|
|
50
|
+
"\nFailure routes:\n" + data.FailureRoutes + "\n" + data.Report, nil
|
|
51
|
+
}
|
|
52
|
+
specs, err := goworkflows.RenderMailboxSpecs(sys, run.Work{
|
|
53
|
+
Parent: task.TicketRef{Key: "PAY-101"}, Repo: "payments", Workflow: wf.Name,
|
|
54
|
+
}, &wf)
|
|
55
|
+
if err != nil {
|
|
56
|
+
t.Fatal(err)
|
|
57
|
+
}
|
|
58
|
+
for _, spec := range specs {
|
|
59
|
+
for _, required := range []string{"PAY-101 / " + spec.Node, wf.Nodes[spec.Node].Description,
|
|
60
|
+
"Success routes:", "Failure routes:", " — when: ", workflow.ReportFormat} {
|
|
61
|
+
if !strings.Contains(spec.Description, required) {
|
|
62
|
+
t.Fatalf("mailbox %s description lacks %q: %q", spec.Node, required, spec.Description)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if strings.Contains(spec.Description, "Required report format:") {
|
|
66
|
+
t.Fatalf("mailbox %s appended generic instructions: %q", spec.Node, spec.Description)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
func TestMailboxesEnsuredForWorkNodesOnly(t *testing.T) {
|
|
45
72
|
log := newEventLog()
|
|
46
73
|
sys := newFakeTaskSystem(log)
|
|
@@ -107,7 +134,7 @@ func TestMailboxesEnsuredForWorkNodesOnly(t *testing.T) {
|
|
|
107
134
|
t.Fatalf("%s description lacks route explanation %q: %q", node, when, d)
|
|
108
135
|
}
|
|
109
136
|
}
|
|
110
|
-
for _, required := range []string{"Required report format:", "STATUS:", "
|
|
137
|
+
for _, required := range []string{"Required report format:", "STATUS:", "NEXT STEP:", "SUMMARY:", "FEEDBACK:"} {
|
|
111
138
|
if !strings.Contains(d, required) {
|
|
112
139
|
t.Fatalf("%s description lacks %q: %q", node, required, d)
|
|
113
140
|
}
|
|
@@ -85,7 +85,11 @@ func (a *Activities) EnsureMailboxes(ctx context.Context, w run.Work, specs []ta
|
|
|
85
85
|
if err != nil {
|
|
86
86
|
return nil, fmt.Errorf("render mailbox %q description: %w", specs[i].Node, err)
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
if data.Report != "" && strings.Contains(custom, data.Report) {
|
|
89
|
+
specs[i].Description = custom
|
|
90
|
+
} else {
|
|
91
|
+
specs[i].Description = appendText(custom, specs[i].Description)
|
|
92
|
+
}
|
|
89
93
|
}
|
|
90
94
|
return sys.EnsureMailboxes(ctx, w.Parent, w.Workflow, specs)
|
|
91
95
|
}
|
|
@@ -486,26 +490,9 @@ func MailboxSpecForNode(wf *workflow.Workflow, ticketKey, name string, n workflo
|
|
|
486
490
|
var b strings.Builder
|
|
487
491
|
b.WriteString(`Required report format:
|
|
488
492
|
|
|
489
|
-
|
|
490
|
-
NEXT STEP: <one valid node name>
|
|
491
|
-
|
|
492
|
-
SUMMARY:
|
|
493
|
-
COMPLETED:
|
|
494
|
-
COMMITS:
|
|
495
|
-
NOT COMPLETED:
|
|
496
|
-
ISSUES DISCOVERED:
|
|
497
|
-
VERIFICATION:
|
|
498
|
-
NOTES:
|
|
499
|
-
|
|
500
|
-
FEEDBACK:
|
|
501
|
-
REASON FOR NEXT STEP:
|
|
502
|
-
REQUIRED ACTIONS:
|
|
503
|
-
RELEVANT CONTEXT:
|
|
504
|
-
EXPECTED RESULT:
|
|
493
|
+
` + workflow.ReportFormat + `
|
|
505
494
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
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.`)
|
|
495
|
+
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, FEEDBACK must be None.`)
|
|
509
496
|
writeRoutes := func(label string, routes []workflow.Route) {
|
|
510
497
|
if len(routes) == 0 {
|
|
511
498
|
return
|
|
@@ -533,7 +520,7 @@ Node names identify workflow stages; they are not task-system statuses. STATUS d
|
|
|
533
520
|
Agent: n.Agent, NodeDescription: n.Description,
|
|
534
521
|
NextSteps: nextStepsText(append(append([]workflow.Route{}, n.OnSuccess...), n.OnFailure...)),
|
|
535
522
|
SuccessRoutes: successRoutes, FailureRoutes: failureRoutes,
|
|
536
|
-
Mailbox: ticketKey + ":" + name,
|
|
523
|
+
Mailbox: ticketKey + ":" + name, Report: workflow.ReportFormat,
|
|
537
524
|
},
|
|
538
525
|
}
|
|
539
526
|
}
|
|
@@ -567,7 +554,11 @@ func RenderMailboxSpecs(sys task.System, w run.Work, wf *workflow.Workflow) ([]t
|
|
|
567
554
|
if err != nil {
|
|
568
555
|
return nil, fmt.Errorf("render mailbox %q description: %w", specs[i].Node, err)
|
|
569
556
|
}
|
|
570
|
-
|
|
557
|
+
if data.Report != "" && strings.Contains(custom, data.Report) {
|
|
558
|
+
specs[i].Description = custom
|
|
559
|
+
} else {
|
|
560
|
+
specs[i].Description = appendText(custom, specs[i].Description)
|
|
561
|
+
}
|
|
571
562
|
}
|
|
572
563
|
return specs, nil
|
|
573
564
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package temporal
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/rajpopat27/relay-flow/internal/run"
|
|
8
|
+
"github.com/rajpopat27/relay-flow/internal/task"
|
|
9
|
+
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type compactMailboxRenderer struct{ *lagTaskSystem }
|
|
13
|
+
|
|
14
|
+
func (*compactMailboxRenderer) RenderText(_ task.TextKind, data task.TextData) (string, error) {
|
|
15
|
+
return data.Ticket + " / " + data.Node + " — " + data.NodeType + " — " + data.Agent +
|
|
16
|
+
"\n" + data.NodeDescription + "\nSuccess routes:\n" + data.SuccessRoutes +
|
|
17
|
+
"\nFailure routes:\n" + data.FailureRoutes + "\n" + data.Report, nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func TestCompactMailboxTemplateDoesNotAppendGenericInstructions(t *testing.T) {
|
|
21
|
+
wf := workflow.Workflow{Name: "test", Nodes: map[string]workflow.Node{
|
|
22
|
+
"coder": {Type: workflow.NodeAgent, Agent: "coder", Description: "Implement", OnSuccess: []workflow.Route{{Target: "end"}}},
|
|
23
|
+
}}
|
|
24
|
+
specs, err := RenderMailboxSpecs(&compactMailboxRenderer{&lagTaskSystem{}}, run.Work{
|
|
25
|
+
Parent: task.TicketRef{Key: "PAY-101"}, Repo: "payments", Workflow: wf.Name,
|
|
26
|
+
}, &wf)
|
|
27
|
+
if err != nil {
|
|
28
|
+
t.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
if len(specs) != 1 {
|
|
31
|
+
t.Fatalf("compact mailbox descriptions = %+v", specs)
|
|
32
|
+
}
|
|
33
|
+
for _, required := range []string{"PAY-101 / coder — agent — coder", "Implement", "Success routes:\nend", "Failure routes:", workflow.ReportFormat} {
|
|
34
|
+
if !strings.Contains(specs[0].Description, required) {
|
|
35
|
+
t.Fatalf("mailbox description lacks %q: %q", required, specs[0].Description)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if strings.Contains(specs[0].Description, "Required report format:") {
|
|
39
|
+
t.Fatalf("generic instructions appended to Jira template: %q", specs[0].Description)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func TestConciseReportRendersOnlySelectedSummaryAndFeedback(t *testing.T) {
|
|
44
|
+
r := workflow.Report{
|
|
45
|
+
Summary: workflow.Summary{Completed: "done", Commits: workflow.None, NotCompleted: workflow.None,
|
|
46
|
+
IssuesDiscovered: workflow.None, Verification: workflow.None, Notes: workflow.None},
|
|
47
|
+
Feedback: workflow.Feedback{ReasonForNextStep: workflow.None, RequiredActions: "review it",
|
|
48
|
+
RelevantContext: workflow.None, ExpectedResult: workflow.None},
|
|
49
|
+
}
|
|
50
|
+
if got := renderSummaryReport(r); got != "done" {
|
|
51
|
+
t.Fatalf("summary comment = %q", got)
|
|
52
|
+
}
|
|
53
|
+
if got := renderFeedbackReport(r); got != "review it" {
|
|
54
|
+
t.Fatalf("feedback comment = %q", got)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -342,6 +342,7 @@ func runGraph(ctx temporalworkflow.Context, start run.Start, state *workflowStat
|
|
|
342
342
|
state.upsertStep(startStep)
|
|
343
343
|
|
|
344
344
|
current := target
|
|
345
|
+
previousFeedback := ""
|
|
345
346
|
lastStepByNode := map[string]int64{}
|
|
346
347
|
lastDepthByNode := map[string]int{}
|
|
347
348
|
for current != "end" {
|
|
@@ -407,7 +408,7 @@ func runGraph(ctx temporalworkflow.Context, start run.Start, state *workflowStat
|
|
|
407
408
|
PromptData: harness.PromptData{
|
|
408
409
|
TaskSystem: "", Ticket: start.Ticket.Key, Workflow: wf.Name, Repo: start.Repo,
|
|
409
410
|
Node: current, NodeType: node.Type, Agent: node.Agent, NodeDescription: node.Description,
|
|
410
|
-
NextSteps: nextStepsText(nextSteps), Mailbox: mailbox.Key,
|
|
411
|
+
NextSteps: nextStepsText(nextSteps), Mailbox: mailbox.Key, PreviousFeedback: previousFeedback,
|
|
411
412
|
},
|
|
412
413
|
NextSteps: nextSteps,
|
|
413
414
|
}
|
|
@@ -516,7 +517,9 @@ func runGraph(ctx temporalworkflow.Context, start run.Start, state *workflowStat
|
|
|
516
517
|
stepMessage := accepted.Report.Summary.Completed
|
|
517
518
|
if accepted.Report.Status == domainworkflow.OutcomeFailure {
|
|
518
519
|
stepStatus = run.StepFailed
|
|
519
|
-
|
|
520
|
+
if accepted.Report.Summary.IssuesDiscovered != domainworkflow.None {
|
|
521
|
+
stepMessage = accepted.Report.Summary.IssuesDiscovered
|
|
522
|
+
}
|
|
520
523
|
}
|
|
521
524
|
stepFinished := temporalworkflow.Now(ctx).UTC()
|
|
522
525
|
step.Status, step.FinishedAt, step.Message, step.Route = stepStatus, &stepFinished, stepMessage, accepted.Report.NextStep
|
|
@@ -569,6 +572,7 @@ func runGraph(ctx temporalworkflow.Context, start run.Start, state *workflowStat
|
|
|
569
572
|
return err
|
|
570
573
|
}
|
|
571
574
|
applyRuntimePolicy(state, current, work.Runtime)
|
|
575
|
+
previousFeedback = accepted.Report.Feedback.RequiredActions
|
|
572
576
|
current = next
|
|
573
577
|
}
|
|
574
578
|
|
|
@@ -859,11 +863,19 @@ func applyRuntimePolicy(state *workflowState, node string, policy run.RuntimePol
|
|
|
859
863
|
}
|
|
860
864
|
|
|
861
865
|
func renderSummaryReport(r domainworkflow.Report) string {
|
|
866
|
+
if r.Summary.Commits == domainworkflow.None && r.Summary.NotCompleted == domainworkflow.None &&
|
|
867
|
+
r.Summary.IssuesDiscovered == domainworkflow.None && r.Summary.Verification == domainworkflow.None && r.Summary.Notes == domainworkflow.None {
|
|
868
|
+
return r.Summary.Completed
|
|
869
|
+
}
|
|
862
870
|
return fmt.Sprintf("COMPLETED:\n%s\n\nCOMMITS:\n%s\n\nNOT COMPLETED:\n%s\n\nISSUES DISCOVERED:\n%s\n\nVERIFICATION:\n%s\n\nNOTES:\n%s",
|
|
863
871
|
r.Summary.Completed, r.Summary.Commits, r.Summary.NotCompleted, r.Summary.IssuesDiscovered, r.Summary.Verification, r.Summary.Notes)
|
|
864
872
|
}
|
|
865
873
|
|
|
866
874
|
func renderFeedbackReport(r domainworkflow.Report) string {
|
|
875
|
+
if r.Summary.Commits == domainworkflow.None && r.Feedback.ReasonForNextStep == domainworkflow.None &&
|
|
876
|
+
r.Feedback.RelevantContext == domainworkflow.None && r.Feedback.ExpectedResult == domainworkflow.None {
|
|
877
|
+
return r.Feedback.RequiredActions
|
|
878
|
+
}
|
|
867
879
|
return fmt.Sprintf("COMMITS:\n%s\n\nREASON FOR NEXT STEP:\n%s\n\nREQUIRED ACTIONS:\n%s\n\nRELEVANT CONTEXT:\n%s\n\nEXPECTED RESULT:\n%s",
|
|
868
880
|
r.Summary.Commits, r.Feedback.ReasonForNextStep, r.Feedback.RequiredActions, r.Feedback.RelevantContext, r.Feedback.ExpectedResult)
|
|
869
881
|
}
|
|
@@ -24,20 +24,32 @@ const (
|
|
|
24
24
|
PromptFeedback PromptKind = "feedback"
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
// Jira prompt defaults keep each agent read limited to the relevant mailbox.
|
|
28
|
+
// A node that needs its parent's details can request the optional parent-read
|
|
29
|
+
// instruction in its node description.
|
|
30
|
+
const JiraInitialPrompt = `Read the description of your assigned Jira task:
|
|
31
|
+
acli jira workitem view "{{mailbox}}" --fields "summary,description" --json
|
|
32
|
+
|
|
33
|
+
Follow the work instructions, valid routes, and report format in that description.`
|
|
34
|
+
|
|
35
|
+
const JiraFeedbackPrompt = `Continue {{node}} from the latest update on your assigned Jira task:
|
|
36
|
+
acli jira workitem comment list --key "{{mailbox}}" --limit 1 --order "-created" --json`
|
|
37
|
+
|
|
27
38
|
// PromptData is the task-system-neutral data core supplies to the selected
|
|
28
39
|
// harness. Harness templates, including HITL instructions, are rendered only
|
|
29
40
|
// by the harness.
|
|
30
41
|
type PromptData struct {
|
|
31
|
-
TaskSystem
|
|
32
|
-
Ticket
|
|
33
|
-
Workflow
|
|
34
|
-
Repo
|
|
35
|
-
Node
|
|
36
|
-
NodeType
|
|
37
|
-
Agent
|
|
38
|
-
NodeDescription
|
|
39
|
-
NextSteps
|
|
40
|
-
Mailbox
|
|
42
|
+
TaskSystem string
|
|
43
|
+
Ticket string
|
|
44
|
+
Workflow string
|
|
45
|
+
Repo string
|
|
46
|
+
Node string
|
|
47
|
+
NodeType workflow.NodeType
|
|
48
|
+
Agent string
|
|
49
|
+
NodeDescription string
|
|
50
|
+
NextSteps string
|
|
51
|
+
Mailbox string
|
|
52
|
+
PreviousFeedback string
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
type LaunchSpec struct {
|
|
@@ -137,23 +137,35 @@ func (h *Harness) FindSession(context.Context, string, string) (harness.Session,
|
|
|
137
137
|
return harness.Session{}, false, nil
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// RenderPrompt renders the selected session prompt
|
|
141
|
-
//
|
|
142
|
-
//
|
|
140
|
+
// RenderPrompt renders the selected session prompt and HITL instructions.
|
|
141
|
+
// Jira first launches omit the node nudge so the initial read stays limited
|
|
142
|
+
// to the assigned mailbox description.
|
|
143
143
|
func (h *Harness) RenderPrompt(kind harness.PromptKind, data harness.PromptData, nudgeTemplate string) (string, error) {
|
|
144
144
|
var tmpl string
|
|
145
145
|
switch kind {
|
|
146
146
|
case harness.PromptInitial:
|
|
147
147
|
tmpl = h.templates.Initial
|
|
148
|
+
if data.TaskSystem == "jira" && tmpl == defaultInitialPrompt {
|
|
149
|
+
tmpl = harness.JiraInitialPrompt
|
|
150
|
+
}
|
|
148
151
|
case harness.PromptFeedback:
|
|
149
152
|
tmpl = h.templates.Feedback
|
|
153
|
+
if data.TaskSystem == "jira" && tmpl == defaultFeedbackPrompt {
|
|
154
|
+
tmpl = harness.JiraFeedbackPrompt
|
|
155
|
+
}
|
|
150
156
|
default:
|
|
151
157
|
return "", fmt.Errorf("opencode: unknown prompt kind %q", kind)
|
|
152
158
|
}
|
|
153
159
|
prompt := renderTemplate(tmpl, data)
|
|
154
|
-
if data.
|
|
160
|
+
if data.TaskSystem == "jira" && data.PreviousFeedback != "" {
|
|
161
|
+
prompt = appendPrompt(prompt, "Previous step feedback: "+data.PreviousFeedback)
|
|
162
|
+
}
|
|
163
|
+
if data.NodeType == workflow.NodeHITL && (data.TaskSystem != "jira" || h.templates.HITL != defaultHITLPrompt) {
|
|
155
164
|
prompt = appendPrompt(prompt, renderTemplate(h.templates.HITL, data))
|
|
156
165
|
}
|
|
166
|
+
if kind == harness.PromptInitial && data.TaskSystem == "jira" {
|
|
167
|
+
return prompt, nil
|
|
168
|
+
}
|
|
157
169
|
return appendPrompt(prompt, renderTemplate(nudgeTemplate, data)), nil
|
|
158
170
|
}
|
|
159
171
|
|
|
@@ -204,6 +216,7 @@ func (h *Harness) BuildCommand(spec harness.LaunchSpec) (runner.Command, error)
|
|
|
204
216
|
"RELAY_FLOW_NODE_TYPE": string(spec.NodeType),
|
|
205
217
|
"RELAY_FLOW_NUDGE_PROMPT": spec.NudgePrompt,
|
|
206
218
|
"RELAY_FLOW_NEXT_STEPS_JSON": string(nextSteps),
|
|
219
|
+
"RELAY_FLOW_REPORT_FORMAT": workflow.ReportFormat,
|
|
207
220
|
} {
|
|
208
221
|
env[key] = value
|
|
209
222
|
}
|
|
@@ -14,7 +14,7 @@ import (
|
|
|
14
14
|
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
const configuredPlugin = "relay-flow-plugin@0.3.
|
|
17
|
+
const configuredPlugin = "relay-flow-plugin@0.3.11-alpha"
|
|
18
18
|
|
|
19
19
|
func TestBuildCommandArgv(t *testing.T) {
|
|
20
20
|
t.Setenv("RELAY_FLOW_HOME", "/var/lib/relay-flow-test")
|
|
@@ -105,6 +105,30 @@ func TestRenderPromptTemplatesExposeAllValues(t *testing.T) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
func TestJiraDefaultPromptsUseOnlyMailboxReadsAndSelectedFeedback(t *testing.T) {
|
|
109
|
+
h := opencode.New()
|
|
110
|
+
data := harness.PromptData{
|
|
111
|
+
TaskSystem: "jira", Ticket: "PAY-101", Mailbox: "PAY-234", Node: "coder", NodeType: workflow.NodeHITL,
|
|
112
|
+
}
|
|
113
|
+
initial, err := h.RenderPrompt(harness.PromptInitial, data, "Read the latest mailbox feedback.")
|
|
114
|
+
if err != nil {
|
|
115
|
+
t.Fatal(err)
|
|
116
|
+
}
|
|
117
|
+
wantInitial := "Read the description of your assigned Jira task:\nacli jira workitem view \"PAY-234\" --fields \"summary,description\" --json\n\nFollow the work instructions, valid routes, and report format in that description."
|
|
118
|
+
if initial != wantInitial {
|
|
119
|
+
t.Fatalf("Jira initial prompt = %q, want %q", initial, wantInitial)
|
|
120
|
+
}
|
|
121
|
+
data.PreviousFeedback = "Review found a missing test; add it."
|
|
122
|
+
continuation, err := h.RenderPrompt(harness.PromptFeedback, data, "")
|
|
123
|
+
if err != nil {
|
|
124
|
+
t.Fatal(err)
|
|
125
|
+
}
|
|
126
|
+
wantContinuation := "Continue coder from the latest update on your assigned Jira task:\nacli jira workitem comment list --key \"PAY-234\" --limit 1 --order \"-created\" --json\n\nPrevious step feedback: Review found a missing test; add it."
|
|
127
|
+
if continuation != wantContinuation {
|
|
128
|
+
t.Fatalf("Jira continuation = %q, want %q", continuation, wantContinuation)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
func TestHarnessConfigRejectsUnknownPromptVariable(t *testing.T) {
|
|
109
133
|
_, err := harness.New("opencode", config.RawValues{"initial": "{{unknown}}"})
|
|
110
134
|
if err == nil || !strings.Contains(err.Error(), "unknown template variable {{unknown}}") {
|
|
@@ -51,7 +51,7 @@ func TestBuildCommandWithoutTaskEnvKeepsRelayVariablesOnly(t *testing.T) {
|
|
|
51
51
|
if err != nil {
|
|
52
52
|
t.Fatalf("BuildCommand: %v", err)
|
|
53
53
|
}
|
|
54
|
-
if len(cmd.Env) !=
|
|
55
|
-
t.Fatalf("Env = %#v, want only the
|
|
54
|
+
if len(cmd.Env) != 10 {
|
|
55
|
+
t.Fatalf("Env = %#v, want only the ten relay-flow variables", cmd.Env)
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -150,7 +150,7 @@ func (*Harness) FindSession(context.Context, string, string) (harness.Session, b
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// RenderPrompt renders the selected initial or feedback template and the
|
|
153
|
-
// node's nudge template. Initial prompts use Pi's native prompt-template
|
|
153
|
+
// node's nudge template (except on a Jira first launch). Initial prompts use Pi's native prompt-template
|
|
154
154
|
// command syntax; feedback is sent to an existing session and must remain
|
|
155
155
|
// raw so Pi does not expand the full prompt template again. HITL approval is
|
|
156
156
|
// not encoded in the prompt; the Pi extension asks for approval through
|
|
@@ -160,8 +160,14 @@ func (h *Harness) RenderPrompt(kind harness.PromptKind, data harness.PromptData,
|
|
|
160
160
|
switch kind {
|
|
161
161
|
case harness.PromptInitial:
|
|
162
162
|
tmpl = h.templates.Initial
|
|
163
|
+
if data.TaskSystem == "jira" && tmpl == defaultInitialPrompt {
|
|
164
|
+
tmpl = harness.JiraInitialPrompt
|
|
165
|
+
}
|
|
163
166
|
case harness.PromptFeedback:
|
|
164
167
|
tmpl = h.templates.Feedback
|
|
168
|
+
if data.TaskSystem == "jira" && tmpl == defaultFeedbackPrompt {
|
|
169
|
+
tmpl = harness.JiraFeedbackPrompt
|
|
170
|
+
}
|
|
165
171
|
default:
|
|
166
172
|
return "", fmt.Errorf("pi: unknown prompt kind %q", kind)
|
|
167
173
|
}
|
|
@@ -170,7 +176,13 @@ func (h *Harness) RenderPrompt(kind harness.PromptKind, data harness.PromptData,
|
|
|
170
176
|
return "", err
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
|
-
prompt :=
|
|
179
|
+
prompt := renderTemplate(tmpl, data)
|
|
180
|
+
if data.TaskSystem == "jira" && data.PreviousFeedback != "" {
|
|
181
|
+
prompt = appendPrompt(prompt, "Previous step feedback: "+data.PreviousFeedback)
|
|
182
|
+
}
|
|
183
|
+
if kind != harness.PromptInitial || data.TaskSystem != "jira" {
|
|
184
|
+
prompt = appendPrompt(prompt, renderTemplate(nudgeTemplate, data))
|
|
185
|
+
}
|
|
174
186
|
if kind == harness.PromptInitial {
|
|
175
187
|
return applyPromptTemplate(data.Agent, prompt), nil
|
|
176
188
|
}
|
|
@@ -213,6 +225,7 @@ func (*Harness) BuildCommand(spec harness.LaunchSpec) (runner.Command, error) {
|
|
|
213
225
|
"RELAY_FLOW_NODE_TYPE": string(spec.NodeType),
|
|
214
226
|
"RELAY_FLOW_NUDGE_PROMPT": spec.NudgePrompt,
|
|
215
227
|
"RELAY_FLOW_NEXT_STEPS_JSON": string(nextSteps),
|
|
228
|
+
"RELAY_FLOW_REPORT_FORMAT": workflow.ReportFormat,
|
|
216
229
|
} {
|
|
217
230
|
env[key] = value
|
|
218
231
|
}
|
|
@@ -42,6 +42,7 @@ func TestBuildCommandUsesStrictPiCLIContract(t *testing.T) {
|
|
|
42
42
|
"RELAY_FLOW_NODE_TYPE": string(base.NodeType),
|
|
43
43
|
"RELAY_FLOW_NUDGE_PROMPT": base.NudgePrompt,
|
|
44
44
|
"RELAY_FLOW_NEXT_STEPS_JSON": string(nextSteps),
|
|
45
|
+
"RELAY_FLOW_REPORT_FORMAT": workflow.ReportFormat,
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
tests := []struct {
|
|
@@ -248,13 +249,13 @@ func runStrictPi(t *testing.T, command runner.Command, cwd, capturePath string)
|
|
|
248
249
|
t.Fatalf("read strict Pi capture: %v", err)
|
|
249
250
|
}
|
|
250
251
|
fields := strings.Split(string(data), "\x00")
|
|
251
|
-
if len(fields) <
|
|
252
|
+
if len(fields) < 13 || fields[len(fields)-1] != "" {
|
|
252
253
|
t.Fatalf("malformed strict Pi capture: %q", data)
|
|
253
254
|
}
|
|
254
255
|
fields = fields[:len(fields)-1]
|
|
255
256
|
capture := piCapture{
|
|
256
257
|
cwd: fields[0],
|
|
257
|
-
args: strings.Split(fields[
|
|
258
|
+
args: strings.Split(fields[11], "\x1f"),
|
|
258
259
|
env: map[string]string{
|
|
259
260
|
"RELAY_FLOW_HOME": fields[1],
|
|
260
261
|
"RELAY_FLOW_RUN_ID": fields[2],
|
|
@@ -265,6 +266,7 @@ func runStrictPi(t *testing.T, command runner.Command, cwd, capturePath string)
|
|
|
265
266
|
"RELAY_FLOW_NODE_TYPE": fields[7],
|
|
266
267
|
"RELAY_FLOW_NUDGE_PROMPT": fields[8],
|
|
267
268
|
"RELAY_FLOW_NEXT_STEPS_JSON": fields[9],
|
|
269
|
+
"RELAY_FLOW_REPORT_FORMAT": fields[10],
|
|
268
270
|
},
|
|
269
271
|
}
|
|
270
272
|
return capture
|
|
@@ -325,11 +327,12 @@ fi
|
|
|
325
327
|
[ -n "${RELAY_FLOW_NODE:-}" ] || exit 3
|
|
326
328
|
[ -n "${RELAY_FLOW_NODE_TYPE:-}" ] || exit 3
|
|
327
329
|
[ -n "${RELAY_FLOW_NEXT_STEPS_JSON:-}" ] || exit 3
|
|
330
|
+
[ -n "${RELAY_FLOW_REPORT_FORMAT:-}" ] || exit 3
|
|
328
331
|
|
|
329
|
-
printf '%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000' \
|
|
332
|
+
printf '%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000%s\000' \
|
|
330
333
|
"$PWD" "$RELAY_FLOW_HOME" "$RELAY_FLOW_RUN_ID" "$RELAY_FLOW_WORKFLOW" \
|
|
331
334
|
"$RELAY_FLOW_REPO" "$RELAY_FLOW_TICKET" "$RELAY_FLOW_NODE" "$RELAY_FLOW_NODE_TYPE" \
|
|
332
|
-
"${RELAY_FLOW_NUDGE_PROMPT:-}" "$RELAY_FLOW_NEXT_STEPS_JSON" "$original_args" > "$capture"
|
|
335
|
+
"${RELAY_FLOW_NUDGE_PROMPT:-}" "$RELAY_FLOW_NEXT_STEPS_JSON" "$RELAY_FLOW_REPORT_FORMAT" "$original_args" > "$capture"
|
|
333
336
|
`
|
|
334
337
|
if err := os.WriteFile(executable, []byte(script), 0o700); err != nil {
|
|
335
338
|
t.Fatal(err)
|
|
@@ -29,7 +29,7 @@ func TestPiRenderPromptSubstitutesInitialAndFeedbackData(t *testing.T) {
|
|
|
29
29
|
if err != nil {
|
|
30
30
|
t.Fatalf("RenderPrompt(initial): %v", err)
|
|
31
31
|
}
|
|
32
|
-
wantInitial := "
|
|
32
|
+
wantInitial := "Read the description of your assigned Jira task:\nacli jira workitem view \"PAY-234\" --fields \"summary,description\" --json\n\nFollow the work instructions, valid routes, and report format in that description."
|
|
33
33
|
if initial != wantInitial {
|
|
34
34
|
t.Fatalf("initial prompt = %q, want %q", initial, wantInitial)
|
|
35
35
|
}
|
|
@@ -38,10 +38,18 @@ func TestPiRenderPromptSubstitutesInitialAndFeedbackData(t *testing.T) {
|
|
|
38
38
|
if err != nil {
|
|
39
39
|
t.Fatalf("RenderPrompt(feedback): %v", err)
|
|
40
40
|
}
|
|
41
|
-
wantFeedback := "
|
|
41
|
+
wantFeedback := "Continue implement from the latest update on your assigned Jira task:\nacli jira workitem comment list --key \"PAY-234\" --limit 1 --order \"-created\" --json\n\nnudge jira|PAY-101|basicFlow|payments|implement|PAY-234|review (when: ready)"
|
|
42
42
|
if feedback != wantFeedback {
|
|
43
43
|
t.Fatalf("feedback prompt = %q, want %q", feedback, wantFeedback)
|
|
44
44
|
}
|
|
45
|
+
data.PreviousFeedback = "Add the missing test."
|
|
46
|
+
continued, err := h.RenderPrompt(harness.PromptFeedback, data, "")
|
|
47
|
+
if err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
if want := "Continue implement from the latest update on your assigned Jira task:\nacli jira workitem comment list --key \"PAY-234\" --limit 1 --order \"-created\" --json\n\nPrevious step feedback: Add the missing test."; continued != want {
|
|
51
|
+
t.Fatalf("Jira Pi continuation = %q, want %q", continued, want)
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
func TestPiRenderPromptHonorsNodeNudgeTimingInput(t *testing.T) {
|
|
@@ -107,11 +115,11 @@ func TestPiRenderPromptUsesNativeTemplateCommandForInitialNamedAgent(t *testing.
|
|
|
107
115
|
Mailbox: "PAY-234",
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
initial, err := h.RenderPrompt(harness.PromptInitial, data, "
|
|
118
|
+
initial, err := h.RenderPrompt(harness.PromptInitial, data, "Read the latest mailbox feedback for {{node}}.")
|
|
111
119
|
if err != nil {
|
|
112
120
|
t.Fatalf("RenderPrompt(initial): %v", err)
|
|
113
121
|
}
|
|
114
|
-
wantInitial := "/coder
|
|
122
|
+
wantInitial := "/coder Read the description of your assigned Jira task:\nacli jira workitem view \"PAY-234\" --fields \"summary,description\" --json\n\nFollow the work instructions, valid routes, and report format in that description."
|
|
115
123
|
if initial != wantInitial {
|
|
116
124
|
t.Fatalf("initial prompt = %q, want %q", initial, wantInitial)
|
|
117
125
|
}
|
|
@@ -120,7 +128,7 @@ func TestPiRenderPromptUsesNativeTemplateCommandForInitialNamedAgent(t *testing.
|
|
|
120
128
|
if err != nil {
|
|
121
129
|
t.Fatalf("RenderPrompt(feedback): %v", err)
|
|
122
130
|
}
|
|
123
|
-
wantFeedback := "
|
|
131
|
+
wantFeedback := "Continue implement from the latest update on your assigned Jira task:\nacli jira workitem comment list --key \"PAY-234\" --limit 1 --order \"-created\" --json\n\nfeedback nudge"
|
|
124
132
|
if feedback != wantFeedback {
|
|
125
133
|
t.Fatalf("feedback prompt = %q, want %q", feedback, wantFeedback)
|
|
126
134
|
}
|
|
@@ -61,7 +61,7 @@ func TestBuildCommandWithoutTaskEnvKeepsRelayVariablesOnly(t *testing.T) {
|
|
|
61
61
|
if err != nil {
|
|
62
62
|
t.Fatalf("BuildCommand: %v", err)
|
|
63
63
|
}
|
|
64
|
-
if len(cmd.Env) !=
|
|
65
|
-
t.Fatalf("Env = %#v, want only the
|
|
64
|
+
if len(cmd.Env) != 10 {
|
|
65
|
+
t.Fatalf("Env = %#v, want only the ten relay-flow variables", cmd.Env)
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -66,17 +66,16 @@ const (
|
|
|
66
66
|
defaultWorkTaskStatus = "In Progress"
|
|
67
67
|
defaultEndParentStatus = "Done"
|
|
68
68
|
currentUserAssigneeFilter = "currentUser()"
|
|
69
|
-
defaultMailboxDescription = `
|
|
70
|
-
Workflow: {{workflow}}
|
|
71
|
-
Node: {{node}}
|
|
72
|
-
Node type: {{nodeType}}
|
|
73
|
-
Agent: {{agent}}
|
|
74
|
-
Mailbox: {{mailbox}}
|
|
75
|
-
|
|
76
|
-
Node work:
|
|
69
|
+
defaultMailboxDescription = `{{ticket}} / {{node}} — {{nodeType}} — {{agent}}
|
|
77
70
|
{{nodeDescription}}
|
|
78
71
|
|
|
79
|
-
|
|
72
|
+
Success routes:
|
|
73
|
+
{{successRoutes}}
|
|
74
|
+
|
|
75
|
+
Failure routes:
|
|
76
|
+
{{failureRoutes}}
|
|
77
|
+
|
|
78
|
+
{{report}}`
|
|
80
79
|
defaultSummaryComment = `Summary for {{node}}
|
|
81
80
|
|
|
82
81
|
{{summaryReport}}`
|
|
@@ -93,6 +92,7 @@ var knownTextVars = map[string]bool{
|
|
|
93
92
|
"nodeDescription": true, "nextSteps": true, "successRoutes": true,
|
|
94
93
|
"failureRoutes": true, "mailbox": true, "sourceNode": true,
|
|
95
94
|
"targetNode": true, "summaryReport": true, "feedbackReport": true,
|
|
95
|
+
"report": true,
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// DefaultConfig supplies Jira task-system text defaults for relay-flow init.
|
|
@@ -509,6 +509,11 @@ func validateTemplates(templates Templates) error {
|
|
|
509
509
|
}
|
|
510
510
|
}
|
|
511
511
|
}
|
|
512
|
+
for _, key := range []string{"report", "ticket", "node", "nodeType", "agent", "nodeDescription", "successRoutes", "failureRoutes"} {
|
|
513
|
+
if !strings.Contains(templates.MailboxDescription, "{{"+key+"}}") {
|
|
514
|
+
return fmt.Errorf("mailboxDescription must contain {{%s}}", key)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
512
517
|
if !strings.Contains(templates.SummaryComment, "{{summaryReport}}") {
|
|
513
518
|
return fmt.Errorf("summaryComment must contain {{summaryReport}}")
|
|
514
519
|
}
|
|
@@ -535,11 +540,11 @@ func (s *system) RenderText(kind task.TextKind, data task.TextData) (string, err
|
|
|
535
540
|
"runID": data.RunID, "ticket": data.Ticket,
|
|
536
541
|
"workflow": data.Workflow, "repo": data.Repo, "node": data.Node,
|
|
537
542
|
"nodeType": data.NodeType, "agent": data.Agent,
|
|
538
|
-
"nodeDescription": data.NodeDescription, "nextSteps": data.NextSteps,
|
|
543
|
+
"nodeDescription": strings.ReplaceAll(data.NodeDescription, "{{ticket}}", data.Ticket), "nextSteps": data.NextSteps,
|
|
539
544
|
"successRoutes": data.SuccessRoutes, "failureRoutes": data.FailureRoutes,
|
|
540
545
|
"mailbox": data.Mailbox, "sourceNode": data.SourceNode,
|
|
541
546
|
"targetNode": data.TargetNode, "summaryReport": data.SummaryReport,
|
|
542
|
-
"feedbackReport": data.FeedbackReport,
|
|
547
|
+
"feedbackReport": data.FeedbackReport, "report": data.Report,
|
|
543
548
|
}
|
|
544
549
|
return textVarPattern.ReplaceAllStringFunc(tmpl, func(match string) string {
|
|
545
550
|
return values[textVarPattern.FindStringSubmatch(match)[1]]
|
|
@@ -10,6 +10,7 @@ import (
|
|
|
10
10
|
"github.com/rajpopat27/relay-flow/internal/config"
|
|
11
11
|
"github.com/rajpopat27/relay-flow/internal/run"
|
|
12
12
|
"github.com/rajpopat27/relay-flow/internal/task"
|
|
13
|
+
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
func TestTaskTextTemplatesExposeValues(t *testing.T) {
|
|
@@ -17,7 +18,7 @@ func TestTaskTextTemplatesExposeValues(t *testing.T) {
|
|
|
17
18
|
sys, err := newSystem(context.Background(), &fakeClient{fake: fake}, task.RepoSpec{
|
|
18
19
|
Name: "payments",
|
|
19
20
|
RootConfig: config.RawValues{"templates": map[string]any{
|
|
20
|
-
"mailboxDescription": "mailbox {{runID}}|{{ticket}}|{{workflow}}|{{repo}}|{{node}}|{{nodeType}}|{{agent}}|{{nodeDescription}}|{{nextSteps}}|{{successRoutes}}|{{failureRoutes}}|{{mailbox}}",
|
|
21
|
+
"mailboxDescription": "mailbox {{runID}}|{{ticket}}|{{workflow}}|{{repo}}|{{node}}|{{nodeType}}|{{agent}}|{{nodeDescription}}|{{nextSteps}}|{{successRoutes}}|{{failureRoutes}}|{{mailbox}}|{{report}}",
|
|
21
22
|
"summaryComment": "summary {{sourceNode}}|{{targetNode}}|{{mailbox}}\n{{summaryReport}}",
|
|
22
23
|
"feedbackComment": "feedback {{sourceNode}}|{{targetNode}}|{{mailbox}}\n{{feedbackReport}}",
|
|
23
24
|
}},
|
|
@@ -31,7 +32,7 @@ func TestTaskTextTemplatesExposeValues(t *testing.T) {
|
|
|
31
32
|
Node: "review", NodeType: "hitl", Agent: "reviewer", NodeDescription: "Review changes.",
|
|
32
33
|
NextSteps: "implement; end", SuccessRoutes: "end", FailureRoutes: "implement",
|
|
33
34
|
Mailbox: "PAY-3", SourceNode: "review", TargetNode: "implement",
|
|
34
|
-
SummaryReport: "COMPLETED:\nreviewed", FeedbackReport: "REQUIRED ACTIONS:\nfix it",
|
|
35
|
+
SummaryReport: "COMPLETED:\nreviewed", FeedbackReport: "REQUIRED ACTIONS:\nfix it", Report: "STATUS: success | failure",
|
|
35
36
|
}
|
|
36
37
|
description, err := sys.RenderText(task.TextMailboxDescription, data)
|
|
37
38
|
if err != nil {
|
|
@@ -52,12 +53,47 @@ func TestTaskTextTemplatesExposeValues(t *testing.T) {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
func TestJiraDefaultMailboxDescriptionIsCompact(t *testing.T) {
|
|
57
|
+
sys, err := newSystem(context.Background(), &fakeClient{fake: &fakeJira{}}, task.RepoSpec{
|
|
58
|
+
Name: "payments", RepoConfig: testRepoConfig(nil),
|
|
59
|
+
})
|
|
60
|
+
if err != nil {
|
|
61
|
+
t.Fatal(err)
|
|
62
|
+
}
|
|
63
|
+
data := task.TextData{
|
|
64
|
+
Ticket: "PAY-101", Node: "coder", NodeType: "agent", Agent: "coder",
|
|
65
|
+
NodeDescription: "Implement the change.", SuccessRoutes: "reviewer — when: ready",
|
|
66
|
+
FailureRoutes: "coder — when: retry", Report: workflow.ReportFormat,
|
|
67
|
+
}
|
|
68
|
+
description, err := sys.RenderText(task.TextMailboxDescription, data)
|
|
69
|
+
if err != nil {
|
|
70
|
+
t.Fatal(err)
|
|
71
|
+
}
|
|
72
|
+
want := "PAY-101 / coder — agent — coder\nImplement the change.\n\nSuccess routes:\nreviewer — when: ready\n\nFailure routes:\ncoder — when: retry\n\n" + workflow.ReportFormat
|
|
73
|
+
if description != want {
|
|
74
|
+
t.Fatalf("description = %q, want %q", description, want)
|
|
75
|
+
}
|
|
76
|
+
if strings.Contains(description, "comment history") {
|
|
77
|
+
t.Fatal("Jira description requested comment history")
|
|
78
|
+
}
|
|
79
|
+
data.NodeDescription = "Read the parent Jira ticket for this step:\nacli jira workitem view \"{{ticket}}\" --fields \"summary,description\" --json"
|
|
80
|
+
withParent, err := sys.RenderText(task.TextMailboxDescription, data)
|
|
81
|
+
if err != nil || !strings.Contains(withParent, `acli jira workitem view "PAY-101" --fields "summary,description" --json`) {
|
|
82
|
+
t.Fatalf("node-specific parent instruction = %q, %v", withParent, err)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
55
86
|
func TestCommentTemplatesRequireReportValues(t *testing.T) {
|
|
56
87
|
for _, tc := range []struct {
|
|
57
88
|
name string
|
|
58
89
|
override map[string]any
|
|
59
90
|
wantError string
|
|
60
91
|
}{
|
|
92
|
+
{name: "description", override: map[string]any{"mailboxDescription": "missing"}, wantError: "mailboxDescription must contain {{report}}"},
|
|
93
|
+
{name: "report only", override: map[string]any{"mailboxDescription": "{{report}}"}, wantError: "mailboxDescription must contain {{ticket}}"},
|
|
94
|
+
{name: "no node work", override: map[string]any{"mailboxDescription": strings.Replace(defaultMailboxDescription, "{{nodeDescription}}", "No work provided", 1)}, wantError: "mailboxDescription must contain {{nodeDescription}}"},
|
|
95
|
+
{name: "no success routes", override: map[string]any{"mailboxDescription": strings.Replace(defaultMailboxDescription, "{{successRoutes}}", "None", 1)}, wantError: "mailboxDescription must contain {{successRoutes}}"},
|
|
96
|
+
{name: "no failure routes", override: map[string]any{"mailboxDescription": strings.Replace(defaultMailboxDescription, "{{failureRoutes}}", "None", 1)}, wantError: "mailboxDescription must contain {{failureRoutes}}"},
|
|
61
97
|
{name: "summary", override: map[string]any{"summaryComment": "missing"}, wantError: "summaryComment must contain {{summaryReport}}"},
|
|
62
98
|
{name: "feedback", override: map[string]any{"feedbackComment": "missing"}, wantError: "feedbackComment must contain {{feedbackReport}}"},
|
|
63
99
|
{name: "unknown", override: map[string]any{"mailboxDescription": "{{mailboxInstructions}}"}, wantError: "unknown template variable {{mailboxInstructions}}"},
|
package/internal/task/task.go
CHANGED
|
@@ -9,6 +9,13 @@ import (
|
|
|
9
9
|
// represents an intentionally empty section.
|
|
10
10
|
const None = "None"
|
|
11
11
|
|
|
12
|
+
// ReportFormat is the canonical agent-facing contract. Runtime plugins parse
|
|
13
|
+
// these four fields into the existing structured Report wire shape.
|
|
14
|
+
const ReportFormat = `STATUS: success | failure
|
|
15
|
+
NEXT STEP: <one valid route>
|
|
16
|
+
SUMMARY: <concise result>
|
|
17
|
+
FEEDBACK: <concise handoff, or None when NEXT STEP is end>`
|
|
18
|
+
|
|
12
19
|
type Summary struct {
|
|
13
20
|
Completed string `json:"completed"`
|
|
14
21
|
Commits string `json:"commits"`
|