relay-flow 0.3.12-alpha → 0.3.13-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 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.12-alpha
31
+ opencode plugin relay-flow-plugin@0.3.13-alpha
32
32
  ```
33
33
 
34
34
  **Pi**
35
35
 
36
36
  ```sh
37
- pi install npm:relay-flow-plugin@0.3.12-alpha
37
+ pi install npm:relay-flow-plugin@0.3.13-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.12-alpha"]
167
+ "plugin": ["relay-flow-plugin@0.3.13-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.12-alpha"]
176
+ "plugin": ["relay-flow-plugin@0.3.13-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.12-alpha
192
+ pi install npm:relay-flow-plugin@0.3.13-alpha
193
193
  ```
194
194
 
195
195
  Relay-flow does not install or configure the package automatically. Pi resolves
@@ -308,6 +308,45 @@ func TestReportValidationErrorIsStructuredOnStderr(t *testing.T) {
308
308
  }
309
309
  }
310
310
 
311
+ func TestReportOtherAPIErrorsPreserveCodeForRetryClassification(t *testing.T) {
312
+ const report = `{"runId":"run-1","node":"coding","reportId":"s:m","report":{"status":"success","nextStep":"end"}}`
313
+ for _, tc := range []struct {
314
+ name, code string
315
+ err error
316
+ }{
317
+ {"invalid request", "invalid", fmt.Errorf("%w: malformed report", server.ErrInvalid)},
318
+ {"server failure", "internalError", errors.New("temporary backend failure")},
319
+ } {
320
+ t.Run(tc.name, func(t *testing.T) {
321
+ home := t.TempDir()
322
+ serveAck(t, home, runsvc.ReportAck{}, tc.err)
323
+ reader, writer, err := os.Pipe()
324
+ if err != nil {
325
+ t.Fatal(err)
326
+ }
327
+ original := os.Stderr
328
+ os.Stderr = writer
329
+ code := cli(t, home, report, "report")
330
+ os.Stderr = original
331
+ _ = writer.Close()
332
+ defer reader.Close()
333
+ output, err := io.ReadAll(reader)
334
+ if err != nil {
335
+ t.Fatal(err)
336
+ }
337
+ var response struct {
338
+ Error struct {
339
+ Code string `json:"code"`
340
+ Message string `json:"message"`
341
+ } `json:"error"`
342
+ }
343
+ if err := json.Unmarshal(output, &response); err != nil || code != exitFail || response.Error.Code != tc.code || response.Error.Message != tc.err.Error() {
344
+ t.Fatalf("CLI response: exit=%d stderr=%q parsed=%+v err=%v", code, output, response, err)
345
+ }
346
+ })
347
+ }
348
+ }
349
+
311
350
  func TestReportUnreachableServerExits1(t *testing.T) {
312
351
  valid := `{"runId":"payments/basicFlow/PAY-101","node":"coding","reportId":"s:m","report":{"status":"success","nextStep":"end","summary":{"completed":"x","commits":"abc123","notCompleted":"None","issuesDiscovered":"None","verification":"x","notes":"None"},"feedback":{"reasonForNextStep":"None","requiredActions":"None","relevantContext":"None","expectedResult":"None"}}}`
313
352
  if code := cli(t, t.TempDir(), valid, "report"); code != 1 {
@@ -1148,7 +1148,7 @@ func cmdReport(c *server.Client, stdin io.Reader) int {
1148
1148
  defer cancel()
1149
1149
  if _, err := c.SubmitReport(ctx, req); err != nil {
1150
1150
  var apiErr *server.APIError
1151
- if errors.As(err, &apiErr) && apiErr.Code == "invalidReport" {
1151
+ if errors.As(err, &apiErr) {
1152
1152
  _ = json.NewEncoder(os.Stderr).Encode(map[string]any{
1153
1153
  "error": map[string]string{"code": apiErr.Code, "message": apiErr.Message},
1154
1154
  })
@@ -316,7 +316,7 @@ func TestInvalidReportIsPermanentAndDoesNotSignal(t *testing.T) {
316
316
  if !errors.Is(err, run.ErrInvalidReport) || ack.Accepted {
317
317
  t.Fatalf("invalid report: ack=%+v err=%v", ack, err)
318
318
  }
319
- if want := `report selects "end": every feedback field must be "None" because end has no mailbox`; err.Error() != want {
319
+ if want := `NEXT STEP is end, so FEEDBACK must be exactly None.`; err.Error() != want {
320
320
  t.Fatalf("validation message = %q, want %q", err, want)
321
321
  }
322
322
  after, err := engine.GetRun(context.Background(), rid)
@@ -85,7 +85,7 @@ func TestTemporalSubmitReportRejectsInvalidReportBeforeSignal(t *testing.T) {
85
85
  if !errors.Is(err, run.ErrInvalidReport) || ack.Accepted || client.signaled {
86
86
  t.Fatalf("invalid Temporal report: ack=%+v err=%v signaled=%v", ack, err, client.signaled)
87
87
  }
88
- if want := `report selects "end": every feedback field must be "None" because end has no mailbox`; err.Error() != want {
88
+ if want := `NEXT STEP is end, so FEEDBACK must be exactly None.`; err.Error() != want {
89
89
  t.Fatalf("validation message = %q, want %q", err, want)
90
90
  }
91
91
  }
@@ -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.12-alpha"
17
+ const configuredPlugin = "relay-flow-plugin@0.3.13-alpha"
18
18
 
19
19
  func TestBuildCommandArgv(t *testing.T) {
20
20
  t.Setenv("RELAY_FLOW_HOME", "/var/lib/relay-flow-test")
@@ -10,7 +10,7 @@ import (
10
10
  "github.com/rajpopat27/relay-flow/internal/config"
11
11
  )
12
12
 
13
- const relayFlowPlugin = "relay-flow-plugin@0.3.12-alpha"
13
+ const relayFlowPlugin = "relay-flow-plugin@0.3.13-alpha"
14
14
 
15
15
  type jsoncToken struct {
16
16
  kind byte
@@ -50,9 +50,6 @@ func (w *Workflow) ValidateReport(node string, report Report) error {
50
50
  if report.Status != OutcomeSuccess && report.Status != OutcomeFailure {
51
51
  return fmt.Errorf("report status %q must be %q or %q", report.Status, OutcomeSuccess, OutcomeFailure)
52
52
  }
53
- if report.Status == OutcomeFailure && report.NextStep == EndNode {
54
- return fmt.Errorf("failure reports cannot select %q", EndNode)
55
- }
56
53
  for _, field := range []struct{ name, value string }{
57
54
  {"summary.completed", report.Summary.Completed},
58
55
  {"summary.commits", report.Summary.Commits},
@@ -74,6 +71,9 @@ func (w *Workflow) ValidateReport(node string, report Report) error {
74
71
  if err != nil {
75
72
  return err
76
73
  }
74
+ if report.Status == OutcomeFailure && report.NextStep == EndNode {
75
+ return fmt.Errorf("STATUS is failure, so NEXT STEP cannot be end; valid targets: %s", strings.Join(sortedTargets(routes), ", "))
76
+ }
77
77
  legal := false
78
78
  for _, r := range routes {
79
79
  if r.Target == report.NextStep {
@@ -88,7 +88,7 @@ func (w *Workflow) ValidateReport(node string, report Report) error {
88
88
  if report.NextStep == EndNode {
89
89
  f := report.Feedback
90
90
  if f.ReasonForNextStep != None || f.RequiredActions != None || f.RelevantContext != None || f.ExpectedResult != None {
91
- return fmt.Errorf("report selects %q: every feedback field must be %q because end has no mailbox", EndNode, None)
91
+ return fmt.Errorf("NEXT STEP is end, so FEEDBACK must be exactly None.")
92
92
  }
93
93
  }
94
94
  return nil
@@ -208,8 +208,8 @@ func TestValidateReportNextStepMustMatchStatusRoute(t *testing.T) {
208
208
  Summary: fullSummary(),
209
209
  Feedback: fullFeedback(),
210
210
  }
211
- if err := wf.ValidateReport("coding", r); err == nil {
212
- t.Fatal("failure report selecting a success-only target accepted")
211
+ if err := wf.ValidateReport("coding", r); err == nil || !strings.Contains(err.Error(), "valid targets: coding") {
212
+ t.Fatalf("failure report selecting a success-only target: %v, want configured failure route", err)
213
213
  }
214
214
  })
215
215
 
@@ -265,8 +265,8 @@ func TestValidateReportEndRequiresNoneFeedback(t *testing.T) {
265
265
  Summary: fullSummary(),
266
266
  Feedback: fullFeedback(),
267
267
  }
268
- if err := wf.ValidateReport("coding", r); err == nil {
269
- t.Fatal("end report carrying feedback accepted; end has no mailbox")
268
+ if err := wf.ValidateReport("coding", r); err == nil || err.Error() != "NEXT STEP is end, so FEEDBACK must be exactly None." {
269
+ t.Fatalf("end report error = %v, want actionable four-field correction", err)
270
270
  }
271
271
  })
272
272
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-flow",
3
- "version": "0.3.12-alpha",
3
+ "version": "0.3.13-alpha",
4
4
  "description": "Graph-based agent workflow engine — tracker-agnostic, pluggable runners",
5
5
  "bin": {
6
6
  "relay-flow": "./bin/relay-flow.js",