@khanglvm/relay 0.2.0 → 0.3.0

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
@@ -5,23 +5,38 @@
5
5
  `rly` lets an AI agent (Claude Code, Codex, or anything that can run a CLI) ask
6
6
  its user structured questions in a clean browser page — single/multi choice,
7
7
  yes-no, free text, rating scales — and/or present rich content blocks (markdown,
8
- charts, diagrams, tables, code, custom HTML), then **block until the user clicks
8
+ charts, diagrams, tables, code, sandboxed HTML, Graphviz and PlantUML diagrams), then **block until the user clicks
9
9
  Submit** and read the answers as JSON. No more "type *done* in the terminal",
10
10
  no more hand-rolled HTML + throwaway servers.
11
11
 
12
12
  Users can **hover chart points, diagram nodes, table cells, or select text to
13
13
  leave inline comments** — returned alongside answers as `result.annotations`.
14
+ Agents can reply to those comments and reopen the board as a conversation thread.
14
15
 
15
16
  - Zero runtime dependencies — plain Node ≥ 18, vanilla HTML/CSS/JS UI
16
17
  - Light/dark theme (auto + manual toggle), responsive, content-focused
17
18
  - Real-time answer autosave (drafts survive timeout/cancel)
18
19
  - Auto-closes the tab after submit and unblocks the CLI
19
- - Native content blocks: markdown, mermaid diagrams, Chart.js charts, tables, code, sandboxed HTML
20
- - Chart.js and Mermaid are **vendored and lazy-loaded** only when a board uses them — the base board stays dependency-free and as fast as before
20
+ - Native content blocks: markdown, mermaid diagrams, Graphviz diagrams, PlantUML, Chart.js charts, tables, code, sandboxed HTML
21
+ - Chart.js, Mermaid, and Graphviz are **vendored and lazy-loaded** only when a board uses them — the base board stays dependency-free and as fast as before
21
22
  - Element-level annotations: users comment on chart points, diagram nodes, table cells, text, or custom HTML elements; returned as `result.annotations`
23
+ - **Threaded annotation replies**: agents can reply to user comments via `rly reopen --replies`; conversations shown inline in the board
24
+ - **Live board mutation**: `rly update` lets an agent push a new spec to an already-open board; the page reloads and prefills answers from draft
22
25
  - Multiple boards at once, local history: reuse / modify / reopen / remove
23
26
  - Agent-first: JSON on stdout, logs on stderr, `--detach` + `wait` for shell tools with execution time limits, built-in agent guide & skill
24
27
 
28
+ ## See it
29
+
30
+ | Light theme | Dark theme |
31
+ |---|---|
32
+ | ![relay board light theme](https://raw.githubusercontent.com/khanglvm/relay/main/docs/assets/board-light.png) | ![relay board dark theme](https://raw.githubusercontent.com/khanglvm/relay/main/docs/assets/board-dark.png) |
33
+
34
+ | Annotation popover | Mobile |
35
+ |---|---|
36
+ | ![Annotation comment on a chart bar](https://raw.githubusercontent.com/khanglvm/relay/main/docs/assets/annotations.png) | ![Mobile view at 375px](https://raw.githubusercontent.com/khanglvm/relay/main/docs/assets/mobile.png) |
37
+
38
+ ![relay demo — board to annotation to summary](https://raw.githubusercontent.com/khanglvm/relay/main/docs/assets/demo.gif)
39
+
25
40
  ## Install
26
41
 
27
42
  ```sh
@@ -198,14 +213,55 @@ on phones. Height: 100–2400 px, default 360. Fragments (no `<html>` tag) are
198
213
  auto-wrapped to match the current theme; full documents receive a
199
214
  `?theme=light|dark` query param.
200
215
 
216
+ ### Graphviz diagram
217
+
218
+ ```json
219
+ { "type": "graphviz", "dot": "digraph { a -> b -> c }", "height": 300 }
220
+ ```
221
+
222
+ Rendered entirely **offline** via the vendored `viz-standalone.js` (Graphviz compiled to
223
+ WebAssembly). Never upscaled past the diagram's natural width; shrinks on narrow screens;
224
+ container max-height 1200 px with scroll. Nodes (`g.node`) and edges (`g.edge`) are
225
+ individually annotatable. Use Graphviz for precise dependency graphs, call graphs, or
226
+ state machines where Mermaid's auto-layout doesn't give enough control.
227
+
228
+ ### PlantUML diagram
229
+
230
+ ```json
231
+ { "type": "plantuml", "code": "@startuml\nA -> B: request\n@enduml", "height": 340 }
232
+ ```
233
+
234
+ Rendered via a **PlantUML server** (default: `https://www.plantuml.com/plantuml`). The
235
+ diagram source is deflate-encoded client-side and sent as a URL parameter — no server
236
+ round-trip for the page itself, just an `<img>` request. If the server is unreachable or
237
+ the browser lacks `CompressionStream`, a muted error card is shown.
238
+
239
+ **Privacy note:** diagram source is encoded and sent to the configured PlantUML server.
240
+ For sensitive diagrams, host your own server and pass `"server": "https://plantuml.example.com"`.
241
+
242
+ The rendered image is registered as a single annotatable element (target kind `image`).
243
+ Use PlantUML for UML sequence diagrams, class diagrams, and component diagrams.
244
+
201
245
  ## Annotations
202
246
 
203
247
  Users can leave inline comments on any annotatable element — chart data points,
204
- mermaid nodes, table cells, text selections inside markdown, and labelled
248
+ mermaid nodes, graphviz nodes and edges, table cells, text selections inside markdown, and labelled
205
249
  elements inside custom HTML. A small pin icon appears on hover; clicking opens a
206
250
  comment popover. Comments are autosaved with the draft and returned in the final
207
251
  result.
208
252
 
253
+ **Threaded replies:** each annotation can have replies. Agents can read
254
+ `result.annotations`, compose replies, and reopen the board as a conversation:
255
+
256
+ ```sh
257
+ # After reading result.annotations from a previous board:
258
+ rly reopen <id> --replies replies.json
259
+ # replies.json: [{"annotationId":"a1","text":"Good catch — fixed in the next sprint."}]
260
+ ```
261
+
262
+ The board reopens with agent replies shown inline under each comment. Users can reply
263
+ back; the conversation grows with each `rly reopen --replies` cycle.
264
+
209
265
  ### result.annotations shape
210
266
 
211
267
  ```json
@@ -226,22 +282,31 @@ result.
226
282
  "value": 19
227
283
  },
228
284
  "text": "Feb spike was due to the onboarding push — not repeatable.",
229
- "createdAt": "2026-06-11T10:23:00.000Z"
285
+ "author": "user",
286
+ "createdAt": "2026-06-11T10:23:00.000Z",
287
+ "replies": [
288
+ { "author": "agent", "text": "Confirmed — excluded from the trend line.", "createdAt": "2026-06-11T11:00:00.000Z" }
289
+ ]
230
290
  }
231
291
  ],
232
292
  "durationMs": 58000
233
293
  }
234
294
  ```
235
295
 
296
+ Each annotation has an optional `author` (`"user"` | `"agent"`, default `"user"`) and
297
+ an optional `replies` array (capped at 50; each reply: `{author, text, createdAt}`).
298
+
236
299
  ### Annotation target kinds
237
300
 
238
301
  | kind | what the user clicked |
239
302
  |---|---|
240
303
  | `chart-element` | a bar, point, or pie slice — includes `datasetIndex`, `index`, `label`, `value` |
241
304
  | `mermaid-node` | a node in a diagram — includes `nodeId`, `text` |
305
+ | `graphviz-node` | a node or edge in a Graphviz diagram — includes `nodeId`, `text` |
242
306
  | `table-cell` | a cell — includes `row` (0-based), `col` (column key), `value` |
243
307
  | `text` | a text selection inside a markdown block — includes `quote`, `prefix`, `suffix` |
244
308
  | `html-element` | a labelled element inside custom HTML (via `kit.js`) — includes `label`, optional `detail` |
309
+ | `image` | a PlantUML diagram image — includes `label` |
245
310
 
246
311
  ### kit.js — annotatable custom HTML
247
312
 
@@ -272,6 +337,8 @@ opens the annotation popover in the parent page anchored to that element.
272
337
  | `rly list [--json]` | Running boards |
273
338
  | `rly open [id]` | Re-open the browser tab of a running board |
274
339
  | `rly reopen <id>` | Serve a saved board again, **prefilled with saved answers** |
340
+ | `rly reopen <id> --replies file.json` | Reopen with agent replies appended to matching annotations |
341
+ | `rly update <id> [--file spec.json \| --title T \| --intro I \| -q "…"]` | Push a new spec to a running board — page reloads, answers survive via draft |
275
342
  | `rly reuse <id> [--dump]` | Re-run a past board as a new one (blank) |
276
343
  | `rly stop <id> \| --all` | Stop running board(s) — draft preserved |
277
344
  | `rly history [--limit n] [--json]` | Saved boards |
package/docs/AGENT.md CHANGED
@@ -149,6 +149,16 @@ per question (`"blocks": [...]` on a question object). Heights clamp to
149
149
  // Mermaid diagram — vendored, lazy-loaded; natural height, max 1200 px + scroll
150
150
  { "type": "mermaid", "code": "graph TD; A-->B; B-->C", "height": 400 }
151
151
 
152
+ // Graphviz diagram — vendored viz-standalone.js (Graphviz-WASM), fully offline
153
+ // Nodes (g.node) and edges (g.edge) are individually annotatable
154
+ { "type": "graphviz", "dot": "digraph { a -> b -> c }", "height": 300 }
155
+
156
+ // PlantUML diagram — rendered via a PlantUML server (default: plantuml.com)
157
+ // Source is deflate-encoded client-side; only an img URL is sent to the server.
158
+ // Use "server" for a self-hosted instance to avoid leaking sensitive diagrams.
159
+ { "type": "plantuml", "code": "@startuml\nA -> B: request\n@enduml", "height": 340 }
160
+ { "type": "plantuml", "code": "...", "server": "https://plantuml.example.com", "height": 300 }
161
+
152
162
  // Chart — shorthand (lazy-loads vendored Chart.js; default height 320)
153
163
  {
154
164
  "type": "chart",
@@ -202,6 +212,8 @@ per question (`"blocks": [...]` on a question object). Heights clamp to
202
212
  | Block | Best for |
203
213
  |---|---|
204
214
  | `mermaid` | flows, state machines, architecture overviews, sequence diagrams |
215
+ | `graphviz` | precise dependency graphs, call graphs, state machines when Mermaid's auto-layout falls short; individually annotatable nodes and edges |
216
+ | `plantuml` | UML diagrams (sequence, class, component) via server rendering; great for detailed interface contracts |
205
217
  | `chart` | numbers, trends, comparisons, metrics |
206
218
  | `table` | structured comparisons, option matrices, data grids |
207
219
  | `markdown` | prose context, background, instructions, section headings |
@@ -262,27 +274,53 @@ intro. Annotations are autosaved with the draft and returned in the final result
262
274
  "id": "a1",
263
275
  "questionId": "q-id or null for board-level",
264
276
  "blockId": "b2",
265
- "target": { ... },
266
- "text": "user comment text",
267
- "createdAt": "2026-06-11T10:23:00.000Z"
277
+ "target": { "kind": "chart-element", "datasetIndex": 0, "index": 1, "label": "Feb", "value": 19 },
278
+ "text": "Feb spike was from the onboarding push — not repeatable.",
279
+ "author": "user",
280
+ "createdAt": "2026-06-11T10:23:00.000Z",
281
+ "replies": [
282
+ { "author": "agent", "text": "Confirmed — excluded from the trend line.", "createdAt": "2026-06-11T11:00:00.000Z" }
283
+ ]
268
284
  }
269
285
  ]
270
286
  ```
271
287
 
272
- ### All 5 target kinds
288
+ `author` is `"user"` (default, when absent) or `"agent"`. `replies` is an array of
289
+ `{author, text, createdAt}` objects, capped at 50 per annotation.
290
+
291
+ ### All target kinds
273
292
 
274
293
  | kind | Fields | Triggered by |
275
294
  |---|---|---|
276
295
  | `chart-element` | `datasetIndex`, `index`, `label`, `value` | clicking a bar, point, or pie slice |
277
296
  | `mermaid-node` | `nodeId`, `text` | clicking a diagram node |
297
+ | `graphviz-node` | `nodeId`, `text` | clicking a Graphviz node or edge |
278
298
  | `table-cell` | `row` (0-based), `col` (column key), `value` | clicking a table cell |
279
299
  | `text` | `quote`, `prefix` (≤30 chars before), `suffix` (≤30 after) | selecting text in a markdown block |
280
300
  | `html-element` | `label`, `detail?` | clicking a `relayKit.commentable()` element |
301
+ | `image` | `label` | clicking the PlantUML diagram image |
281
302
 
282
303
  Read annotations as first-class feedback — they often carry the sharpest insight
283
304
  (e.g. a user circling the one data point that concerns them, or quoting the exact
284
305
  sentence they disagree with).
285
306
 
307
+ ### Threaded replies — `rly reopen --replies`
308
+
309
+ After reading `result.annotations`, an agent can reply to specific comments and
310
+ reopen the board as a conversation:
311
+
312
+ ```sh
313
+ rly reopen <id> --replies replies.json
314
+ ```
315
+
316
+ `replies.json` is an array of `{"annotationId": "a1", "text": "..."}` objects.
317
+ The server seeds the draft from the saved result, appends each reply with
318
+ `author: "agent"` and `createdAt: now`, then serves the board prefilled. Unknown
319
+ annotation IDs cause a `CliError` (exit 4) listing valid IDs.
320
+
321
+ The UI shows agent and user replies as a thread under each comment — agent replies
322
+ use an accent chip, user replies use a muted chip.
323
+
286
324
  ## Managing boards
287
325
 
288
326
  ```sh
@@ -290,6 +328,8 @@ rly list [--json] # running boards (id, url, pid)
290
328
  rly open [id] # re-open the browser tab of a running board
291
329
  rly reopen <id> # serve a SAVED board again, prefilled with its saved
292
330
  # answers/draft; user can edit and resubmit
331
+ rly reopen <id> --replies replies.json
332
+ # reopen with agent replies (see Threaded replies above)
293
333
  rly reuse <id> # re-run a past board as a NEW board (blank answers)
294
334
  rly spec <id> # print a saved spec — edit it, then `rly ask --file`
295
335
  rly history [--json] # saved boards with statuses
@@ -300,6 +340,26 @@ rly rm <id> | --all # delete saved board(s)
300
340
  Multiple boards can run at once (each gets its own port on 127.0.0.1).
301
341
  Storage lives in `~/.relay` (override with `RLY_HOME`).
302
342
 
343
+ ## Live board mutation — `rly update`
344
+
345
+ Push a new spec to an already-open board without stopping it:
346
+
347
+ ```sh
348
+ rly update <boardId> --file new-spec.json # replace the full spec
349
+ rly update <boardId> --title "New title" # patch just the title
350
+ rly update <boardId> --intro "New intro" # patch just the intro
351
+ rly update <boardId> -q "!Priority::single::p0,p1,p2" # append a question
352
+ ```
353
+
354
+ The page **reloads** for the user and prefills their previous answers from the
355
+ autosaved draft — answers for question IDs that no longer exist are silently
356
+ ignored. A small toast "Board updated by the agent" appears for 4 seconds.
357
+
358
+ Stdout: `{"status":"updated","boardId":"…","rev":2,"url":"…"}`.
359
+
360
+ **Caution:** the page reloads for the user. Batch your changes into one `rly update`
361
+ call rather than calling it repeatedly in a loop.
362
+
303
363
  ## Tips for agents
304
364
 
305
365
  - Prefer `--detach` + `rly wait` if your shell tool kills long commands.
@@ -313,5 +373,11 @@ Storage lives in `~/.relay` (override with `RLY_HOME`).
313
373
  leave inline comments — they won't discover it otherwise.
314
374
  - Check `result.annotations` before generating your next output; a comment on a
315
375
  specific data point or a quoted sentence often overrides the checkbox answer.
376
+ - Use `rly reopen <id> --replies replies.json` to answer the user's element
377
+ comments and reopen the board as a conversation thread.
378
+ - Use `rly update <id>` to push spec changes to a running board — the page
379
+ reloads and answers survive via draft autosave. Batch updates; do not spam.
380
+ - For sensitive PlantUML diagrams, set `"server": "https://your-server"` to avoid
381
+ sending source to the public plantuml.com server.
316
382
  - Bundled universal skill (Claude Code, Codex, any SKILL.md-aware agent):
317
383
  `rly skill install` — or `npx skills add khanglvm/relay`.
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanglvm/relay",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Browser-based question boards with rich blocks (markdown, charts, mermaid, tables, code, sandboxed HTML) and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, wait for submit, read answers as JSON.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: relay
3
- description: Ask the user interactive questions in a browser board (single/multi choice, yes-no, free text, scale) and/or present rich content blocks (markdown, mermaid diagrams, charts, interactive tables, code, sandboxed HTML), then wait for Submit and read JSON answers plus element-level annotations. PROACTIVELY use whenever you would otherwise (a) call a native ask-user/question tool with 2+ questions or options that need explanation, (b) describe a UI/design/plan/architecture in prose that a visual would show better — draft diagrams (mermaid), charts, and interactive tables as native blocks, or (c) hand-roll an HTML file or local server to demo an idea - relay replaces all three. Triggers - clarify requirements before ambiguous work, choose between approaches, plan approval, design/UX feedback, mockup or prototype review, compare alternatives, survey, metrics review, "ask the user", "show the user", "which do you prefer", "get feedback", diagram, chart, data table, architecture overview, metrics. Skip only for a single trivial yes/no confirmation.
3
+ description: Ask the user interactive questions in a browser board (single/multi choice, yes-no, free text, scale) and/or present rich content blocks (markdown, mermaid diagrams, graphviz, plantuml, uml, charts, interactive tables, code, sandboxed HTML), then wait for Submit and read JSON answers plus element-level annotations. PROACTIVELY use whenever you would otherwise (a) call a native ask-user/question tool with 2+ questions or options that need explanation, (b) describe a UI/design/plan/architecture in prose that a visual would show better — draft diagrams (mermaid, graphviz, plantuml), charts, and interactive tables as native blocks, or (c) hand-roll an HTML file or local server to demo an idea - relay replaces all three. Triggers - clarify requirements before ambiguous work, choose between approaches, plan approval, design/UX feedback, mockup or prototype review, compare alternatives, survey, metrics review, "ask the user", "show the user", "which do you prefer", "get feedback", diagram, chart, data table, architecture overview, metrics, dependency graph, sequence diagram, class diagram, uml, graphviz, plantuml. Skip only for a single trivial yes/no confirmation.
4
4
  ---
5
5
 
6
6
  # relay (`rly`)
@@ -35,20 +35,21 @@ questions into ONE board rather than opening several in a row.
35
35
 
36
36
  ## Choose a pattern
37
37
 
38
- 1. **Blocking** simple, but your shell tool must tolerate waiting:
38
+ **DEFAULT: detached.** Most agent shell tools kill long-running commands, and a
39
+ blocking `rly ask` that gets killed cancels the board. Always prefer:
39
40
 
40
- ```sh
41
- rly ask --file spec.json --timeout 1800 # prints result JSON when user submits
42
- ```
41
+ ```sh
42
+ rly ask --file spec.json --detach # returns {"boardId":"b-…","url":…} immediately
43
+ rly wait b-xxxxx --timeout 550 # blocks until submit, prints result JSON
44
+ # (on exit 2 "wait-timeout" just run wait again)
45
+ rly result b-xxxxx # non-blocking peek (includes live draft)
46
+ ```
43
47
 
44
- 2. **Detached** use when your shell tool has an execution time limit
45
- (e.g. run it, then wait in a separate call):
48
+ Blocking mode (`rly ask --file spec.json --timeout 1800`, no --detach) is fine
49
+ ONLY when your shell tool has no execution time limit.
46
50
 
47
- ```sh
48
- rly ask --file spec.json --detach # {"boardId":"b-…","url":…} immediately
49
- rly wait b-xxxxx --timeout 3500 # blocks until submit, prints result JSON
50
- rly result b-xxxxx # non-blocking peek (includes live draft)
51
- ```
51
+ Useful flags: `--no-open` (don't auto-open the browser — for tests/CI; real
52
+ users need the tab, so omit it normally) · `--title` · `--timeout <sec>`.
52
53
 
53
54
  Exit codes: 0 submitted · 2 timeout · 3 cancelled · 5 not found. On
54
55
  timeout/cancel the result still contains the autosaved `draft` of partial
@@ -66,7 +67,8 @@ answers and any annotations written so far.
66
67
  { "id": "parts", "type": "multi", "label": "Include?", "options": ["api", "ui"], "note": true },
67
68
  { "id": "ship", "type": "yesno", "label": "Ship now?" },
68
69
  { "id": "why", "type": "textarea", "label": "Reasoning?" },
69
- { "id": "conf", "type": "scale", "label": "Confidence", "min": 1, "max": 5 }
70
+ { "id": "conf", "type": "scale", "label": "Confidence", "min": 1, "max": 5,
71
+ "minLabel": "low", "maxLabel": "high" }
70
72
  ]
71
73
  }
72
74
  ```
@@ -92,6 +94,9 @@ Add `"blocks": [...]` to the root or to any question.
92
94
  ```jsonc
93
95
  { "type": "markdown", "md": "## Section\n**prose**" }
94
96
  { "type": "mermaid", "code": "graph TD; A-->B", "height": 400 }
97
+ { "type": "graphviz", "dot": "digraph { a -> b }", "height": 300 }
98
+ { "type": "plantuml", "code": "@startuml\nA->B\n@enduml", "height": 300 }
99
+ { "type": "plantuml", "code": "...", "server": "https://plantuml.example.com" }
95
100
  { "type": "chart", "kind": "bar", "title": "...",
96
101
  "labels": ["Jan","Feb"], "series": [{"label":"x","data":[1,2]}], "height": 320 }
97
102
  { "type": "chart", "config": { /* full Chart.js v4 config */ }, "height": 300 }
@@ -101,14 +106,15 @@ Add `"blocks": [...]` to the root or to any question.
101
106
  { "type": "html", "htmlFile": "viz.html", "height": 400 }
102
107
  ```
103
108
 
104
- Chart.js and Mermaid are **vendored and lazy-loaded** — the base board stays
105
- dependency-free. Legacy `"html"` / `"htmlFile"` / `"htmlHeight"` on root or
106
- questions are still accepted and normalised automatically.
109
+ Chart.js, Mermaid, and Graphviz are **vendored and lazy-loaded** — the base board
110
+ stays dependency-free. PlantUML uses the public plantuml.com server by default;
111
+ pass `"server"` for a self-hosted instance. Legacy `"html"` / `"htmlFile"` /
112
+ `"htmlHeight"` on root or questions are still accepted and normalised automatically.
107
113
 
108
114
  ## Annotations
109
115
 
110
- Users can hover chart points, diagram nodes, table cells, or select text in
111
- markdown to leave inline comments. Always mention this in the board intro.
116
+ Users can hover chart points, diagram nodes (mermaid + graphviz), table cells, or
117
+ select text in markdown to leave inline comments. Always mention this in the board intro.
112
118
 
113
119
  `result.annotations` is an array of:
114
120
 
@@ -117,18 +123,32 @@ markdown to leave inline comments. Always mention this in the board intro.
117
123
  "id": "a1",
118
124
  "questionId": "q-id or null",
119
125
  "blockId": "b2",
120
- "target": {
121
- "kind": "chart-element | mermaid-node | table-cell | text | html-element",
122
- "..."
123
- },
126
+ "target": { "kind": "chart-element | mermaid-node | graphviz-node | table-cell | text | html-element | image", "..." },
124
127
  "text": "user comment",
125
- "createdAt": "ISO"
128
+ "author": "user",
129
+ "createdAt": "ISO",
130
+ "replies": [{ "author": "agent", "text": "acknowledged", "createdAt": "ISO" }]
126
131
  }
127
132
  ```
128
133
 
129
134
  Read annotations before generating your next output — a comment on a specific
130
135
  data point often carries sharper signal than a checkbox answer.
131
136
 
137
+ ### Reply to annotations (agent → user conversation)
138
+
139
+ ```sh
140
+ # 1. Read result from a previous board
141
+ rly result <id> # or rly wait <id>
142
+
143
+ # 2. Build replies file
144
+ # replies.json: [{"annotationId":"a1","text":"Good catch — fixed."}]
145
+
146
+ # 3. Reopen as a conversation
147
+ rly reopen <id> --replies replies.json
148
+ ```
149
+
150
+ Unknown annotation IDs cause an error listing valid IDs (exit 4).
151
+
132
152
  ## Recipes
133
153
 
134
154
  **Plan approval** — board-level `markdown` block rendering the plan, one `yesno`
@@ -152,5 +172,19 @@ and table cells to comment on specific values.
152
172
 
153
173
  `rly history` (saved boards) · `rly spec <id>` (print spec to modify) ·
154
174
  `rly reuse <id>` (re-run blank) · `rly reopen <id>` (re-open with saved
155
- answers prefilled) · `rly list` / `rly open` / `rly stop <id>` · `rly rm <id>`.
175
+ answers prefilled) · `rly reopen <id> --replies file.json` (add agent replies) ·
176
+ `rly list` / `rly open` / `rly stop <id>` · `rly rm <id>`.
156
177
  Multiple boards can run concurrently.
178
+
179
+ ### Live mutation — `rly update`
180
+
181
+ Push a new spec to a running board. The page reloads and prefills answers from the
182
+ autosaved draft — answers survive, the user sees a toast "Board updated by the agent".
183
+
184
+ ```sh
185
+ rly update <boardId> --file new-spec.json # replace spec
186
+ rly update <boardId> --title T --intro I # patch fields
187
+ rly update <boardId> -q "!Priority::single::p0,p1" # append question
188
+ ```
189
+
190
+ Batch your changes into one call — the page reloads for the user on each update.
@@ -26,6 +26,15 @@
26
26
  "code": "graph LR\n Client-->|HTTPS|Gateway\n Gateway-->AuthService\n Gateway-->APIService\n APIService-->DB[(Postgres)]\n APIService-->Cache[(Redis)]\n APIService-->Queue[[Job queue]]\n Queue-->Worker\n Worker-->DB",
27
27
  "height": 300
28
28
  },
29
+ {
30
+ "type": "markdown",
31
+ "md": "## Service dependency graph\nThe Graphviz diagram below shows precise call-time dependencies between services. Click any node or edge to leave a comment."
32
+ },
33
+ {
34
+ "type": "graphviz",
35
+ "dot": "digraph services {\n rankdir=LR\n node [shape=box style=filled fontname=Helvetica fontsize=11]\n Gateway [fillcolor=\"#c8e6c9\"]\n APIService [fillcolor=\"#fff9c4\"]\n AuthService[fillcolor=\"#c8e6c9\"]\n DB [shape=cylinder fillcolor=\"#bbdefb\"]\n Cache [shape=cylinder fillcolor=\"#bbdefb\"]\n Worker [fillcolor=\"#fff9c4\"]\n Gateway -> APIService\n Gateway -> AuthService\n APIService -> DB\n APIService -> Cache\n APIService -> Worker [style=dashed label=\"async\"]\n Worker -> DB\n}",
36
+ "height": 260
37
+ },
29
38
  {
30
39
  "type": "markdown",
31
40
  "md": "## Service health snapshot\nCurrent status as of the last deployment. Click any cell to comment."
package/src/cli.js CHANGED
@@ -8,6 +8,7 @@ import { normalizeSpec, questionFromInline, SPEC_SCHEMA } from './spec.js';
8
8
  import {
9
9
  createBoard,
10
10
  loadBoard,
11
+ saveBoard,
11
12
  deleteBoard,
12
13
  listBoards,
13
14
  listRunning,
@@ -26,7 +27,7 @@ const VERSION = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'),
26
27
 
27
28
  const VALUED_FLAGS = new Set([
28
29
  'file', 'html', 'html-file', 'title', 'intro', 'timeout', 'port',
29
- 'submit-label', 'height', 'limit', 'target', 'id',
30
+ 'submit-label', 'height', 'limit', 'target', 'id', 'replies',
30
31
  ]);
31
32
 
32
33
  function camel(key) {
@@ -183,8 +184,58 @@ async function cmdAsk(args, mode) {
183
184
  return runOrDetach(record, args);
184
185
  }
185
186
 
187
+ // Seeds the draft from the last result (as runBoard would on reopen) and
188
+ // appends agent replies to the matching annotations, so an agent can ANSWER
189
+ // the user's element comments and re-open the board as a conversation.
190
+ // Persists the record with the result archived so runBoard doesn't re-seed.
191
+ function seedAgentReplies(record, replies) {
192
+ if (!Array.isArray(replies)) throw new CliError('--replies file must be a JSON array of {annotationId, text}.', 4);
193
+ // Mirror runBoard's reopen draft-seeding from the prior result.
194
+ if (record.result) {
195
+ if (record.result.answers) {
196
+ record.draft = {
197
+ answers: record.result.answers,
198
+ comment: record.result.comment || '',
199
+ notes: record.result.notes || {},
200
+ annotations: record.result.annotations || [],
201
+ updatedAt: new Date().toISOString(),
202
+ };
203
+ }
204
+ record.pastResults = [...(record.pastResults || []), record.result].slice(-10);
205
+ record.result = null;
206
+ }
207
+ const annotations = (record.draft && Array.isArray(record.draft.annotations)) ? record.draft.annotations : [];
208
+ const validIds = annotations.map((a) => a && a.id).filter(Boolean);
209
+ const now = new Date().toISOString();
210
+ replies.forEach((r, i) => {
211
+ if (r === null || typeof r !== 'object' || Array.isArray(r)) {
212
+ throw new CliError(`--replies[${i}]: must be an object {annotationId, text}.`, 4);
213
+ }
214
+ const annotationId = typeof r.annotationId === 'string' ? r.annotationId : '';
215
+ const text = typeof r.text === 'string' ? r.text : '';
216
+ if (!annotationId) throw new CliError(`--replies[${i}]: missing "annotationId".`, 4);
217
+ if (!text.trim()) throw new CliError(`--replies[${i}]: missing "text".`, 4);
218
+ const ann = annotations.find((a) => a && a.id === annotationId);
219
+ if (!ann) {
220
+ throw new CliError(
221
+ `--replies[${i}]: unknown annotationId "${annotationId}". Valid ids: ${validIds.length ? validIds.join(', ') : '(none)'}.`,
222
+ 4
223
+ );
224
+ }
225
+ if (!Array.isArray(ann.replies)) ann.replies = [];
226
+ ann.replies.push({ author: 'agent', text, createdAt: now });
227
+ });
228
+ if (!record.draft) record.draft = { answers: {}, comment: '', notes: {}, annotations, updatedAt: now };
229
+ else record.draft.annotations = annotations;
230
+ saveBoard(record);
231
+ }
232
+
186
233
  async function cmdReopen(args) {
187
234
  const record = mustLoad(args._[0]);
235
+ if (args.replies !== undefined) {
236
+ const replies = parseJson(readFileOrThrow(args.replies), args.replies);
237
+ seedAgentReplies(record, replies);
238
+ }
188
239
  const running = loadRunning(record.id);
189
240
  if (running && isAlive(running.pid)) {
190
241
  openUrl(running.url);
@@ -204,6 +255,64 @@ async function cmdReuse(args) {
204
255
  return runOrDetach(record, args);
205
256
  }
206
257
 
258
+ // Live-mutates a running board: rebuild the spec (full replace via --file, or
259
+ // patch the current spec via --title/--intro/-q), then POST it (already
260
+ // normalized) to the board's /api/update with the per-board mutation token.
261
+ async function cmdUpdate(args) {
262
+ const id = args._[0];
263
+ if (!id) throw new CliError('usage: rly update <board-id> (--file new-spec.json | --title T | --intro I | -q "...")');
264
+ const record = loadBoard(id);
265
+ if (!record) throw new CliError(`board "${id}" not found. See \`rly history\`.`, 5);
266
+ const running = loadRunning(id);
267
+ if (!running || !isAlive(running.pid)) {
268
+ throw new CliError(`board "${id}" is not running — \`rly reopen ${id}\` to serve it, then update.`, 5);
269
+ }
270
+
271
+ let spec;
272
+ if (args.file) {
273
+ const raw =
274
+ args.file === '-'
275
+ ? parseJson(await readStdin(), 'stdin')
276
+ : parseJson(readFileOrThrow(args.file), args.file);
277
+ spec = normalizeSpec(raw);
278
+ } else if (args.title || args.intro || args.q.length) {
279
+ // Patch the CURRENT spec, then re-normalize so it's a clean normalized spec.
280
+ const raw = { ...record.spec };
281
+ if (args.title) raw.title = args.title;
282
+ if (args.intro) raw.intro = args.intro;
283
+ if (args.q.length) {
284
+ raw.questions = [...(raw.questions || []), ...args.q.map((s, i) => questionFromInline(s, i))];
285
+ }
286
+ spec = normalizeSpec(raw);
287
+ } else {
288
+ throw new CliError('update needs --file <spec.json>, --title, --intro, or -q "...".', 4);
289
+ }
290
+
291
+ let res;
292
+ try {
293
+ res = await fetch(new URL('/api/update', running.url), {
294
+ method: 'POST',
295
+ headers: { 'content-type': 'application/json', 'x-relay-token': running.token || '' },
296
+ body: JSON.stringify({ spec }),
297
+ });
298
+ } catch (err) {
299
+ throw new CliError(`could not reach board "${id}" at ${running.url}: ${String((err && err.message) || err)}`, 5);
300
+ }
301
+ if (res.status === 403) throw new CliError(`board "${id}" rejected the update token (stale running file?).`, 5);
302
+ if (!res.ok) {
303
+ let detail = '';
304
+ try {
305
+ detail = (await res.json()).error || '';
306
+ } catch {
307
+ // non-JSON body
308
+ }
309
+ throw new CliError(`board "${id}" rejected the update${detail ? `: ${detail}` : ''}.`, 4);
310
+ }
311
+ const body = await res.json();
312
+ printJson({ status: 'updated', boardId: id, rev: body.rev, url: running.url });
313
+ return 0;
314
+ }
315
+
207
316
  async function cmdWait(args) {
208
317
  const id = args._[0];
209
318
  if (!id) throw new CliError('usage: rly wait <board-id> [--timeout <sec>]');
@@ -539,8 +648,10 @@ USAGE
539
648
  rly result <id> result/status now (includes live autosaved draft while open)
540
649
  rly list [--json] running boards
541
650
  rly open [id] re-open the browser tab of a running board
542
- rly reopen <id> serve a saved board again, prefilled with its saved answers
651
+ rly reopen <id> [--replies f.json] serve a saved board again, prefilled with saved answers
652
+ (--replies [{annotationId,text}] = agent answers to element comments)
543
653
  rly reuse <id> [--dump] re-run a past board as a new board (--dump prints its spec)
654
+ rly update <id> --file spec.json live-mutate a RUNNING board (or --title/--intro/-q); page reloads
544
655
  rly stop <id> | --all stop running board(s) (status: cancelled, draft preserved)
545
656
  rly history [--limit n] [--json] saved boards
546
657
  rly spec <id> print a saved board's spec JSON (edit, then ask --file again)
@@ -590,6 +701,8 @@ export async function main(argv) {
590
701
  return await cmdReopen(parseArgs(rest));
591
702
  case 'reuse':
592
703
  return await cmdReuse(parseArgs(rest));
704
+ case 'update':
705
+ return await cmdUpdate(parseArgs(rest));
593
706
  case 'wait':
594
707
  return await cmdWait(parseArgs(rest));
595
708
  case 'result':