relay-flow 0.2.2-alpha → 0.2.4-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 +154 -13
- package/cmd/relay-flow/commands_test.go +5 -1
- package/cmd/relay-flow/main.go +27 -1
- package/cmd/relay-flow/pi_wiring_test.go +64 -0
- package/cmd/relay-flow/serve.go +14 -0
- package/examples/beads-workflow.yaml +3 -3
- package/examples/config-reference.yaml +144 -0
- package/examples/minimal-beads-task-workflow.yaml +34 -0
- package/examples/minimal-jira-task-workflow.yaml +68 -0
- package/examples/workflow-reference.yaml +111 -0
- package/internal/execution/goworkflows/activities.go +19 -0
- package/internal/execution/goworkflows/engine.go +20 -0
- package/internal/execution/goworkflows/engine_test.go +1 -1
- package/internal/execution/goworkflows/fakes_test.go +34 -6
- package/internal/execution/goworkflows/interpreter.go +48 -1
- package/internal/execution/goworkflows/projection.go +52 -11
- package/internal/execution/goworkflows/recovery_test.go +129 -0
- package/internal/harness/opencode/opencode.go +4 -4
- package/internal/harness/opencode/opencode_test.go +59 -4
- package/internal/harness/opencode/repo_setup.go +42 -2
- package/internal/harness/pi/config_test.go +46 -0
- package/internal/harness/pi/lifecycle_test.go +69 -0
- package/internal/harness/pi/pi.go +261 -0
- package/internal/harness/pi/pi_test.go +322 -0
- package/internal/harness/pi/prompt_test.go +121 -0
- package/internal/harness/pi/testdata/pi-0.84.1/capture.json +126 -0
- package/internal/harness/pi/testdata/pi-0.84.1/noninteractive-output.txt +11 -0
- package/internal/harness/pi/testdata/pi-0.84.1/tui-output-sanitized.txt +17 -0
- package/internal/harness/pi/validation_test.go +168 -0
- package/internal/identity/identity.go +28 -1
- package/internal/identity/identity_test.go +40 -0
- package/internal/run/manager.go +193 -25
- package/internal/run/run.go +24 -6
- package/internal/run/run_manager_test.go +100 -0
- package/internal/runner/herdr/baseref.go +60 -0
- package/internal/runner/herdr/herdr.go +578 -0
- package/internal/runner/herdr/herdr_test.go +688 -0
- package/internal/runner/herdr/herdrcli/contract.go +128 -0
- package/internal/runner/herdr/herdrcli/exec.go +68 -0
- package/internal/runner/herdr/herdrcli/herdrcli_test.go +274 -0
- package/internal/runner/herdr/herdrcli/live_test.go +132 -0
- package/internal/runner/herdr/herdrcli/operations.go +281 -0
- package/internal/runner/herdr/herdrcli/response.go +116 -0
- package/internal/runner/herdr/herdrcli/testdata/empty-panes.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/empty-tabs.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/error-not-git-worktree.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/error-pane-not-found.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/error-workspace-not-found.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/error-worktree-not-found.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/malformed.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-close.json +6 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-get.json +25 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-list.json +45 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-process-info-shell.json +22 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-process-info.json +23 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-rename.json +23 -0
- package/internal/runner/herdr/herdrcli/testdata/snapshot.json +213 -0
- package/internal/runner/herdr/herdrcli/testdata/strict-herdr.sh +175 -0
- package/internal/runner/herdr/herdrcli/testdata/tab-create.json +31 -0
- package/internal/runner/herdr/herdrcli/testdata/tab-list.json +26 -0
- package/internal/runner/herdr/herdrcli/testdata/workspace-close.json +6 -0
- package/internal/runner/herdr/herdrcli/testdata/worktree-create.json +58 -0
- package/internal/runner/herdr/herdrcli/testdata/worktree-list.json +35 -0
- package/internal/runner/herdr/herdrcli/testdata/worktree-open.json +59 -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/beads.go +16 -16
- package/internal/task/beads/config_compatibility_test.go +7 -8
- package/internal/task/beads/status_compatibility_test.go +43 -0
- package/internal/task/jira/filters_test.go +32 -6
- package/internal/task/jira/helpers_test.go +3 -0
- package/internal/task/jira/jira.go +43 -17
- package/internal/task/jira/transition_defaults_test.go +43 -0
- package/internal/task/task.go +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# relay-flow
|
|
2
2
|
|
|
3
|
-
Durable, graph-based agent workflow runner. A ticket is the unit of work; a workflow YAML declares nodes and routes; a durable engine (go-workflows + SQLite) drives progression, waits, retries, and recovery. The task system (Jira and Beads built-ins) supplies parent tickets and mailbox subtasks; the runner (Orca built-in) owns worktrees and terminals; the harness (OpenCode built-
|
|
3
|
+
Durable, graph-based agent workflow runner. A ticket is the unit of work; a workflow YAML declares nodes and routes; a durable engine (go-workflows + SQLite) drives progression, waits, retries, and recovery. The task system (Jira and Beads built-ins) supplies parent tickets and mailbox subtasks; the runner (Orca built-in) owns worktrees and terminals; the harness (OpenCode and Pi built-ins) owns agent sessions and report semantics. All three are pluggable.
|
|
4
4
|
|
|
5
5
|
This is a ground-up rewrite. The previous per-workflow, in-memory daemon is gone. There is no migration path and no compatibility layer.
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ This is a ground-up rewrite. The previous per-workflow, in-memory daemon is gone
|
|
|
25
25
|
go install github.com/rajpopat27/relay-flow/cmd/relay-flow@latest
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
OpenCode plugin
|
|
28
|
+
OpenCode plugin configuration uses both entrypoints. The server entrypoint is listed in `opencode.json`:
|
|
29
29
|
|
|
30
30
|
```json
|
|
31
31
|
{
|
|
@@ -34,7 +34,42 @@ OpenCode plugin: add `"relay-flow-plugin"` to the `plugin` array in your repo's
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
The
|
|
37
|
+
The native HITL approval entrypoint is listed in `.opencode/tui.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
42
|
+
"plugin": ["relay-flow-plugin"]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The OpenCode harness adds both entries when a repo is registered. The server
|
|
47
|
+
entrypoint registers sessions, handles agent reports, and nudges invalid agent
|
|
48
|
+
output. The TUI entrypoint handles only HITL reports: after a valid completed
|
|
49
|
+
assistant report it shows a native Approve/Reject dialog. Approval delivers
|
|
50
|
+
`{runId, node, reportId, report}` via `relay-flow report` with retry; rejection
|
|
51
|
+
delivers nothing. `reportId` comes from the harness session/message identity;
|
|
52
|
+
`nodeVisitID` is internal and is never part of either plugin payload.
|
|
53
|
+
|
|
54
|
+
Pi plugin: install the same published package manually in Pi's global package
|
|
55
|
+
settings before starting a Pi harness session:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pi install npm:relay-flow-plugin@<version>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Relay-flow does not install or configure the package automatically. Pi resolves
|
|
62
|
+
`pi.ts` from the package's `pi.extensions` manifest entry, so do not add
|
|
63
|
+
`-e`/`--extension` to the relay-flow launch command. The runner launches Pi in
|
|
64
|
+
an interactive PTY; the package's OpenCode entry point remains available for
|
|
65
|
+
OpenCode sessions.
|
|
66
|
+
|
|
67
|
+
The plugin is the report-path half of the harness contract: it registers each
|
|
68
|
+
emitted harness session with `{runId, node, sessionId}`, parses the agent's
|
|
69
|
+
structured report, applies the agent/HITL nudge policy, and delivers
|
|
70
|
+
`{runId, node, reportId, report}` via `relay-flow report` with retry.
|
|
71
|
+
`reportId` comes from the harness session/message identity; `nodeVisitID` is
|
|
72
|
+
internal and is never part of either plugin payload.
|
|
38
73
|
|
|
39
74
|
### One-time machine setup
|
|
40
75
|
|
|
@@ -53,6 +88,13 @@ relay-flow init --task-plugin beads --runner-plugin orca --harness-plugin openco
|
|
|
53
88
|
|
|
54
89
|
Beads authentication is owned by the Beads workspace and its `bd`/Dolt configuration. `relay-flow task auth` is a no-op for Beads and does not create relay-flow credentials; do not add a Jira token to a Beads-only installation.
|
|
55
90
|
|
|
91
|
+
Pi is available through the existing harness selection. For a non-interactive
|
|
92
|
+
setup, select it with the existing flags:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
relay-flow init --task-plugin jira --runner-plugin orca --harness-plugin pi
|
|
96
|
+
```
|
|
97
|
+
|
|
56
98
|
The full machine layout is fixed under `~/.relay-flow` (0700):
|
|
57
99
|
|
|
58
100
|
```
|
|
@@ -159,10 +201,14 @@ Beads workflow filters are structured and evaluated in relay-flow. For example,
|
|
|
159
201
|
taskConfig:
|
|
160
202
|
filters:
|
|
161
203
|
parentStatuses: [open]
|
|
162
|
-
issueTypes: [
|
|
204
|
+
issueTypes: [task]
|
|
163
205
|
labels: [relay-ready]
|
|
164
206
|
```
|
|
165
207
|
|
|
208
|
+
Jira and Beads use the same conceptual `Task` issue type, but each adapter
|
|
209
|
+
keeps its provider-native spelling: Jira uses `Task` and Beads uses `task`.
|
|
210
|
+
Filter values are exact and are not translated between providers.
|
|
211
|
+
|
|
166
212
|
The supported Beads status names are `open`, `in_progress`, `blocked`, `deferred`, `hooked`, and `closed`. The claimed-parent poll uses the canonical active set `open,in_progress,blocked,deferred`; it intentionally does not substitute `hooked` for `deferred`. Omitted lifecycle settings move the parent to `in_progress` at `start`, a work-node mailbox to `in_progress`, and the parent to `closed` at `end` — the same shape as Jira, with Beads-native values. Relay-flow creates one Repo Poller per registered repo, not one poller per workflow. Each poll reads ready top-level parents and relay-owned active parents, deduplicates them, and never routes mailbox children. Claims are permanent `wf:<workflow>` labels.
|
|
167
213
|
|
|
168
214
|
Beads does not need relay-flow credentials or a Beads-specific poller. In server mode, leave Dolt and Beads server setup running outside relay-flow and point each repo at its own `beadsDir`.
|
|
@@ -175,7 +221,12 @@ relay-flow workflow submit --file <path>
|
|
|
175
221
|
|
|
176
222
|
Workflows live at `~/.relay-flow/workflows/<name>.yaml` after submit. Replacement and removal are rejected while any run of that workflow is active.
|
|
177
223
|
|
|
178
|
-
Use [`examples/default-story-workflow.yaml`](examples/default-story-workflow.yaml)
|
|
224
|
+
Use [`examples/config-reference.yaml`](examples/config-reference.yaml) for the complete machine configuration, [`examples/workflow-reference.yaml`](examples/workflow-reference.yaml) for the complete workflow schema, or the provider-specific minimal workflows [`examples/minimal-jira-task-workflow.yaml`](examples/minimal-jira-task-workflow.yaml) and [`examples/minimal-beads-task-workflow.yaml`](examples/minimal-beads-task-workflow.yaml). Runtime node agents should follow [`docs/agent-instructions.md`](docs/agent-instructions.md). The existing [`examples/default-story-workflow.yaml`](examples/default-story-workflow.yaml) remains a more detailed Jira Story example, while [`examples/beads-workflow.yaml`](examples/beads-workflow.yaml) shows the Beads lifecycle shape. Replace the repo name and uncomment only the optional fields you need.
|
|
225
|
+
|
|
226
|
+
Task, runner, and harness plugins are selected machine-wide. A single relay-flow
|
|
227
|
+
configuration cannot run Jira and Beads simultaneously; use separate
|
|
228
|
+
`RELAY_FLOW_HOME` directories or machine configurations when both providers are
|
|
229
|
+
needed.
|
|
179
230
|
|
|
180
231
|
### Run
|
|
181
232
|
|
|
@@ -269,19 +320,89 @@ A parent moved to `in_progress` stays visible to the claimed-parent poll, which
|
|
|
269
320
|
|
|
270
321
|
Beads and Jira use the shared `filters`, `templates`, optional top-level
|
|
271
322
|
`assignee`, and `transitionTo` field names. `transitionTo` uses
|
|
272
|
-
`parentStatus` for the parent issue and `taskStatus` for a mailbox.
|
|
273
|
-
adapters
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
323
|
+
`parentStatus` for the parent issue and `taskStatus` for a mailbox. Both
|
|
324
|
+
adapters support the structured `filters.assignees` list. Jira values are
|
|
325
|
+
normalized account emails; Beads values are the provider's assignee strings.
|
|
326
|
+
Jira additionally supports the reserved `currentUser()` value inside
|
|
327
|
+
`filters.assignees`; relay-flow resolves it to the authenticated Jira email
|
|
328
|
+
without sending it as JQL. On first Jira authentication, when no root
|
|
329
|
+
`taskConfig.assignee` is configured, relay-flow stores the authenticated email
|
|
330
|
+
as the root mailbox assignee. In both adapters `assignee` applies to a node's
|
|
331
|
+
mailbox assignment; only `filters.assignees` controls parent-ticket pickup.
|
|
332
|
+
When `filters.assignees` is absent, there is no assignee filter. To make that
|
|
333
|
+
explicit while keeping mailbox assignment, set an empty list:
|
|
334
|
+
|
|
335
|
+
```yaml
|
|
336
|
+
taskConfig:
|
|
337
|
+
filters:
|
|
338
|
+
assignees: []
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
This still applies the other filters, such as project, status, issue type, and
|
|
342
|
+
labels. If mailbox assignment should also be disabled for a workflow, override
|
|
343
|
+
`assignee` with an empty string at that scope as well. Beads requires the repo-only
|
|
344
|
+
`taskConfig.beadsDir` shown above; Jira instead uses its repo `project` and
|
|
345
|
+
`component` keys. `project` and `component` are not Beads fields, and a Beads
|
|
346
|
+
issue prefix is not a component or workspace selector. Beads status values remain native (`open`,
|
|
279
347
|
`in_progress`, `blocked`, `deferred`, `hooked`, `closed`), while Jira values
|
|
280
348
|
remain native (`In Progress`, `Done`, and so on); relay-flow does not
|
|
281
349
|
translate arbitrary values between providers. Beads rejects workflow/node-level
|
|
282
350
|
template overrides because the fixed task text rendering contract has no
|
|
283
351
|
lower-scope input.
|
|
284
352
|
|
|
353
|
+
### Pi harness
|
|
354
|
+
|
|
355
|
+
Pi has one built-in coding agent. Relay-flow keeps Pi's configured model,
|
|
356
|
+
provider, tools, extensions, and settings, while allowing a workflow node to
|
|
357
|
+
select a repository-owned role prompt. `agent: default` uses Pi's built-in
|
|
358
|
+
coding agent without an additional role prompt. A non-default value such as
|
|
359
|
+
`coder` or `reviewer` is resolved to a readable, non-empty
|
|
360
|
+
`.pi/roles/<agent>.md` file in the registered repository and passed to Pi with
|
|
361
|
+
`--append-system-prompt`.
|
|
362
|
+
|
|
363
|
+
Pi has no native `pi agent list` command. Role existence is therefore verified
|
|
364
|
+
by the filesystem check above; missing, empty, non-regular, or unsafe role
|
|
365
|
+
paths fail workflow preflight. The workflow agent value is never treated as a
|
|
366
|
+
model ID and relay-flow never passes an OpenCode-style `--agent` option.
|
|
367
|
+
|
|
368
|
+
For example, a Pi workflow with repository roles uses:
|
|
369
|
+
|
|
370
|
+
```yaml
|
|
371
|
+
type: agent
|
|
372
|
+
agent: coder
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
with:
|
|
376
|
+
|
|
377
|
+
```text
|
|
378
|
+
.pi/roles/coder.md
|
|
379
|
+
.pi/roles/reviewer.md
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Pi node launches use the installed Pi 0.84.1 interactive command contract:
|
|
383
|
+
|
|
384
|
+
```text
|
|
385
|
+
pi --name <ticket>:<node> [--append-system-prompt <role-file>] [--session-id <persisted-session-id>] <prompt>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The prompt is one positional argument. Pi 0.84.1 rejects a bare `--`, so the
|
|
389
|
+
launch command does not include one. A persisted session uses
|
|
390
|
+
`--session-id`; print mode, JSON/RPC mode, and extension-install flags are not
|
|
391
|
+
used. The runner supplies a PTY for both standard streams, and the Pi process
|
|
392
|
+
remains available for interactive input after a response settles.
|
|
393
|
+
|
|
394
|
+
For a `hitl` node, a valid report is approved directly in Pi's host UI with
|
|
395
|
+
`ctx.ui.select`:
|
|
396
|
+
|
|
397
|
+
```text
|
|
398
|
+
Approve relay-flow report for <ticket>:<node>
|
|
399
|
+
Approve
|
|
400
|
+
Reject
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Approve delivers the report. Reject or Escape submits nothing and leaves the
|
|
404
|
+
durable run waiting; Pi does not require an LLM Question tool for this step.
|
|
405
|
+
|
|
285
406
|
---
|
|
286
407
|
|
|
287
408
|
## Structured node report
|
|
@@ -309,7 +430,27 @@ EXPECTED RESULT: ...
|
|
|
309
430
|
|
|
310
431
|
The labels above are fixed; configurable templates do not change the parsed report contract. The plugin submits one `report` object containing both lower-camel `summary` and `feedback` objects. Relay-flow validates that complete shape once, renders `summaryReport` through the task system's summary-comment template on the current mailbox, and renders `feedbackReport` through its feedback-comment template on only the selected next mailbox. `None` is the literal marker for an intentionally empty section. When `NEXT STEP` is `end`, every FEEDBACK field must be `None` and no feedback comment is written.
|
|
311
432
|
|
|
312
|
-
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; invalid HITL output stays silent.
|
|
433
|
+
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; invalid or missing HITL output stays silent, while a valid HITL report opens the native TUI approval dialog. Relay-flow HITL approval does not use OpenCode's Question tool.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Canceled run restart
|
|
438
|
+
|
|
439
|
+
Cancellation is permanent for the current execution. A canceled ticket is not
|
|
440
|
+
restarted by polling or by a ticket-status change. Start a fresh attempt
|
|
441
|
+
explicitly:
|
|
442
|
+
|
|
443
|
+
```sh
|
|
444
|
+
relay-flow run restart --ticket PAY-101
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
The new attempt starts at `start`, preserves the existing worktree/mailboxes/
|
|
448
|
+
comments/labels, and uses a numeric attempt ID (`2`, `3`, ...), with a fenced
|
|
449
|
+
execution ID such as `payments/basicFlow/PAY-101~attempt~2`. If a human has
|
|
450
|
+
moved the parent ticket to an incompatible status, `run get` shows `blocked`
|
|
451
|
+
with an instruction to move it to an allowed active start status; relay-flow
|
|
452
|
+
retries automatically and never overwrites the human-owned status. Done/Closed
|
|
453
|
+
tickets are not reopened automatically.
|
|
313
454
|
|
|
314
455
|
---
|
|
315
456
|
|
|
@@ -47,7 +47,7 @@ func TestCommandSurfaceExists(t *testing.T) {
|
|
|
47
47
|
{"workflow", "submit", "--file", "x.yaml"}, {"workflow", "remove", "--name", "x"},
|
|
48
48
|
{"workflow", "list"}, {"workflow", "get", "--name", "x"},
|
|
49
49
|
{"repo", "register"}, {"repo", "remove", "--name", "x"}, {"repo", "list"}, {"repo", "get", "--name", "x"},
|
|
50
|
-
{"run", "list"}, {"run", "get", "--ticket", "PAY-101"}, {"run", "cancel", "--ticket", "PAY-101"},
|
|
50
|
+
{"run", "list"}, {"run", "get", "--ticket", "PAY-101"}, {"run", "restart", "--ticket", "PAY-101"}, {"run", "cancel", "--ticket", "PAY-101"},
|
|
51
51
|
}
|
|
52
52
|
for _, argv := range commands {
|
|
53
53
|
// Recognized commands do not exit 2 ("usage/unknown"); they may exit
|
|
@@ -75,6 +75,7 @@ func TestRequiredFlagMissingExits2(t *testing.T) {
|
|
|
75
75
|
{"repo", "remove"}, // missing --name
|
|
76
76
|
{"repo", "get"}, // missing --name
|
|
77
77
|
{"run", "get"}, // missing --ticket
|
|
78
|
+
{"run", "restart"}, // missing --ticket
|
|
78
79
|
{"run", "cancel"}, // missing --ticket
|
|
79
80
|
} {
|
|
80
81
|
if code := cli(t, home, "", argv...); code != 2 {
|
|
@@ -1004,6 +1005,9 @@ func (s *ackServer) ListRuns(context.Context, runsvc.Filter) ([]runsvc.Run, erro
|
|
|
1004
1005
|
func (s *ackServer) GetRunByTicket(context.Context, string) (runsvc.Run, error) {
|
|
1005
1006
|
panic("unreachable")
|
|
1006
1007
|
}
|
|
1008
|
+
func (s *ackServer) RestartRun(context.Context, string) (runsvc.Run, error) {
|
|
1009
|
+
panic("unreachable")
|
|
1010
|
+
}
|
|
1007
1011
|
func (s *ackServer) CancelRun(context.Context, string, string) error { panic("unreachable") }
|
|
1008
1012
|
func (s *ackServer) DiscoverRepos(context.Context) ([]runner.RepoCandidate, error) {
|
|
1009
1013
|
panic("unreachable")
|
package/cmd/relay-flow/main.go
CHANGED
|
@@ -37,6 +37,8 @@ import (
|
|
|
37
37
|
// Adapter registrations (factories registered via init for plugin
|
|
38
38
|
// name validation at init-time).
|
|
39
39
|
_ "github.com/rajpopat27/relay-flow/internal/harness/opencode"
|
|
40
|
+
_ "github.com/rajpopat27/relay-flow/internal/harness/pi"
|
|
41
|
+
_ "github.com/rajpopat27/relay-flow/internal/runner/herdr"
|
|
40
42
|
_ "github.com/rajpopat27/relay-flow/internal/runner/orca"
|
|
41
43
|
_ "github.com/rajpopat27/relay-flow/internal/task/beads"
|
|
42
44
|
_ "github.com/rajpopat27/relay-flow/internal/task/jira"
|
|
@@ -149,6 +151,7 @@ Usage:
|
|
|
149
151
|
|
|
150
152
|
relay-flow run list
|
|
151
153
|
relay-flow run get --ticket <key>
|
|
154
|
+
relay-flow run restart --ticket <key>
|
|
152
155
|
relay-flow run cancel --ticket <key>`)
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -946,6 +949,25 @@ func cmdRun(c *server.Client, args []string) int {
|
|
|
946
949
|
enc.SetIndent("", " ")
|
|
947
950
|
_ = enc.Encode(rn)
|
|
948
951
|
return exitOK
|
|
952
|
+
case "restart":
|
|
953
|
+
fs := flag.NewFlagSet("run restart", flag.ContinueOnError)
|
|
954
|
+
ticket := fs.String("ticket", "", "ticket key")
|
|
955
|
+
if err := fs.Parse(args[1:]); err != nil {
|
|
956
|
+
return exitUsage
|
|
957
|
+
}
|
|
958
|
+
if *ticket == "" {
|
|
959
|
+
fmt.Fprintln(os.Stderr, "run restart: --ticket is required")
|
|
960
|
+
return exitUsage
|
|
961
|
+
}
|
|
962
|
+
rn, err := c.RestartRun(context.Background(), *ticket)
|
|
963
|
+
if err != nil {
|
|
964
|
+
fmt.Fprintln(os.Stderr, err)
|
|
965
|
+
return exitFail
|
|
966
|
+
}
|
|
967
|
+
enc := json.NewEncoder(os.Stdout)
|
|
968
|
+
enc.SetIndent("", " ")
|
|
969
|
+
_ = enc.Encode(rn)
|
|
970
|
+
return exitOK
|
|
949
971
|
case "cancel":
|
|
950
972
|
fs := flag.NewFlagSet("run cancel", flag.ContinueOnError)
|
|
951
973
|
ticket := fs.String("ticket", "", "ticket key")
|
|
@@ -968,7 +990,11 @@ func cmdRun(c *server.Client, args []string) int {
|
|
|
968
990
|
}
|
|
969
991
|
|
|
970
992
|
func formatRunListRow(r runsvc.Run) string {
|
|
971
|
-
|
|
993
|
+
attempt := r.AttemptID
|
|
994
|
+
if attempt == 0 {
|
|
995
|
+
attempt = 1
|
|
996
|
+
}
|
|
997
|
+
row := fmt.Sprintf("%s\t%s\t%s\t%s\tattempt=%d", r.ID, r.Ticket.Key, r.Workflow, r.State, attempt)
|
|
972
998
|
if r.Retry != nil {
|
|
973
999
|
row += fmt.Sprintf("\tretrying attempt=%d next=%s error=%q",
|
|
974
1000
|
r.Retry.Attempt, r.Retry.NextRetryAt.Format(time.RFC3339), r.Retry.LastError)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/rajpopat27/relay-flow/internal/config"
|
|
10
|
+
"github.com/rajpopat27/relay-flow/internal/harness"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestPiAppearsInDynamicHarnessSelection(t *testing.T) {
|
|
14
|
+
names := harness.Names()
|
|
15
|
+
if !containsHarnessName(names, "pi") {
|
|
16
|
+
t.Fatalf("harness.Names() = %v, want pi", names)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
var selected string
|
|
20
|
+
field, err := pluginSelectField("Select harness", names, &selected)
|
|
21
|
+
if err != nil {
|
|
22
|
+
t.Fatalf("pluginSelectField: %v", err)
|
|
23
|
+
}
|
|
24
|
+
var output bytes.Buffer
|
|
25
|
+
if err := field.RunAccessible(&output, strings.NewReader("1\n")); err != nil {
|
|
26
|
+
t.Fatalf("RunAccessible: %v", err)
|
|
27
|
+
}
|
|
28
|
+
if !strings.Contains(output.String(), "Select harness") {
|
|
29
|
+
t.Fatalf("selection output %q missing unchanged harness title", output.String())
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func TestInitPiFlagUsesGenericHarnessSelectionPath(t *testing.T) {
|
|
34
|
+
home := t.TempDir()
|
|
35
|
+
code, output := captureStdout(t, func() int {
|
|
36
|
+
return cli(t, home, "", "init",
|
|
37
|
+
"--task-plugin", "jira",
|
|
38
|
+
"--runner-plugin", "orca",
|
|
39
|
+
"--harness-plugin", "pi")
|
|
40
|
+
})
|
|
41
|
+
if code != 0 {
|
|
42
|
+
t.Fatalf("Pi init exit = %d, want 0", code)
|
|
43
|
+
}
|
|
44
|
+
if !strings.Contains(output, "Harness: pi") {
|
|
45
|
+
t.Fatalf("Pi init output = %q, want generic harness summary", output)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
cfg, err := config.LoadMachine(filepath.Join(home, ".relay-flow", "config.yaml"))
|
|
49
|
+
if err != nil {
|
|
50
|
+
t.Fatalf("load Pi machine config: %v", err)
|
|
51
|
+
}
|
|
52
|
+
if cfg.HarnessPlugin != "pi" {
|
|
53
|
+
t.Fatalf("harnessPlugin = %q, want pi", cfg.HarnessPlugin)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func containsHarnessName(names []string, want string) bool {
|
|
58
|
+
for _, name := range names {
|
|
59
|
+
if name == want {
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false
|
|
64
|
+
}
|
package/cmd/relay-flow/serve.go
CHANGED
|
@@ -34,6 +34,8 @@ import (
|
|
|
34
34
|
|
|
35
35
|
// Adapter registrations (factories are registered by name via init).
|
|
36
36
|
_ "github.com/rajpopat27/relay-flow/internal/harness/opencode"
|
|
37
|
+
_ "github.com/rajpopat27/relay-flow/internal/harness/pi"
|
|
38
|
+
_ "github.com/rajpopat27/relay-flow/internal/runner/herdr"
|
|
37
39
|
_ "github.com/rajpopat27/relay-flow/internal/runner/orca"
|
|
38
40
|
_ "github.com/rajpopat27/relay-flow/internal/task/beads"
|
|
39
41
|
_ "github.com/rajpopat27/relay-flow/internal/task/jira"
|
|
@@ -249,6 +251,8 @@ func serveRoot(ctx context.Context, p paths.Paths, recover bool) error {
|
|
|
249
251
|
// the SAME registry by pointer (via replaceInternal), so the engine,
|
|
250
252
|
// pollers, and handlers observe one in-memory repo set.
|
|
251
253
|
wfSvc := workflow.NewService(store, engine, repoExists{repoReg})
|
|
254
|
+
runManager.Repos = repoReg
|
|
255
|
+
runManager.Workflows = wfSvc.Registry()
|
|
252
256
|
wfSvc.Gate = lifecycleGate
|
|
253
257
|
wfSvc.ValidateTaskConfig = workflowConfigValidator(repoReg)
|
|
254
258
|
// Submit/Remove must also rebuild repo bindings under the gate (spec
|
|
@@ -455,6 +459,16 @@ func (d *serveDeps) ListRuns(ctx context.Context, filter runsvc.Filter) ([]runsv
|
|
|
455
459
|
func (d *serveDeps) GetRunByTicket(ctx context.Context, ticket string) (runsvc.Run, error) {
|
|
456
460
|
return d.engine.FindRunByTicket(ctx, ticket)
|
|
457
461
|
}
|
|
462
|
+
func (d *serveDeps) RestartRun(ctx context.Context, ticket string) (runsvc.Run, error) {
|
|
463
|
+
rn, err := d.runManager.RestartByTicket(ctx, ticket)
|
|
464
|
+
if err != nil {
|
|
465
|
+
if errors.Is(err, runsvc.ErrRestartConflict) {
|
|
466
|
+
return runsvc.Run{}, fmt.Errorf("%w: %v", server.ErrConflict, err)
|
|
467
|
+
}
|
|
468
|
+
return runsvc.Run{}, err
|
|
469
|
+
}
|
|
470
|
+
return rn, nil
|
|
471
|
+
}
|
|
458
472
|
func (d *serveDeps) CancelRun(ctx context.Context, ticket, reason string) error {
|
|
459
473
|
return d.runManager.CancelByTicket(ctx, ticket, reason)
|
|
460
474
|
}
|
|
@@ -16,13 +16,13 @@ taskConfig:
|
|
|
16
16
|
parentStatuses:
|
|
17
17
|
- open
|
|
18
18
|
issueTypes:
|
|
19
|
-
-
|
|
19
|
+
- task
|
|
20
20
|
labels:
|
|
21
21
|
- relay-ready
|
|
22
22
|
# assignees:
|
|
23
23
|
# - owner@example.com
|
|
24
|
-
# Optional
|
|
25
|
-
#
|
|
24
|
+
# Optional mailbox assignee inherited by nodes. It does not filter parent
|
|
25
|
+
# tickets; configure filters.assignees explicitly to filter pickup.
|
|
26
26
|
# assignee: owner@example.com
|
|
27
27
|
|
|
28
28
|
# transitionTo describes one lifecycle point, so configure it on a node.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Complete relay-flow machine configuration reference.
|
|
2
|
+
#
|
|
3
|
+
# This example uses Jira + Orca + OpenCode. Replace the example values before
|
|
4
|
+
# use. The live machine file is ~/.relay-flow/config.yaml.
|
|
5
|
+
#
|
|
6
|
+
# One task plugin, runner plugin, and harness plugin are selected for the whole
|
|
7
|
+
# machine. Jira and Beads therefore use separate relay-flow configurations.
|
|
8
|
+
|
|
9
|
+
pollIntervalSeconds: 15
|
|
10
|
+
completedRunRetentionDays: 30
|
|
11
|
+
|
|
12
|
+
# These default to true when omitted. keepTerminalsAlive: true requires
|
|
13
|
+
# keepSessionsAlive: true.
|
|
14
|
+
keepTerminalsAlive: true
|
|
15
|
+
keepSessionsAlive: true
|
|
16
|
+
|
|
17
|
+
taskPlugin: jira
|
|
18
|
+
|
|
19
|
+
taskConfig:
|
|
20
|
+
# Root mailbox assignee. This does not filter parent tickets; use
|
|
21
|
+
# filters.assignees for ticket pickup. Replace it with a real Jira account
|
|
22
|
+
# email or omit it.
|
|
23
|
+
assignee: relay-bot@example.com
|
|
24
|
+
|
|
25
|
+
# Task-system filters are adapter-owned. These values are Jira-native.
|
|
26
|
+
filters:
|
|
27
|
+
parentStatuses:
|
|
28
|
+
- To Do
|
|
29
|
+
issueTypes:
|
|
30
|
+
- Task
|
|
31
|
+
labels:
|
|
32
|
+
- relay-ready
|
|
33
|
+
assignees:
|
|
34
|
+
- currentUser()
|
|
35
|
+
# Jira resolves currentUser() to the authenticated Jira email and does
|
|
36
|
+
# not send it as JQL. Ticket pickup is controlled only by this list;
|
|
37
|
+
# the top-level assignee controls mailbox assignment.
|
|
38
|
+
|
|
39
|
+
# Avoid setting transitionTo at root scope unless the same values should
|
|
40
|
+
# apply to every lifecycle point, including end. Prefer node-level values.
|
|
41
|
+
# transitionTo:
|
|
42
|
+
# parentStatus: In Progress
|
|
43
|
+
# taskStatus: In Progress
|
|
44
|
+
|
|
45
|
+
# Task-system text templates. The fixed report contract is appended by
|
|
46
|
+
# relay-flow and is not changed by these templates.
|
|
47
|
+
templates:
|
|
48
|
+
mailboxDescription: |-
|
|
49
|
+
Parent ticket: {{ticket}}
|
|
50
|
+
Workflow: {{workflow}}
|
|
51
|
+
Node: {{node}}
|
|
52
|
+
Node type: {{nodeType}}
|
|
53
|
+
Agent: {{agent}}
|
|
54
|
+
Mailbox: {{mailbox}}
|
|
55
|
+
|
|
56
|
+
Node work:
|
|
57
|
+
{{nodeDescription}}
|
|
58
|
+
|
|
59
|
+
Read this mailbox's comments for feedback from previous nodes.
|
|
60
|
+
|
|
61
|
+
summaryComment: |-
|
|
62
|
+
Summary for {{node}}
|
|
63
|
+
|
|
64
|
+
{{summaryReport}}
|
|
65
|
+
|
|
66
|
+
feedbackComment: |-
|
|
67
|
+
Feedback from {{sourceNode}} to {{targetNode}} mailbox {{mailbox}}
|
|
68
|
+
|
|
69
|
+
{{feedbackReport}}
|
|
70
|
+
|
|
71
|
+
runnerPlugin: orca
|
|
72
|
+
|
|
73
|
+
runnerConfig:
|
|
74
|
+
# Optional Orca base branch. When omitted, Orca derives it from the repo's
|
|
75
|
+
# primary worktree.
|
|
76
|
+
baseRef: main
|
|
77
|
+
|
|
78
|
+
harnessPlugin: opencode
|
|
79
|
+
|
|
80
|
+
harnessConfig:
|
|
81
|
+
# OpenCode supports initial, feedback, and hitl templates.
|
|
82
|
+
initial: |-
|
|
83
|
+
Task system: {{taskSystem}}
|
|
84
|
+
Use the {{taskSystem}} tools to read the parent ticket {{ticket}}.
|
|
85
|
+
|
|
86
|
+
Your mailbox is {{mailbox}}. Read its description and comments for node instructions and feedback.
|
|
87
|
+
|
|
88
|
+
feedback: |-
|
|
89
|
+
New feedback was added to the comments section of your mailbox subtask {{mailbox}}. Read it.
|
|
90
|
+
|
|
91
|
+
hitl: |-
|
|
92
|
+
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
|
+
|
|
94
|
+
repos:
|
|
95
|
+
payments:
|
|
96
|
+
# Code/runner repository. It must already be registered in Orca.
|
|
97
|
+
path: /work/payments
|
|
98
|
+
|
|
99
|
+
taskConfig:
|
|
100
|
+
# Jira repo-scoped required values. component is derived from the
|
|
101
|
+
# relay-flow repo name during repo registration.
|
|
102
|
+
project: PAY
|
|
103
|
+
component: payments
|
|
104
|
+
|
|
105
|
+
# Optional repo-level overrides inherit into workflows and nodes.
|
|
106
|
+
# assignee: relay-bot@example.com
|
|
107
|
+
# filters:
|
|
108
|
+
# labels:
|
|
109
|
+
# - backend
|
|
110
|
+
|
|
111
|
+
# Beads variant:
|
|
112
|
+
# taskPlugin: beads
|
|
113
|
+
# repos:
|
|
114
|
+
# payments:
|
|
115
|
+
# path: /work/payments
|
|
116
|
+
# taskConfig:
|
|
117
|
+
# beadsDir: /work/payments/.beads
|
|
118
|
+
#
|
|
119
|
+
# Beads uses the shared taskConfig fields (filters, assignee, transitionTo,
|
|
120
|
+
# templates) but does not accept Jira-only project/component fields. beadsDir
|
|
121
|
+
# is required at repo scope and must not be supplied only at root scope.
|
|
122
|
+
#
|
|
123
|
+
# Pi variant:
|
|
124
|
+
# harnessPlugin: pi
|
|
125
|
+
# harnessConfig:
|
|
126
|
+
# initial: |
|
|
127
|
+
# Task system: {{taskSystem}}
|
|
128
|
+
# Use the {{taskSystem}} tools to read the parent ticket {{ticket}}.
|
|
129
|
+
#
|
|
130
|
+
# Your mailbox is {{mailbox}}. Read its description and comments for node instructions and feedback.
|
|
131
|
+
# feedback: |
|
|
132
|
+
# New feedback was added to the comments section of your mailbox subtask {{mailbox}}. Read it.
|
|
133
|
+
#
|
|
134
|
+
# Pi accepts only initial and feedback harness templates. Pi workflow nodes
|
|
135
|
+
# may use agent: default or a repository role in .pi/roles/<agent>.md; HITL
|
|
136
|
+
# approval is provided by Pi's host UI.
|
|
137
|
+
#
|
|
138
|
+
# Herdr variant:
|
|
139
|
+
# runnerPlugin: herdr
|
|
140
|
+
# runnerConfig:
|
|
141
|
+
# session: relay-flow
|
|
142
|
+
# socketPath: /run/user/1000/herdr.sock
|
|
143
|
+
#
|
|
144
|
+
# Do not combine Orca-only baseRef with Herdr-only configuration.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Smallest Beads Task workflow.
|
|
2
|
+
# Register the code repository with a repo-scoped beadsDir before submitting.
|
|
3
|
+
# For Pi, use default or a repository role from .pi/roles/<agent>.md.
|
|
4
|
+
name: minimalBeadsTaskFlow
|
|
5
|
+
|
|
6
|
+
repos:
|
|
7
|
+
- payments
|
|
8
|
+
|
|
9
|
+
cleanupRunnerOnEnd: true
|
|
10
|
+
|
|
11
|
+
taskConfig:
|
|
12
|
+
filters:
|
|
13
|
+
parentStatuses:
|
|
14
|
+
- open
|
|
15
|
+
issueTypes:
|
|
16
|
+
- task
|
|
17
|
+
labels:
|
|
18
|
+
- relay-ready
|
|
19
|
+
|
|
20
|
+
nodes:
|
|
21
|
+
start:
|
|
22
|
+
onSuccess:
|
|
23
|
+
- target: implement
|
|
24
|
+
|
|
25
|
+
implement:
|
|
26
|
+
type: agent
|
|
27
|
+
agent: build
|
|
28
|
+
description: Implement the Beads Task and verify the changes.
|
|
29
|
+
onSuccess:
|
|
30
|
+
- target: end
|
|
31
|
+
onFailure:
|
|
32
|
+
- target: implement
|
|
33
|
+
|
|
34
|
+
end: {}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Smallest Jira Task workflow with implementation, agent review, and HITL PR review.
|
|
2
|
+
# Register the repo with Jira project/component values before submitting.
|
|
3
|
+
# For Pi, replace build and plan with default.
|
|
4
|
+
name: minimalJiraTaskFlow
|
|
5
|
+
|
|
6
|
+
repos:
|
|
7
|
+
- payments
|
|
8
|
+
|
|
9
|
+
cleanupRunnerOnEnd: true
|
|
10
|
+
|
|
11
|
+
taskConfig:
|
|
12
|
+
filters:
|
|
13
|
+
parentStatuses:
|
|
14
|
+
- To Do
|
|
15
|
+
issueTypes:
|
|
16
|
+
- Task
|
|
17
|
+
labels:
|
|
18
|
+
- relay-ready
|
|
19
|
+
assignees:
|
|
20
|
+
- currentUser()
|
|
21
|
+
# Jira resolves currentUser() to the authenticated Jira email.
|
|
22
|
+
# Replace this list with `assignees: []` to match all assignees.
|
|
23
|
+
|
|
24
|
+
nodes:
|
|
25
|
+
start:
|
|
26
|
+
onSuccess:
|
|
27
|
+
- target: implement
|
|
28
|
+
|
|
29
|
+
implement:
|
|
30
|
+
type: agent
|
|
31
|
+
agent: build
|
|
32
|
+
description: |
|
|
33
|
+
Implement the Jira Task in the ticket worktree.
|
|
34
|
+
Run the relevant tests and verification commands.
|
|
35
|
+
onSuccess:
|
|
36
|
+
- target: review
|
|
37
|
+
when: Implementation and verification are complete.
|
|
38
|
+
onFailure:
|
|
39
|
+
- target: implement
|
|
40
|
+
when: Implementation needs another pass.
|
|
41
|
+
|
|
42
|
+
review:
|
|
43
|
+
type: agent
|
|
44
|
+
agent: plan
|
|
45
|
+
description: |
|
|
46
|
+
Review the implementation for correctness, regressions, missing tests,
|
|
47
|
+
and unresolved issues. Prepare the work for human PR review.
|
|
48
|
+
onSuccess:
|
|
49
|
+
- target: prReview
|
|
50
|
+
when: The implementation is ready for human PR review.
|
|
51
|
+
onFailure:
|
|
52
|
+
- target: implement
|
|
53
|
+
when: The review found issues requiring implementation changes.
|
|
54
|
+
|
|
55
|
+
prReview:
|
|
56
|
+
type: hitl
|
|
57
|
+
agent: plan
|
|
58
|
+
description: |
|
|
59
|
+
Review the implementation and proposed changes with a human.
|
|
60
|
+
Approve the work or request implementation changes.
|
|
61
|
+
onSuccess:
|
|
62
|
+
- target: end
|
|
63
|
+
when: The human approves the PR review.
|
|
64
|
+
onFailure:
|
|
65
|
+
- target: implement
|
|
66
|
+
when: The human requests changes.
|
|
67
|
+
|
|
68
|
+
end: {}
|