relay-flow 0.0.1 → 0.2.0-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.
Files changed (116) hide show
  1. package/README.md +148 -143
  2. package/cmd/relay-flow/commands_test.go +464 -0
  3. package/cmd/relay-flow/main.go +670 -180
  4. package/cmd/relay-flow/scenario_test.go +1135 -0
  5. package/cmd/relay-flow/serve.go +609 -0
  6. package/go.mod +69 -2
  7. package/go.sum +185 -0
  8. package/internal/config/config.go +88 -0
  9. package/internal/config/machine.go +99 -48
  10. package/internal/config/machine_test.go +248 -0
  11. package/internal/config/merge_test.go +118 -0
  12. package/internal/config/writeatomic.go +36 -0
  13. package/internal/config/writeatomic_test.go +98 -0
  14. package/internal/execution/goworkflows/activities.go +490 -0
  15. package/internal/execution/goworkflows/engine.go +487 -0
  16. package/internal/execution/goworkflows/engine_test.go +600 -0
  17. package/internal/execution/goworkflows/fakes_test.go +517 -0
  18. package/internal/execution/goworkflows/interpreter.go +605 -0
  19. package/internal/execution/goworkflows/logging_test.go +154 -0
  20. package/internal/execution/goworkflows/mailbox_test.go +423 -0
  21. package/internal/execution/goworkflows/node_runtime_integration_test.go +127 -0
  22. package/internal/execution/goworkflows/node_runtime_test.go +486 -0
  23. package/internal/execution/goworkflows/projection.go +504 -0
  24. package/internal/execution/goworkflows/recovery_test.go +1092 -0
  25. package/internal/execution/goworkflows/retry_log_test.go +59 -0
  26. package/internal/execution/goworkflows/retry_projection_test.go +98 -0
  27. package/internal/harness/contract_test.go +169 -0
  28. package/internal/harness/factory.go +63 -0
  29. package/internal/harness/harness.go +41 -0
  30. package/internal/harness/opencode/opencode.go +166 -0
  31. package/internal/harness/opencode/opencode_test.go +50 -0
  32. package/internal/harness/plugin_selection_test.go +126 -0
  33. package/internal/identity/identity.go +37 -0
  34. package/internal/logging/logging.go +56 -0
  35. package/internal/logging/logging_test.go +116 -0
  36. package/internal/paths/paths.go +67 -0
  37. package/internal/recover/recover.go +115 -0
  38. package/internal/repo/poller.go +186 -0
  39. package/internal/repo/poller_test.go +327 -0
  40. package/internal/repo/repo.go +119 -0
  41. package/internal/repo/service.go +216 -0
  42. package/internal/repo/service_test.go +298 -0
  43. package/internal/retry/retry.go +118 -0
  44. package/internal/router/router.go +83 -0
  45. package/internal/router/router_test.go +144 -0
  46. package/internal/run/manager.go +108 -0
  47. package/internal/run/run.go +140 -0
  48. package/internal/run/run_identity_test.go +52 -0
  49. package/internal/run/run_manager_test.go +266 -0
  50. package/internal/runner/contract_test.go +221 -0
  51. package/internal/runner/factory.go +65 -0
  52. package/internal/runner/orca/orca.go +363 -170
  53. package/internal/runner/orca/orca_test.go +134 -160
  54. package/internal/runner/orca/orcacli/orcacli.go +215 -0
  55. package/internal/runner/orca/orcacli/orcacli_test.go +154 -0
  56. package/internal/runner/orca/orcacli/testdata/repo-list.json +18 -0
  57. package/internal/runner/orca/orcacli/testdata/strict-orca.sh +30 -0
  58. package/internal/runner/orca/orcacli/testdata/terminal-close.json +12 -0
  59. package/internal/runner/orca/orcacli/testdata/terminal-create.json +18 -0
  60. package/internal/runner/orca/orcacli/testdata/terminal-list.json +51 -0
  61. package/internal/runner/orca/orcacli/testdata/terminal-send.json +1 -0
  62. package/internal/runner/orca/orcacli/testdata/terminal-show.json +1 -0
  63. package/internal/runner/orca/orcacli/testdata/worktree-create.json +22 -0
  64. package/internal/runner/orca/orcacli/testdata/worktree-list.json +31 -0
  65. package/internal/runner/orca/orcacli/testdata/worktree-remove.json +6 -0
  66. package/internal/runner/runner.go +47 -64
  67. package/internal/server/api_test.go +300 -0
  68. package/internal/server/client.go +192 -74
  69. package/internal/server/fixture_test.go +248 -0
  70. package/internal/server/server.go +425 -248
  71. package/internal/server/shutdown_test.go +116 -0
  72. package/internal/task/contract_test.go +223 -0
  73. package/internal/task/factory.go +103 -0
  74. package/internal/task/jira/acli/acli.go +306 -0
  75. package/internal/task/jira/acli/acli_test.go +208 -0
  76. package/internal/task/jira/acli/testdata/acli_comments.json +55 -0
  77. package/internal/task/jira/acli/testdata/search_invalid_assignee.txt +1 -0
  78. package/internal/task/jira/acli/testdata/search_invalid_status.txt +1 -0
  79. package/internal/task/jira/acli/testdata/search_success.json +1 -0
  80. package/internal/task/jira/filters_test.go +234 -0
  81. package/internal/task/jira/helpers_test.go +60 -0
  82. package/internal/task/jira/jira.go +507 -0
  83. package/internal/task/jira/normalize.go +101 -0
  84. package/internal/task/jira/testdata/acli_search.json +120 -0
  85. package/internal/task/jira/transition_defaults_test.go +156 -0
  86. package/internal/task/jira/validation_test.go +94 -0
  87. package/internal/task/task.go +84 -0
  88. package/internal/workflow/report.go +85 -0
  89. package/internal/workflow/report_test.go +259 -0
  90. package/internal/workflow/service.go +142 -0
  91. package/internal/workflow/store.go +136 -0
  92. package/internal/workflow/store_test.go +282 -0
  93. package/internal/workflow/workflow.go +342 -0
  94. package/internal/workflow/workflow_test.go +410 -0
  95. package/package.json +1 -1
  96. package/internal/acli/acli.go +0 -229
  97. package/internal/config/demo_test.go +0 -17
  98. package/internal/config/schema.go +0 -193
  99. package/internal/config/schema_test.go +0 -162
  100. package/internal/daemon/daemon.go +0 -218
  101. package/internal/daemon/daemon_test.go +0 -204
  102. package/internal/discovery/discovery.go +0 -122
  103. package/internal/discovery/discovery_test.go +0 -62
  104. package/internal/opencode/opencode.go +0 -26
  105. package/internal/orcacli/orcacli.go +0 -264
  106. package/internal/runner/orca/README.md +0 -64
  107. package/internal/runner/runner_test.go +0 -64
  108. package/internal/server/server_test.go +0 -195
  109. package/internal/tasks/jira/README.md +0 -69
  110. package/internal/tasks/jira/component_test.go +0 -16
  111. package/internal/tasks/jira/decode.go +0 -24
  112. package/internal/tasks/jira/jira.go +0 -231
  113. package/internal/tasks/jira/jira_test.go +0 -259
  114. package/internal/tasks/jira/jql_test.go +0 -16
  115. package/internal/tasks/tasks.go +0 -90
  116. package/internal/tasks/tasks_test.go +0 -91
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # relay-flow
2
2
 
3
- Graph-based agent workflow engine. Tickets are tokens moving across **nodes**; each node has an **agent** (an OpenCode agent); the node's edges (`onSuccess`/`onFailure`) decide where the token goes next. The tracker (Jira built-in) is the scoreboard; the runner (Orca built-in) is the playing field. Both are pluggable.
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 built-in) supplies parent tickets and mailbox subtasks; the runner (Orca built-in) owns worktrees and terminals; the harness (OpenCode built-in) owns agent sessions and report semantics. All three are pluggable.
4
+
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.
4
6
 
5
7
  ---
6
8
 
@@ -10,190 +12,193 @@ Graph-based agent workflow engine. Tickets are tokens moving across **nodes**; e
10
12
 
11
13
  | Tool | Why |
12
14
  |---|---|
13
- | [opencode](https://opencode.ai) | Agents run in opencode sessions |
14
- | [Orca](https://github.com/Necmttn/orca) CLI + app | Worktrees + terminals (the built-in runner) |
15
- | [acli](https://developer.atlassian.com/cloud/acli) | Jira access (the built-in tracker) |
16
- | Go 1.21+ | Build the CLI |
15
+ | [opencode](https://opencode.ai) | Agents run in opencode sessions (harness) |
16
+ | [Orca](https://github.com/Necmttn/orca) CLI + app | Worktrees + terminals (runner) |
17
+ | [acli](https://developer.atlassian.com/cloud/acli) | Jira access (task system) |
18
+ | Go 1.24+ | Build the CLI |
17
19
 
18
20
  ### Install
19
21
 
20
22
  ```sh
21
- # simplest — prebuilt binary into ~/.local/bin:
22
- curl -fsSL https://raw.githubusercontent.com/rajpopat27/relay-flow/main/install.sh | sh
23
+ go install github.com/rajpopat27/relay-flow/cmd/relay-flow@latest
24
+ ```
25
+
26
+ OpenCode plugin: add `"relay-flow-plugin"` to the `plugin` array in your repo's `opencode.json`:
23
27
 
24
- # or once npm unblocks (24h cooldown after unpublish):
25
- npm install -g relay-flow
28
+ ```json
29
+ {
30
+ "$schema": "https://opencode.ai/config.json",
31
+ "plugin": ["relay-flow-plugin"]
32
+ }
33
+ ```
34
+
35
+ The plugin is the report-path half of the harness contract: it parses the agent's structured report, applies the agent/HITL nudge policy, and delivers `{runId, node, reportId, report}` via `relay-flow report` with retry.
26
36
 
27
- # or with Go:
28
- go install github.com/rajpopat27/relay-flow/cmd/relay-flow@v0.1.2
37
+ ### One-time machine setup
29
38
 
30
- # or with Homebrew (after the v0.1.2 release publishes the formula):
31
- brew install rajpopat27/tap/relay-flow
39
+ ```sh
40
+ relay-flow init
32
41
  ```
33
42
 
34
- Install the opencode plugin (report loopback): copy `plugin/report-status.ts` into your repo's `.opencode/plugin/` directory (auto-loaded by opencode), committed so every ticket worktree inherits it.
43
+ Prompts for the three plugin names (`task`, `runner`, `harness` e.g. `jira`, `orca`, `opencode`), atomically writes `~/.relay-flow/config.yaml` (0600), and initializes `~/.relay-flow/state.db`. Refuses to overwrite existing configuration or history.
35
44
 
36
- ### Required configuration
45
+ The full machine layout is fixed under `~/.relay-flow` (0700):
37
46
 
38
- 1. **Machine identity** (per machine, never committed):
39
- ```sh
40
- relay-flow init --assignee "Jane Doe" # Jira display name or accountId
41
- ```
42
- Writes `~/.relay-flow/config.yaml` (0600), probe-validated against Jira.
47
+ ```
48
+ config.yaml 0600 machine config
49
+ state.db 0600 durable execution (SQLite)
50
+ server.sock 0600 CLI ↔ server
51
+ server.lock 0600 single-process flock
52
+ server.log 0600
53
+ plugin.log 0600
54
+ workflows/<name>.yaml 0644 submitted workflow definitions
55
+ ```
43
56
 
44
- 2. **Orca repo** — the repo must be registered in Orca (`orca repo add --path .`) with a base ref set (`orca repo set-base-ref --repo id:<id> --ref master`).
57
+ ### Register a repo
45
58
 
46
- 3. **Jira board transitions** must allow the moves your edges imply (e.g. To Do → In Progress → Testing → In Review → Done).
59
+ ```sh
60
+ relay-flow repo register
61
+ ```
47
62
 
48
- 4. **Workflow YAML** at `.workflow/workflow.yaml` (committed, team-shared):
63
+ Discovers/selects a runner repo, collects the task plugin's required keys, validates runner + task connectivity, and atomically saves. Registration is rejected while another registered repo already holds the same canonical task scope (e.g. same Jira site+project+component).
49
64
 
50
- ```yaml
51
- name: xyzTaskFlow # camelCase identity: registry key + claim label wf:<name>
52
- pollIntervalSeconds: 15 # optional, default 15
65
+ ### Submit a workflow
66
+
67
+ ```sh
68
+ relay-flow workflow submit --file <path>
69
+ ```
70
+
71
+ Workflows live at `~/.relay-flow/workflows/<name>.yaml` after submit. Replacement and removal are rejected while any run of that workflow is active.
72
+
73
+ ### Run
74
+
75
+ ```sh
76
+ relay-flow serve # normal start; requires an initialized database
77
+ relay-flow serve --recover # explicit destructive rebuild from the task system
78
+ relay-flow stop
79
+ ```
80
+
81
+ `serve --recover` treats ALL SQLite execution state as gone, closes surviving run-owned terminals (preserving worktrees and code), resets Jira parent+mailbox state, and starts every labeled parent in a fresh deterministic run from `start` with fresh `nodeVisitID`s. Recovery never runs automatically; database loss is never inferred.
82
+
83
+ ---
53
84
 
54
- tasks: # ticket-system adapter
55
- type: jira
56
- config: # opaque to core; strictly validated by the adapter
57
- query: project = ABCD # JQL fragment (no issuetype/assignee/ORDER BY)
58
- issueTypes: [Task]
59
- assigneeIsAgent: true # or omit → assignee comes from `relay-flow init`
85
+ ## Workflow YAML
60
86
 
61
- runner: # execution backend
62
- type: orca
87
+ ```yaml
88
+ name: basicFlow # lowerCamel; determines claim label wf:basicFlow
89
+ repos: [payments] # one or more registered repos, unique
90
+ cleanupRunnerOnEnd: false # optional; when true the runner tears down at end
63
91
 
64
- closeOn: [done] # terminal nodes whose tickets close their terminals
92
+ taskConfig: # optional; adapter-owned; merged root repo → workflow → node
93
+ transitions:
94
+ start: { parent: "In Progress" }
95
+ work: { mailbox: "In Progress" }
96
+ end: { parent: "Done" }
65
97
 
66
98
  nodes:
99
+ start:
100
+ onSuccess: [{ target: coding }]
101
+
67
102
  coding:
68
- agent: build # OpenCode agent for this node
69
- when: "In Progress" # tracker state routing tickets here (unique per file)
70
- onSuccess: reviewing # outcome edges required for agent nodes
71
- onFailure: coding # self-loop allowed → comment only, no transition
72
- nudgePrompt: "..." # optional; {{ticket}} {{node}} templates; sane default
103
+ type: agent # or hitl
104
+ agent: build # opencode agent
105
+ description: | # becomes the mailbox description and launch prompt
106
+ Implement the ticket.
107
+ onSuccess: [{ target: reviewing, when: "work complete" }]
108
+ onFailure: [{ target: coding, when: "retry" }]
109
+ nudgePrompt: "Check edge cases for {{ticket}} before reporting." # optional custom instructions
110
+
73
111
  reviewing:
74
- agent: build # the same agent may serve many nodes
75
- when: "In Review"
76
- onSuccess: done
77
- onFailure: coding
78
- done:
79
- when: "Done" # no agent → terminal / human-gate node
112
+ type: hitl
113
+ agent: build
114
+ description: Human review.
115
+ onSuccess: [{ target: end }]
116
+ onFailure: [{ target: coding }]
117
+
118
+ end: {}
80
119
  ```
81
120
 
82
- Validation is strict and happens at submit: unknown fields, duplicate `when` values, dangling edges, agent nodes missing edges, and every referenced tracker state is probe-validated against the tracker.
121
+ Rules enforced at submit:
83
122
 
84
- ### Run
123
+ - `start` and `end` are reserved lifecycle nodes. `start` has exactly one success target and no type/agent/description/routes on failure. `end` has no type/agent/description/routes.
124
+ - Every other node is `agent` or `hitl`, has an agent and a description, and declares at least one valid route for every permitted outcome.
125
+ - Routes are single-target; no route may target `start`.
126
+ - The graph must be fully reachable from `start`; unknown fields are rejected; `runnerPlugin`/`harnessPlugin`/`closeOn`/legacy `tasks`/`runner` keys are rejected.
127
+ - Only `agent` and `hitl` nodes receive mailbox subtasks; `start` and `end` never do.
128
+ - `cleanupRunnerOnEnd` is the only workflow cleanup knob and takes priority over terminal retention after `end`; the word `terminal` refers to runner terminals only.
85
129
 
86
- ```sh
87
- relay-flow serve # central process (artifacts in ~/.relay-flow/)
88
- relay-flow submit -f .workflow/workflow.yaml
89
- relay-flow stop serve
130
+ ### Task-config merge
131
+
132
+ `taskConfig` may appear at root, repo, workflow, and node scopes. The adapter merges in that order: maps merge recursively, later scalar/list replaces, omitted keys inherit, explicit YAML `null` is rejected. The merged values decode against one adapter-owned typed config at use time.
133
+
134
+ ### Jira transition defaults
135
+
136
+ Omitted transitions default to:
137
+
138
+ - `start`: parent → `In Progress`
139
+ - work node: mailbox → `In Progress` (parent unchanged)
140
+ - `end`: parent → `Done`
141
+
142
+ ---
143
+
144
+ ## Structured node report
145
+
146
+ Every visit (agent or HITL) ends with the same contract:
147
+
148
+ ```
149
+ STATUS: success | failure
150
+ NEXT STEP: <one configured route for that status>
151
+
152
+ SUMMARY
153
+ - Completed: ...
154
+ - Not completed: ... | None
155
+ - Issues discovered: ... | None
156
+ - Verification: ...
157
+ - Notes: ... | None
158
+
159
+ FEEDBACK
160
+ - Reason for next step: ...
161
+ - Required actions: ...
162
+ - Relevant context: ...
163
+ - Expected result: ...
90
164
  ```
91
165
 
92
- `relay-flow report` is invoked by the plugin, not by hand.
166
+ `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.
167
+
168
+ The plugin delivers the 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. Duplicate/stale reports are acked safely with no repeated graph effects. Invalid agent output is nudged; invalid HITL output stays silent.
93
169
 
94
170
  ---
95
171
 
96
172
  ## Architecture
97
173
 
98
- ### The board-game model
99
-
100
- - **Nodes** are squares. Each maps to exactly one tracker state via `when`. One node = one state; the same agent may serve many nodes.
101
- - **Agents** are robots sitting on squares. A ticket landing on an agent's square triggers work in a dedicated terminal/worktree.
102
- - **Edges** (`onSuccess`/`onFailure`) are the only legal moves. Self-loops comment without transitioning (trackers have no self-transitions).
103
- - **Terminal nodes** (in `closeOn`) tear down the ticket's terminals. **Agentless nodes** (no `agent:`) are human gates: the daemon claims the ticket (so other workflows skip it) but never spawns, nudges, or closes anything.
104
- - The **tracker is the single source of truth**. The server holds no database; restart = resubmit, and claim labels (`wf:<name>`) survive to drive recovery.
105
-
106
- ### End-to-end sequence
107
-
108
- ```mermaid
109
- sequenceDiagram
110
- participant J as Tracker (Jira)
111
- participant S as relay-flow serve
112
- participant R as Runner (Orca)
113
- participant O as OpenCode + plugin
114
-
115
- loop every pollIntervalSeconds
116
- S->>J: List() — one query (query + issuetype + component + assignee)
117
- J-->>S: tickets (Node via when-map, ClaimedBy via wf:* labels)
118
- end
119
- S->>J: Claim(ticket) — add label wf:xyzTaskFlow
120
- S->>R: Spawn(ticket, node, agent, env RELAY_*)
121
- R->>R: ensure worktree → terminal key:agent:node → opencode --prompt
122
- R->>O: agent session starts
123
- O->>O: works… ends reply with STATUS/SUMMARY
124
- O->>S: plugin: relay-flow report --workflow --ticket --node --outcome --summary
125
- S->>J: Report → transition to target node's state + comment
126
- S-->>O: {action: transitioned | commented | error}
127
- Note over O: action=error → plugin retries 3× → nudges the session
128
- ```
174
+ - **Task system** owns parent tickets, mailbox subtasks, task state, labels, comments, and adapter config. The parent ticket is the unit of work.
175
+ - **Durable workflow engine** (go-workflows + SQLite) owns graph progression, waits, reports, retries, and recovery. No custom state machine.
176
+ - **Mailbox subtask** is one agent/HITL node's scratch space; its description defines the node's work and its comments hold the node's summary plus selected incoming feedback.
177
+ - **Harness** owns agent launch, session/report behavior, parsing, nudging, and resume semantics.
178
+ - **Runner** owns ticket worktrees/environments, terminals, liveness, and execution of harness commands.
179
+ - **Compensation/rollback never exists.** Recovery always rolls forward through idempotent activities.
129
180
 
130
- ### The poll-cycle 3-way switch
131
-
132
- ```mermaid
133
- flowchart TD
134
- L[tasks.List] --> T{per ticket}
135
- T -->|ClaimedBy = other workflow| SKIP1[skip — mutex]
136
- T -->|Node unmapped| SKIP2[log + skip]
137
- T -->|node in closeOn| CLOSE[runner.Close — tear down terminals]
138
- T -->|node agentless| GATE[claim if unclaimed, then leave for the human]
139
- T -->|ClaimedBy = me, unknown in memory| BOUNCE[go bounce]
140
- T -->|unclaimed| DISP[go dispatch]
141
-
142
- DISP --> C1[tasks.Claim] --> S1[runner.Spawn fresh session]
143
- BOUNCE --> F{runner.Find by title}
144
- F -->|session alive| N1[Nudge once per node visit]
145
- F -->|gone| S2[Spawn fresh — claim already held]
146
- ```
181
+ ### Identity
147
182
 
148
- Claimed tickets are never touched by other workflows: the label is the cross-workflow mutex. "Claimed by me but unknown in memory" happens after a server restart — the bounce path re-finds the terminal by title (`<key>:<agent>:<node>`) and nudges it in place, preserving the agent's context instead of burning tokens on a fresh session.
149
-
150
- ### Components
151
-
152
- ```mermaid
153
- flowchart LR
154
- subgraph CLI[relay-flow CLI]
155
- M[cmd/relay-flow<br/>serve · submit · report · init]
156
- end
157
- subgraph SRV[server — one process, N workflows]
158
- H[/submit · /report · /shutdown/]
159
- D1[daemon: poll loop] --> T1[tasks iface]
160
- D1 --> R1[runner iface]
161
- end
162
- subgraph ADAPTERS[adapters — registry pattern]
163
- J[tasks/jira<br/>acli] -.-> T1
164
- O[runner/orca<br/>worktrees + terminals] -.-> R1
165
- end
166
- M -->|unix socket ~/.relay-flow/server.sock| H
167
- H --> D1
168
- ```
183
+ - `runID` is deterministic from `repo/workflow/ticket`.
184
+ - `nodeVisitID` is generated once per node entry as a durable replay-safe side effect; it changes on revisit and on fresh runs after `--recover`.
185
+ - Terminal titles are stable `<ticket>:<node>` — they never carry `nodeVisitID`, workflow, or agent.
186
+ - Report wire keys are `runId` / `node` / `reportId`; `nodeVisitID` stays internal.
169
187
 
170
- | Component | Location | Role |
171
- |---|---|---|
172
- | opencode plugin | `plugin/report-status.ts` | Parses STATUS/SUMMARY deterministically, calls `relay-flow report` (thin socket client), retries/nudges |
173
- | CLI | `cmd/relay-flow/` | `serve` hosts workflows; `submit` registers one; `report` is a one-shot client |
174
- | daemon | `internal/daemon/` | Poll loop, 3-way switch, dispatch/bounce goroutines |
175
- | server | `internal/server/` | Socket lifecycle, submit validation, report routing |
176
- | config | `internal/config/` | Workflow YAML schema + graph validation |
177
- | Jira adapter | `internal/tasks/jira/` | [readme](internal/tasks/jira/README.md) — query/claim/report over acli |
178
- | Orca adapter | `internal/runner/orca/` | [readme](internal/runner/orca/README.md) — worktrees, terminals, prompts |
188
+ ### Poll cycle
179
189
 
180
- ### Key invariants
190
+ One Repo Poller per registered repo (not per workflow) fetches active parent tickets on the configured `pollIntervalSeconds` (default 15). Per ticket, the Ticket Router resolves at most one workflow: multiple `wf:*` claims → `InvalidClaimError`; exactly one claim resolves directly; zero filter matches → `ErrNoMatch`; one match → that workflow; multiple matches → `AmbiguousError` with no mutation. Successful resolution goes to the Run Manager; the run is claimed with `wf:<name>` before `EnsureRun` fires.
181
191
 
182
- - **Fail fast**: everything validates at submit — YAML structure, graph shape, tracker states, assignee, adapters' configs.
183
- - **No fallback paths**: report goes through the server or not at all; server down = system down (the plugin's 3× retry + nudge covers transient gaps).
184
- - **Labels are never removed**: `wf:<name>` is the crash-recovery anchor.
185
- - **Terminals outlive their node visit** — a bounce reuses the session; only `closeOn` nodes close them.
186
- - **Parallelism**: one long-lived poll goroutine per workflow; short-lived dispatch/bounce goroutines per ticket; tickets at the same node run in parallel terminals with isolated worktrees.
192
+ ### Shutdown and recovery
187
193
 
188
- ### Extending
194
+ - Graceful shutdown stops accepting requests and new polls immediately, cancels worker polling, waits up to 30s for running calls, then closes the socket and database. Durable unfinished work resumes on the next normal start.
195
+ - Completed/canceled runs are removed after `completedRunRetentionDays` (default 30). The retention sweep runs once at startup, never on a ticker.
189
196
 
190
- New tracker (beads, Linear, GitHub): implement `tasks.Tasks` + register — see [tasks/jira README](internal/tasks/jira/README.md#writing-a-new-tasks-adapter-beads-linear-github-).
191
- New execution backend (tmux, …): implement `runner.Runner` + register — see [runner/orca README](internal/runner/orca/README.md#writing-a-new-runner-tmux-).
197
+ ---
192
198
 
193
199
  ## Development
194
200
 
195
201
  ```sh
196
- cd cli
197
- go test ./... -race
198
- go install ./...
202
+ go test ./...
203
+ cd plugin && bun test
199
204
  ```