@sjawhar/opencode-legion-envoy 8.16.0 → 8.17.1
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/dist/dispatch.js +10 -0
- package/dist/src/server.js +10 -0
- package/package.json +1 -1
- package/skills/AGENTS.md +1 -1
- package/skills/dispatch/SKILL.md +107 -409
- package/skills/dispatch/references/api.md +16 -48
- package/skills/dispatch/references/asks.md +19 -78
- package/skills/dispatch/references/document-edits.md +38 -187
- package/skills/dispatch/references/documents.md +32 -224
- package/skills/dispatch/references/examples.md +10 -27
- package/skills/dispatch/references/issues.md +34 -196
- package/skills/dispatch/references/messages.md +13 -58
- package/skills/dispatch/references/reading.md +10 -45
- package/skills/dispatch-brainstorming/SKILL.md +34 -99
- package/skills/dispatch-first/SKILL.md +16 -34
- package/skills/legion-architect/SKILL.md +78 -52
- package/skills/legion-controller/SKILL.md +3 -3
- package/skills/legion-retro/SKILL.md +14 -11
- package/skills/legion-worker/SKILL.md +11 -11
- package/skills/legion-worker/references/merge-gate.md +74 -28
- package/skills/legion-worker/references/pr-body.md +2 -2
|
@@ -1,53 +1,21 @@
|
|
|
1
1
|
# Authentication and the HTTP API
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
commands do not cover.
|
|
3
|
+
Read this to set up a Dispatch token, or to call a route no `dispatch` command covers.
|
|
5
4
|
|
|
6
5
|
## Agent authentication
|
|
7
6
|
|
|
8
|
-
Use a personal
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
records the minting human as the owner of that session's writes.
|
|
24
|
-
`DISPATCH_AGENT_TOKEN` is the shared devbox fallback; do not configure it for an individual agent.
|
|
25
|
-
|
|
26
|
-
The deployed Dispatch server's browser origin is configured separately with
|
|
27
|
-
`DISPATCH_SERVER_URL` in the deployment `compose/.env`. Do not change an
|
|
28
|
-
agent's `envoy.json` to set the sign-in callback origin: the value must be the
|
|
29
|
-
exact URL humans type in their browser, and `<DISPATCH_SERVER_URL>/auth/callback`
|
|
30
|
-
is the callback registered on Dispatch's sign-in client (`DISPATCH_SIGNIN_CLIENT_ID`):
|
|
31
|
-
the sign-in pool's app client, or the Google web client.
|
|
32
|
-
|
|
33
|
-
### Finding a route
|
|
34
|
-
|
|
35
|
-
The `dispatch` commands cover the everyday surface. For anything else, ask the server: `GET /api/v1` (no
|
|
36
|
-
credential) returns every route as `{method, path, auth, description}` sorted by path — `auth`
|
|
37
|
-
is `public`, `any` (a human or a bearer), `human` (a bearer gets `403 HUMAN_ONLY`), or `bearer`.
|
|
38
|
-
A path Dispatch does not serve under `/api` or `/v1` answers
|
|
39
|
-
`404 {"code":"NOT_FOUND","error":"no route for GET /v1/issues","hint":"GET /api/v1 lists every
|
|
40
|
-
route"}`; when you see that, you typed the path wrong — read the index rather than guessing. Every
|
|
41
|
-
`/api/v1` error body carries a `code`; branch on the code, never on the text.
|
|
42
|
-
|
|
43
|
-
`GET /api/v1/issues` answers every matching issue as an array, or one page when you name `limit`
|
|
44
|
-
(1–250) or `offset` (alone it pages 50): `{issues, total, limit, offset}`, where `total` counts
|
|
45
|
-
every issue your filters match. Walking the pages with the next `offset` is exact only while the
|
|
46
|
-
listing does not change: an issue that enters or leaves what the filters match, or whose status or
|
|
47
|
-
rank changes, between two reads shifts rows across a page boundary, and one issue comes back twice
|
|
48
|
-
and another never, even when you stop at `total`. The unpaged array is the only exact set one read
|
|
49
|
-
gives. `cursor` is `400 INVALID_QUERY`. `dispatch issues` pages for you.
|
|
50
|
-
The event logs page with `after` or `before` and `limit`, and `GET /api/v1/search` takes a `limit`
|
|
51
|
-
of at most 50 and has no next page. Every other route answers without paging and ignores a paging
|
|
52
|
-
parameter.
|
|
53
|
-
|
|
7
|
+
- Use a personal token. A human mints it in Dispatch **Settings → Agent tokens**. Supply it as `DISPATCH_TOKEN` or as `dispatch.token` in the session's `envoy.json`. The server records that human as the owner of your writes.
|
|
8
|
+
- Which `envoy.json`: the session's own directory under an Oh My Pi profile other than `default`, a Claude Code `CLAUDE_CONFIG_DIR` or an OpenCode `OPENCODE_CONFIG_DIR`. Otherwise the file `ENVOY_CONFIG_FILE` names, else `~/.config/legion/envoy.json` (under `$XDG_CONFIG_HOME` when set).
|
|
9
|
+
- The plugin passes its choice in `ENVOY_CONFIG_FILE` with `DISPATCH_HOST`. In a Claude Code Bash that lacks both, `CLAUDE_CODE_CHILD_SESSION=1` makes the command read `CLAUDE_CONFIG_DIR`'s file. A Legion pane reads the `DISPATCH_TOKEN_FILE` path the daemon sets.
|
|
10
|
+
- A `dispatch.token` written `!<command>` is a command that prints the token.
|
|
11
|
+
- A checkout's `.envoy/envoy.json` sets `dispatch.serverUrl` and `dispatch.token` only together. Its `envoy` section is not read. A file's token goes only to its own server: a `DISPATCH_URL` for another server needs `DISPATCH_TOKEN` or `DISPATCH_TOKEN_FILE`.
|
|
12
|
+
- Never configure `DISPATCH_AGENT_TOKEN` (the shared devbox fallback) for one agent.
|
|
13
|
+
- Never set the sign-in callback origin in an agent's `envoy.json`. It is `DISPATCH_SERVER_URL` in the deployment `compose/.env`: the exact URL humans type. `<DISPATCH_SERVER_URL>/auth/callback` is registered on the sign-in client (`DISPATCH_SIGNIN_CLIENT_ID`).
|
|
14
|
+
|
|
15
|
+
## Finding a route
|
|
16
|
+
|
|
17
|
+
- `GET /api/v1` (no credential) lists every route as `{method, path, auth, description}`. `auth` is `public`, `any`, `human` (a bearer gets `403 HUMAN_ONLY`) or `bearer`.
|
|
18
|
+
- `404 NOT_FOUND` with "GET /api/v1 lists every route" means a wrong path. Read the index, do not guess.
|
|
19
|
+
- Branch on an error's `code`, never its text.
|
|
20
|
+
- `GET /api/v1/issues` returns every match unpaged, or one page with `limit` (1–250) or `offset`: `{issues, total, limit, offset}`. Only the unpaged array is exact: a page walk can repeat or skip an issue that changes. `cursor` is `400 INVALID_QUERY`. `dispatch issues` pages for you.
|
|
21
|
+
- Event logs page with `after` or `before` and `limit`. `GET /api/v1/search` takes `limit` up to 50 with no next page. Other routes do not page.
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
# Asks after they open: who answers, editing, resolving, replying, and following
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
to-do, edit or retract an ask, answer a clarification, say whose turn it is, or follow or leave its
|
|
5
|
-
thread. Whether to ask at all, and how to write the question, is "Before you ask" in
|
|
6
|
-
`skill://dispatch`.
|
|
3
|
+
Read this once an ask is open. Whether to ask, and how to write it, is "Before you ask" in `skill://dispatch`.
|
|
7
4
|
|
|
8
5
|
## Who answers an ask
|
|
9
6
|
|
|
10
|
-
An ask goes to the issue's assignee
|
|
11
|
-
|
|
7
|
+
- An ask goes to the issue's assignee's Inbox. On an unassigned issue it is in nobody's default Inbox or `Needs you` count.
|
|
8
|
+
- `dispatch whoami` prints whom you act for: your token owner's email, or no owner (a service token shows as `<namespace>/<name>`, as your writes do). An issue you create goes to your owner, else inherits its parent's assignee, else stays unassigned.
|
|
9
|
+
- On an unassigned issue where the answer matters, assign it to your owner, or name in the question who should answer. Assign with `PATCH /api/v1/issues/{key}` and `{"assignee": "<email>"}`. An email nobody signed in with gets `ASSIGNEE_NOT_ALLOWED`.
|
|
10
|
+
- Never reassign an issue a human holds to get a faster answer. That is the human's call.
|
|
12
11
|
|
|
13
12
|
## Handing a human a to-do, editing, resolving, and replying
|
|
14
13
|
|
|
15
|
-
A to-do
|
|
16
|
-
|
|
17
|
-
answer. There is no fixed vocabulary and the server treats no label specially.
|
|
18
|
-
If an outcome needs a reason, say so in that option's description, and the human's free-text answer
|
|
19
|
-
carries it:
|
|
14
|
+
- A to-do follows the question-and-options contract of "Writing for the human". For a human-only action, what it changes and risks constrains the answer. No label is special. If an outcome needs a reason, say so in its description.
|
|
15
|
+
|
|
20
16
|
```bash
|
|
21
17
|
dispatch ask --issue DSP-42 \
|
|
22
18
|
--question 'The fix is merged but cannot run in production: the signing certificate it needs expired, and only its owner can renew it in the registrar console. Renewing replaces the certificate every client checks. How should we proceed? Recommendation: renew it now because the fix cannot run without it.' \
|
|
@@ -24,78 +20,23 @@ dispatch ask --issue DSP-42 \
|
|
|
24
20
|
--option 'Leave it expired: Changes nothing now but leaves the fix unable to run.'
|
|
25
21
|
```
|
|
26
22
|
|
|
27
|
-
Correct
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
|
|
32
|
-
A human can change an answer they gave. The change arrives as another `ask.answered` frame whose
|
|
33
|
-
`previous_answer` names the answer it replaced, through the same route the first answer took, and
|
|
34
|
-
`dispatch read --ref dispatch://KEY/ask/<id>` lists every earlier answer. Act on the newest answer.
|
|
35
|
-
|
|
36
|
-
An ask that lives as an `ask` block in a document keeps its question and options in the block, and `dispatch edit-ask` writes the
|
|
37
|
-
block along with the row, so the edit stands and the document reads the same. It changes only the fields you name: pass `--urgency`
|
|
38
|
-
alone and the question's own wording, formatting, links and comment anchors are untouched. Pass `--question` or `--option` and that part
|
|
39
|
-
is rewritten, so anchors inside the text you replaced move as they would for any document edit. Either way it is a document edit: it
|
|
40
|
-
writes a new version, which on a spec awaiting approval closes the design gate until the new version is approved. Editing the block
|
|
41
|
-
with `dispatch doc-edit` works too and is the way to change anything else about it, including adding formatting to a question.
|
|
42
|
-
Re-sending a field unchanged rewrites nothing, so retrying the whole ask is safe.
|
|
43
|
-
Text the block cannot carry back unchanged is refused outright, naming the field and writing nothing - an option label containing
|
|
44
|
-
`": "`, the separator between a label and its description, is one example of text that cannot survive the round trip. Blank lines separate paragraphs;
|
|
45
|
-
a single newline is kept as a line break.
|
|
23
|
+
- Correct an open ask in place with `dispatch edit-ask`, not a second ask, while the same decision stays open. The human then reconfirms. You cannot edit an answered or resolved ask. If the decision changed, retract it and open a new one.
|
|
24
|
+
- `edit-ask` on a block ask writes the block too, so it mints a version and closes a spec's design gate until approved. Change anything else about the block with `dispatch doc-edit`. A retry with unchanged fields is safe.
|
|
25
|
+
- A human can change an answer. A new `ask.answered` carries `previous_answer`. Act on the newest.
|
|
26
|
+
- Retract a moot or superseded ask, or resolve one whose answer you found:
|
|
46
27
|
|
|
47
|
-
An ask stays open until a human answers, unless its question no longer needs that answer. Retract a moot or superseded question, or
|
|
48
|
-
self-resolve one after finding the answer:
|
|
49
28
|
```bash
|
|
50
29
|
dispatch resolve-ask --ask <id> --kind retracted --reason 'A newer ask supersedes this question.'
|
|
51
30
|
```
|
|
52
|
-
Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the reason because the question remains
|
|
53
|
-
in its Conversation card and reply thread; a reason beginning `removed from the document in version` is refused, because that is how a
|
|
54
|
-
retraction the document's own settlement wrote is recognised. Resolving a block ask records it in the block too, so it stays resolved
|
|
55
|
-
however the document moves afterwards. `dispatch doc-edit` refuses deleting the block of an open ask; a block removed another way, by a
|
|
56
|
-
whole-document `dispatch artifact` replace or by a person in the browser, closes its ask, and putting the block back reopens it. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
|
|
57
|
-
resolved. A human may reply to an open or answered ask; so may you, e.g. after finding the answer — use `--reply-to-ask` on
|
|
58
|
-
`dispatch comment` (mutually exclusive with `--reply-to`).
|
|
59
|
-
A review comment you opened has its own closer, `dispatch resolve-comment` — see
|
|
60
|
-
[Comments and suggestions](skill://dispatch/references/documents.md).
|
|
61
|
-
|
|
62
|
-
A human answers or asks back from the same field; a question-shaped free-text answer is offered as a clarification first. A human
|
|
63
|
-
reply while your ask is still open (the delivered `comment.created` carries `ask_state: open`) is a request for clarification, not
|
|
64
|
-
an answer: the human did not understand the question or needs more before choosing. The ask now waits on you in their Inbox. Answer
|
|
65
|
-
in the same thread with `dispatch comment --reply-to-ask <id>`, or reword the question itself with `dispatch edit-ask` when the wording
|
|
66
|
-
was the problem; either puts the ask back in front of them. Do not open a second ask.
|
|
67
31
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
turn to you. The output names the state (`ask now waiting on agent` / `human`), the delivered `comment.created` carries it as
|
|
74
|
-
`ask_waiting_on`, and every ask read carries it as `waiting_on`.
|
|
32
|
+
- Use `retracted` for obsolete, `resolved` for found. Always give a reason: the question stays in its card. Resolution never records a human decision. An answered ask cannot be resolved.
|
|
33
|
+
- `doc-edit` refuses to delete an open ask's block. A block deleted another way (an upload, the browser) closes its ask. Restoring it reopens the ask.
|
|
34
|
+
- Reply under an ask with `dispatch comment --reply-to-ask <id>`. Close your review comment with `dispatch resolve-comment` ([Documents](skill://dispatch/references/documents.md#comments-and-suggestions)).
|
|
35
|
+
- A human reply while the ask is open (`ask_state: open`) is a request for clarification, not an answer. Answer in the thread with `--reply-to-ask`, or reword with `edit-ask`. Do not open a second ask.
|
|
36
|
+
- Each reply sets whose turn it is. Your default reply (`--turn human`) puts the ask in the human's `Waiting on you`. For any progress or working-on-it note, use `--turn agent`. A human reply hands the turn to you. Reads show it as `waiting_on`.
|
|
75
37
|
|
|
76
38
|
## Following
|
|
77
39
|
|
|
78
|
-
|
|
79
|
-
`dispatch
|
|
80
|
-
|
|
81
|
-
route. The output says so (`You follow this ask: its answer and replies reach you directly.`) once per ask.
|
|
82
|
-
Leave a thread you no longer need, or rejoin one, with:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
dispatch follow --ask <id> --action unfollow # or --action follow
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
`--ask` is the full ask id or a `dispatch://KEY/ask/<id>` reference; `dispatch follow`, `dispatch edit-ask`, and
|
|
89
|
-
`dispatch resolve-ask` also take an 8+ hex prefix that is unique among your own open asks, and refuse anything shorter by naming
|
|
90
|
-
those asks. A human may also remove you from the ask card; either way you are
|
|
91
|
-
told with an `ask.follower_removed` notice, and a human adding you arrives as `ask.follower_added`.
|
|
92
|
-
|
|
93
|
-
No write subscribes you to an issue or document. Following covers your own asks and the threads you joined; a person's reply to
|
|
94
|
-
a comment you started, and a person's new comment on a document you wrote or have a version crediting you, reach you directly as
|
|
95
|
-
well; everything else on the owner — other sessions' asks, comments, messages, status changes — reaches you only if you subscribe
|
|
96
|
-
to the owner topic yourself.
|
|
97
|
-
Every write result names that line: `envoy_subscribe notifications.dispatch.issue.<KEY>.>` for an issue,
|
|
98
|
-
`envoy_subscribe notifications.dispatch.document.<PROJECT>.<SLUG>.>` for a project document. The owner topic carries every Dispatch
|
|
99
|
-
event; `notify` only controls agent wake and routed delivery. A human may unsubscribe you from the issue or document header; you
|
|
100
|
-
are told with a `subscription.removed` notice when that happens.
|
|
101
|
-
|
|
40
|
+
- You follow an ask you opened or replied to, or one a human added you to. Its answer, edits, resolution and replies reach your agent topic directly.
|
|
41
|
+
- Leave or rejoin: `dispatch follow --ask <id> --action unfollow` (or `follow`). `--ask` takes a full id, a `dispatch://KEY/ask/<id>`, or an 8+ hex prefix unique among your open asks. Removal arrives as `ask.follower_removed`, an add as `ask.follower_added`.
|
|
42
|
+
- No write subscribes you to an owner. Replies to your comments and new comments on your documents also reach you. For everything else, subscribe: `envoy_subscribe notifications.dispatch.issue.<KEY>.>` or `envoy_subscribe notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The topic carries every event. `notify` controls only wake and routed delivery. A human can unsubscribe you (`subscription.removed`).
|
|
@@ -1,208 +1,59 @@
|
|
|
1
1
|
# Editing a document
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
with `dispatch doc-edit`, never re-uploaded. "The Spec" in `skill://dispatch` sends you here for
|
|
5
|
-
the command's shape, how to target the text you mean, what each operation costs a block, and how to
|
|
6
|
-
reject a stale edit.
|
|
3
|
+
Edit every Dispatch document (an issue's spec, a project document) in place with `dispatch doc-edit`. Never upload it again. `dispatch doc-edit --help` has the full rules for each operation: markers, task boxes, hard breaks, anchors and preconditions.
|
|
7
4
|
|
|
8
5
|
```bash
|
|
9
6
|
dispatch doc-edit --issue <KEY> --artifact spec --summary '<names the version>' --ops-json-file - <<'EOF'
|
|
10
7
|
[{"op": "replace", "find": "old sentence", "with": "new sentence"}]
|
|
11
8
|
EOF
|
|
12
9
|
```
|
|
13
|
-
`--project` replaces `--issue` for a project document, and `--precondition-json` guards against a
|
|
14
|
-
stale edit (below). It prints whether the edit applied, the version it wrote, which operations
|
|
15
|
-
changed nothing (`unchanged_ops`) or were lost to a concurrent change (`lost_ops`), and the document
|
|
16
|
-
token this edit produced, as a `Document token: <token>` line. The ops are a JSON array of this exact `EditOp` shape:
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
find?: string;
|
|
22
|
-
with?: string;
|
|
23
|
-
occurrence?: number;
|
|
24
|
-
markdown?: string;
|
|
25
|
-
after?: string;
|
|
26
|
-
before?: string;
|
|
27
|
-
block?: string;
|
|
28
|
-
index?: number;
|
|
29
|
-
type?: string;
|
|
30
|
-
attributes?: Record<string, unknown>;
|
|
31
|
-
};
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
An operation takes only these keys. A key it does not declare is refused before the command sends
|
|
35
|
-
anything, naming the operation and its keys, because every key but `op` is optional: a
|
|
36
|
-
misspelled `with` would otherwise be dropped and the `replace` would delete the text you meant to
|
|
37
|
-
rewrite.
|
|
11
|
+
- Ops: `replace`, `delete`, `insert`, `retype`, `move`, `delete_row`, `delete_column`. Keys: `op`, `find`, `with`, `occurrence`, `markdown`, `after`, `before`, `block`, `index`, `type`, `attributes`. An unknown key is refused before sending. A misspelled `with` would turn the `replace` into a delete.
|
|
12
|
+
- The output names the version, `unchanged_ops`, `lost_ops`, and a `Document token:`.
|
|
13
|
+
- Use `--summary` to name a version that records a decision.
|
|
38
14
|
|
|
39
|
-
|
|
40
|
-
code without backticks, bold without asterisks, and link text without link syntax. A table-cell
|
|
41
|
-
anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay
|
|
42
|
-
within one textblock; split changes that span separate blocks into separate operations.
|
|
15
|
+
## Targeting text
|
|
43
16
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`
|
|
47
|
-
Ordinary inserts create a sibling block before or after the quote, heading, or block's enclosing document block, and a move lands the
|
|
48
|
-
block at that same boundary; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no header or
|
|
49
|
-
delimiter rows) extends that table before or after the matched row instead, decided in three steps. A fragment whose every line yields
|
|
50
|
-
a cell and an unescaped `|` (a lone `|` yields no cell, and a line whose only pipe is `\|` has no separator), with no line a delimiter
|
|
51
|
-
row of three hyphens or more a cell, is parsed as rows of that table. What that parse refuses is refused: a row holding text past the
|
|
52
|
-
table's width as `TABLE_WIDTH` (blank cells there are dropped), a line the table cannot hold as a row (indented code, `2. | a | b |`)
|
|
53
|
-
as `INVALID_OP`. When it refuses nothing and reads every line as a row, the rows are inserted, short ones padded. Any other fragment is
|
|
54
|
-
read on its own as blocks, which are inserted after the table or refused as such blocks would be: a whole table you paste there, its
|
|
55
|
-
delimiter row three hyphens or more a cell, becomes a second table, while one with any delimiter cell of one or two hyphens (`| - |`,
|
|
56
|
-
`| --- | - |`) passes the first step, so its header and delimiter rows are inserted as rows of the table, or refused as `TABLE_WIDTH`
|
|
57
|
-
where it is wider than the table. Deleting
|
|
58
|
-
a cell's quoted text removes only that text. `delete_row` / `delete_column` instead mutate their named table in place, keeping the
|
|
59
|
-
table's block id. A row index includes the header: row `0` is the header and its deletion promotes the first body row. The last body
|
|
60
|
-
row and any row's last column cannot be deleted. An index is required. A missing, non-integer, negative, or out-of-range index is
|
|
61
|
-
`INVALID_OP` on `index`, naming the supplied value and the table's actual dimensions before making any change. Markdown parsing
|
|
62
|
-
canonicalizes short ragged rows by padding missing cells, so column deletion preserves every non-selected cell in the canonical table.
|
|
63
|
-
`dispatch doc-read --issue <KEY> --blocks` (or `--project` and `--artifact`) lists every block of the live document in place of its
|
|
64
|
-
markdown, one line each, in document order and indented under the block holding it: its id, its type, its token, how many comments
|
|
65
|
-
and asks anchor to it (a table counts its cells' anchors) and its opening words. Under a table it lists each row by its index, what
|
|
66
|
-
`delete_row` takes (0 is the header), and each cell by its row and its column, what `delete_column` takes, with the opening words
|
|
67
|
-
of the header drawn above it. A cell spanning columns or rows counts once in its row, so its column is that index and not where
|
|
68
|
-
the markdown writes it. A Dispatch that does not list table rows yet says so instead of an index. It takes no `--version`, since
|
|
69
|
-
only the live document's ids and tokens address an edit, and an uploaded file has no blocks.
|
|
70
|
-
`GET /api/v1/artifacts/<artifact UUID>/blocks` and `GET /api/v1/artifacts/<artifact UUID>/blocks/<block id>` answer the same
|
|
71
|
-
facts for a reader outside the command. A row or column
|
|
72
|
-
deletion that would remove an open ask or unresolved comment anchor is `INVALID_OP` on `index`, naming the axis and anchor ids;
|
|
73
|
-
answered asks and resolved comments are history and do not block it. A `find` or quote anchor tolerates inline Markdown
|
|
74
|
-
(`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the quote and the three nearest blocks so
|
|
75
|
-
the next quote lands, and a `find` cut before a closing `**` or `` ` `` is refused as an unbalanced inline mark rather than reported
|
|
76
|
-
as a miss. A `heading:` anchor matches the whole heading text exactly — a prefix of a longer heading is a miss, naming the anchor and
|
|
77
|
-
the nearest headings. `replace` is inline: `with` is the new text of the matched span inside its block, so a marker of a *different*
|
|
78
|
-
kind from the block's own (`4. Design` written into a heading, `# Title` into a paragraph) stays literal text and never turns the
|
|
79
|
-
block into a list or heading. A `with` that opens with a marker of the *same* kind as the matched block's own would write it twice and
|
|
80
|
-
is rejected (`INVALID_OP` on `with`) — including prose that merely looks like a marker (`1999. was a year` into an ordered item),
|
|
81
|
-
which is written as text with a backslash escape (`1999\. was a year`) — omit the marker to replace the block's text, or use `insert`
|
|
82
|
-
plus `delete` to change the block's kind, level or number. The one exception is a heading rename whose `find` carried a heading
|
|
83
|
-
marker: `replace(find="## Old", with="## New")` gives `## New`. A different level in `with` applies only when `find` named the
|
|
84
|
-
heading's actual level — `find="## Old"`, `with="### New"` retitles and makes it an h3 — because `# ` is the level-blind selector,
|
|
85
|
-
so `find="# Old"` renames the text and keeps whatever level it selected. A task item (`- [ ]` / `- [x]`) is ticked the same way:
|
|
86
|
-
when `find` matches from the start of the item's text and `with` opens with `[x] ` or `[ ] `, the opening sets the item's
|
|
87
|
-
checkbox and the rest is its new text — `replace(find="Write it", with="[x] Write it")` ticks it, keeping its block id — since
|
|
88
|
-
the box is the item's attribute, not text, and written as text it would read `- [ ] [x] …`. The same opening over a plain list
|
|
89
|
-
item or a paragraph stays literal text, as any marker of another kind does. `with` that forms more than one
|
|
90
|
-
paragraph is rejected (`INVALID_OP` on `with`) — see the recipe for a multi-paragraph rewrite below; so is any non-empty `with` that
|
|
91
|
-
renders to no text, which a line indented four spaces or a tab does (markdown reads that as a code block), as does whitespace
|
|
92
|
-
alone. An empty `with` deletes the matched text on purpose; a list item, quote, typed block or footnote definition left
|
|
93
|
-
holding only the emptied paragraph keeps it, and reads back holding it. Emptying a task item's first paragraph while
|
|
94
|
-
another block follows it in the item is `INVALID_OP`, since the browser editor reads no such item as a task. Inside a code
|
|
95
|
-
block none of this applies: `with` is the code's literal text, written as sent, whitespace, markdown syntax and
|
|
96
|
-
references included, except that line breaks at the end of the code's text, and a line holding only whitespace in a
|
|
97
|
-
list item's code, do not survive the next read. A line of colons in code inside a typed block is kept: Dispatch writes
|
|
98
|
-
that typed block's fence longer than any such line the browser editor would end it at. Text a `replace` writes that
|
|
99
|
-
would read as block syntax at a line start is stored escaped and reads back as the characters you sent: `---` over a
|
|
100
|
-
paragraph is stored `\---`, not a rule, so to add a rule, `insert` it beside the paragraph (`insert` with markdown
|
|
101
|
-
`***`). A CR LF or a lone carriage return in any text you write - an insert's markdown, a `with` in text or in code,
|
|
102
|
-
a suggestion, an upload or a spec - is stored as a line feed, as the browser editor reads both. Use zero-based
|
|
103
|
-
`occurrence` for a
|
|
104
|
-
repeated target; re-read a missing or ambiguous target before retrying. Pass `--summary` to name the version when recording a decision.
|
|
17
|
+
- Quote text as rendered: inline code without backticks, bold without asterisks, link text without syntax, a cell by its text, code without fences. One quote stays in one textblock. Split changes that span blocks.
|
|
18
|
+
- A miss names the three nearest blocks. A `heading:` anchor must match the whole heading. Use zero-based `occurrence` for a repeat. Re-read before you retry a missing or ambiguous target.
|
|
19
|
+
- `dispatch doc-read --blocks` lists each live block's id, type, token, anchor count and opening words, and each table row and cell index. Only live ids address an edit.
|
|
105
20
|
|
|
106
|
-
|
|
107
|
-
each `with` is the new text of one paragraph, and outside a code block a `with` that forms two paragraphs is
|
|
108
|
-
refused whatever the text says. Give each paragraph you rewrite its own `replace`, which keeps that paragraph's block id and every
|
|
109
|
-
anchor outside the text you rewrite. A comment or ask anchored to the text you rewrite loses its quote but keeps
|
|
110
|
-
its pin to the block, so the dashboard still shows it beside that paragraph; a delete (below) loses both. An
|
|
111
|
-
anchor that straddles the boundary keeps its mark over the words you left alone, with its quote shortened to
|
|
112
|
-
them: rewriting `charlie delta.` under a comment on `bravo charlie` leaves that comment quoting `bravo `. When
|
|
113
|
-
the new text has more paragraphs than the old, write them as **one** `insert` whose `markdown` holds them all,
|
|
114
|
-
anchored on the last paragraph you rewrote with `after` quoting it exactly as it now reads (the operations in
|
|
115
|
-
one batch apply in order): each separate `insert` at the same anchor lands directly after it, so two of them
|
|
116
|
-
come out in the reverse of the order you wrote. An insert lands after the top-level block that holds the quote,
|
|
117
|
-
so beside a paragraph inside a list item or a typed block it goes after the whole list or block. When it has fewer,
|
|
118
|
-
`delete` each leftover paragraph, with its whole text as `find` or its id as `block`. All of that holds while
|
|
119
|
-
the new text is paragraphs: `replace` keeps a block's kind, so any block that is not a paragraph — a heading,
|
|
120
|
-
list, table, blockquote, rule, code fence or typed block — cannot be replaced into place. Its marker is written
|
|
121
|
-
as literal text, except a backtick fence, which becomes an inline code span whose text is everything inside it,
|
|
122
|
-
info string and line breaks included (```` ```go\nx := 1``` ```` becomes the code span `go` + a line break +
|
|
123
|
-
`x := 1`), and a tilde fence, which stays literal.
|
|
21
|
+
## Rewriting paragraphs
|
|
124
22
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
23
|
+
- **One `replace` per paragraph, then a read-back.** A `with` of two paragraphs is refused. Each `replace` keeps the block id and the anchors outside the text you change. An anchor on changed text loses its quote but keeps its pin.
|
|
24
|
+
- More new paragraphs than old: add the rest as **one** `insert` holding them all, `after` the last rewritten paragraph as it now reads. Two inserts at one anchor come out reversed. An insert lands after the top-level block that holds the quote.
|
|
25
|
+
- Fewer: `delete` each leftover paragraph by its whole text or `block` id.
|
|
26
|
+
- `replace` keeps a block's kind. When the new text adds a non-paragraph block (heading, list, table, quote, rule, fence or typed block) beside paragraphs, `insert` it beside the paragraph you replaced. Only when no paragraph of the new text stays, `delete` the old block, then `insert` the new one, anchored on the block before or after it.
|
|
27
|
+
- An empty `with` deletes the matched text on purpose and leaves an empty block. Read the passage and its neighbours back with `dispatch doc-read`. A grep for your new words is not a check.
|
|
28
|
+
- Dispatch refuses block HTML (`<div>`, `<!-- -->`, a lone `<br>` paragraph) in `with`. Inline HTML stays.
|
|
29
|
+
- To add a rule, `insert` `***`. A `---` in `with` is stored as `\---`.
|
|
130
30
|
|
|
131
|
-
|
|
132
|
-
table cell (`INVALID_OP` on `with`): both are written on one line, so the break would end the block there. A line
|
|
133
|
-
break inside a code span or inline HTML there is refused the same way. Write the text without the break, or
|
|
134
|
-
`insert` a new block after this one.
|
|
31
|
+
## Blocks and ids
|
|
135
32
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
The tools refuse a `delete` or `retype` that would take out an `ask` block whose ask is still open, delete-then-insert included:
|
|
142
|
-
reword it with `replace`, relocate it with `move`, or change its question, options, urgency or `multiple` with `dispatch edit-ask` if you asked it. A single
|
|
143
|
-
batch that moves an open block out of a container and then deletes the container is refused; make them two calls (the check reads
|
|
144
|
-
the document as it stood before the batch).
|
|
145
|
-
Then read the document back with
|
|
146
|
-
`dispatch doc-read` and read the passage and its neighbours, not a grep for the words you added: an empty
|
|
147
|
-
`with` deletes the matched text on purpose, so a `replace` whose `with` you meant to fill
|
|
148
|
-
empties that paragraph — the block and its id stay, holding nothing — and only a read shows what the document now says.
|
|
33
|
+
- A block loses its id only when deleted: by `delete`, by `delete_row` / `delete_column` (its cells), or by a `move` that empties its container. Its anchors go with it. `replace`, `insert`, `move` and `retype` keep ids.
|
|
34
|
+
- `delete` of a block's whole text deletes the block. A partial match keeps it. A bullet with a nested list hoists the list. A bullet with other content needs `delete {block:"<item id>"}`.
|
|
35
|
+
- Dispatch refuses a `delete` or `retype` that takes out an open ask's block, delete-then-insert included. Reword with `replace`, relocate with `move`, or use `dispatch edit-ask`. Moving an open block out of a container and deleting the container takes two calls.
|
|
36
|
+
- `retype` turns a paragraph or typed block into a typed block in place, keeping its id. Use it when a paragraph is the question that should become a decision.
|
|
37
|
+
- An ask block has two ids. The block id (`:::ask{#<id> …}`) goes bare in `block` for `move` / `delete` (`block:<id>` only in `before` / `after`). The ask id goes to `dispatch open-asks`, `dispatch read` and `--reply-to-ask`. `dispatch://KEY/ask/<block-id>` answers `not found`.
|
|
149
38
|
|
|
150
|
-
|
|
151
|
-
comment or ask anchor still mints its version — mints no version, named or not: the response carries
|
|
152
|
-
`changed: false` with `unchanged_ops` naming each operation that did nothing, and the output says nothing changed. A `--summary`
|
|
153
|
-
does not force a version for such a batch; `POST /api/v1/artifacts/<id>/versions`, which names the current state on purpose, still does.
|
|
39
|
+
## Tables
|
|
154
40
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
that holds a nested list hoists that list's items into the bullet's place (as an outliner does); a bullet with any other content
|
|
158
|
-
(paragraphs, code, tables) is refused with `INVALID_OP` naming `delete {block:"<item id>"}`, which removes the item with its content.
|
|
159
|
-
`delete` with `block` removes any block by id (paragraph, heading, list, list item, table, or typed block; an answered `ask` block keeps
|
|
160
|
-
its answer as the record, and a resolved one, which carries no answer, its resolution), and `move` with `block` relocates one, keeping its id and
|
|
161
|
-
attributes — a moved `ask` keeps its ask and answer. **A block loses its id only when it is removed**, and its anchors go with it:
|
|
162
|
-
`delete` by text or by id removes the block and any container it empties; `delete_row` / `delete_column` remove their cells' ids,
|
|
163
|
-
which is why they are refused while an open ask or unresolved comment sits on them; and a `move` that takes the last block out of a
|
|
164
|
-
blockquote or list item removes that emptied container, the list too when no item remains, and each enclosing container that
|
|
165
|
-
held nothing else (`> - Only.` loses the blockquote as well as the item and the list). The moved block itself keeps its id,
|
|
166
|
-
as do `replace` (an accepted empty `with` and a heading-level change included), `insert` and `retype`; `retype` carries the
|
|
167
|
-
paragraph's id onto the typed block it becomes. Block ids are the `#id` a typed block renders
|
|
168
|
-
(`:::ask{#5467e5ce-…}`) and, for every block including untyped ones, the ids `dispatch doc-read --blocks` lists. A later operation in the same atomic batch that names a block removed by
|
|
169
|
-
an earlier `delete {block}` fails as `INVALID_OP` naming the earlier operation and the parent block that cascaded the removal. A move
|
|
170
|
-
whose anchor lies inside the moved block, or a delete that would leave a typed block without the body its content rule requires, is
|
|
171
|
-
`INVALID_OP` naming the field and the rule.
|
|
41
|
+
- `delete_row` / `delete_column` take the table `block` and a zero-based `index`. Row 0 is the header. The last body row and a row's last column cannot go. Open asks or unresolved comments on the cells block it.
|
|
42
|
+
- Inserting bare body rows (no header or delimiter) at a cell quote extends that table. A wider row is `TABLE_WIDTH`. A full table with a header pasted there becomes a second table.
|
|
172
43
|
|
|
173
|
-
|
|
174
|
-
inline marks. To reject a stale edit, pass `--precondition-json` with exactly one of
|
|
175
|
-
`{"document": "<document token>"}` or
|
|
176
|
-
`{"blocks": [{"id": "<block id>", "token": "<block token>"}]}`. A document token comes from
|
|
177
|
-
`dispatch doc-read` or from the previous `dispatch doc-edit`, whose output carries the token its
|
|
178
|
-
own edit produced: a run of guarded edits passes each output's token to the next one and reads the
|
|
179
|
-
document once, not once per edit. The server resolves the whole batch before
|
|
180
|
-
mutation: a block guard must cover every content block it changes, or Dispatch returns
|
|
181
|
-
`400 INVALID_PRECONDITION` without applying anything. Use a document token for insert and move because they
|
|
182
|
-
depend on document order. A block token lets other sections change concurrently; a new anchored ask or comment
|
|
183
|
-
changes the relevant token. A stale guard returns `409 PRECONDITION_FAILED` with each mismatch and current
|
|
184
|
-
token; Dispatch applies no part of that batch. It is the hashline `#TAG` property applied to stable block ids,
|
|
185
|
-
not line numbers: canonical Markdown lines shift under concurrent edits and rendering changes, while block ids
|
|
186
|
-
survive moves and retyping.
|
|
44
|
+
## Stale and concurrent edits
|
|
187
45
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`lost_ops` names operations means the version was written and the live document already lacks what
|
|
193
|
-
those operations wrote, because the deletion landed after the version: re-read before building on
|
|
194
|
-
it. `lost_ops: []` is the ordinary outcome, and a result that says it could not confirm the edit
|
|
195
|
-
survived means the room is reloading or holds a tree too deep to read — re-read, and a read that
|
|
196
|
-
answers `DOC_SCHEMA` says which. An operation that only removes text (`delete`, `delete_row`,
|
|
197
|
-
`delete_column`, a `replace` that shortens) is never reported lost: a concurrent deletion cannot
|
|
198
|
-
undo a removal.
|
|
46
|
+
- Guard with `--precondition-json`: `{"document": "<token>"}` or `{"blocks": [{"id": "<id>", "token": "<token>"}]}`. Use a document token for `insert` and `move`. A block guard must cover every block the batch changes.
|
|
47
|
+
- Chain guarded edits by passing each output's token to the next. Read once, not once per edit.
|
|
48
|
+
- `409 PRECONDITION_FAILED` or `409 EDIT_LOST_TO_CONCURRENT_CHANGE`: nothing written. Re-read and decide again. Non-empty `lost_ops` on success: a browser deleted that text after the write. Re-read before you build on it. A result that could not check survival: re-read.
|
|
49
|
+
- A batch that changes nothing mints no version, even with `--summary`. `POST /api/v1/artifacts/<id>/versions` names the current state as a version on purpose.
|
|
199
50
|
|
|
200
|
-
|
|
201
|
-
block id, keeps a typed block's body, and uses `attributes` for client-owned typed attributes. Use it when
|
|
202
|
-
an existing paragraph is the question that should become a decision.
|
|
51
|
+
## Edge cases
|
|
203
52
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
53
|
+
- Inside a code block, `with` is the code's literal text. Trailing line breaks, and whitespace-only lines in a list item's code, do not survive a read.
|
|
54
|
+
- A CR LF or lone CR in any text you write is stored as a line feed.
|
|
55
|
+
- Replacing a non-paragraph block writes its marker as text. A backtick fence becomes an inline code span. A tilde fence stays literal.
|
|
56
|
+
- An anchor that straddles the rewritten text keeps its mark on the words you left, with a shorter quote.
|
|
57
|
+
- Emptying a task item's first paragraph while another block follows it is `INVALID_OP`.
|
|
58
|
+
- A later op in one batch that names a block an earlier `delete {block}` took out fails as `INVALID_OP`. So does a move anchored inside the moved block, or a delete that leaves a typed block without its required body.
|
|
59
|
+
- An op that only takes text out is never reported lost.
|