@sjawhar/pi-envoy 0.0.0 → 8.1.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 +160 -0
- package/dist/THIRD_PARTY_NOTICES +513 -0
- package/dist/envoy.js +37511 -0
- package/dist/skills/dispatch/SKILL.md +498 -0
- package/dist/skills/dispatch/references/api.md +40 -0
- package/dist/skills/dispatch/references/asks.md +116 -0
- package/dist/skills/dispatch/references/document-edits.md +202 -0
- package/dist/skills/dispatch/references/documents.md +230 -0
- package/dist/skills/dispatch/references/examples.md +132 -0
- package/dist/skills/dispatch/references/issues.md +213 -0
- package/dist/skills/dispatch/references/messages.md +74 -0
- package/dist/skills/dispatch/references/reading.md +64 -0
- package/dist/skills/dispatch-brainstorming/SKILL.md +113 -0
- package/dist/skills/dispatch-first/SKILL.md +58 -0
- package/dist/skills/envoy/SKILL.md +275 -0
- package/package.json +42 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Asks after they open: who answers, editing, resolving, replying, and following
|
|
2
|
+
|
|
3
|
+
`skill://dispatch` sends you here once you are asking: to find who answers an ask, hand a human a
|
|
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`.
|
|
7
|
+
|
|
8
|
+
## Who answers an ask
|
|
9
|
+
|
|
10
|
+
An ask goes to the issue's assignee: their Inbox opens on **Mine**, which lists asks on the issues they hold plus an Unassigned band; an ask on an unassigned issue waits in that band for someone to take it. Find out who Dispatch takes you for with:
|
|
11
|
+
```ts
|
|
12
|
+
dispatch_whoami({})
|
|
13
|
+
```
|
|
14
|
+
It returns `details` `{ session, owner, service }`: `owner` is the lowercase email of the person whose personal token you run under, or `null` under the shared token or a verified service token, in which case `service` is that token's subject (`system:serviceaccount:<namespace>:<name>`, `null` otherwise) and every write you make is rendered `(as <namespace>/<name>)` — the namespace is kept because every namespace has a `default` service account, and a subject that is not a Kubernetes one is shown whole. An issue you create without `assignee` goes to your owner; with no owner it inherits its parent's assignee, or stays unassigned without a parent. If an issue you are asking on is unassigned and the answer matters, assign it to your owner (`PATCH /api/v1/issues/{key}` with `{"assignee": "<email>"}`; any authenticated caller may reassign, and an email nobody has signed in with is refused with `ASSIGNEE_NOT_ALLOWED`) or name in the question who should answer it. Never reassign an issue a human holds to get an answer faster: that is the human's call.
|
|
15
|
+
|
|
16
|
+
## Handing a human a to-do, editing, resolving, and replying
|
|
17
|
+
|
|
18
|
+
A to-do handed to a human still follows the question and options contract that `skill://dispatch`'s
|
|
19
|
+
"Writing for the human" states. For a human-only action, what it changes and risks constrains the
|
|
20
|
+
answer. There is no fixed vocabulary and the server treats no label specially.
|
|
21
|
+
If an outcome needs a reason, say so in that option's description, and the human's free-text answer
|
|
22
|
+
carries it:
|
|
23
|
+
```ts
|
|
24
|
+
dispatch_ask({
|
|
25
|
+
issue: "DSP-42",
|
|
26
|
+
question:
|
|
27
|
+
"The verified fix remains unavailable in production because deployment is pending. Deploying changes production and could expose a deployment problem. How should we proceed? Recommendation: deploy the verified fix now because it restores the intended behavior.",
|
|
28
|
+
options: [
|
|
29
|
+
{ label: "Deploy the verified fix", description: "Restores the fix but can expose a deployment problem." },
|
|
30
|
+
{ label: "Hold the deploy", description: "Avoids a production change now but leaves production without the fix." },
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Correct or refine an open ask in place instead of opening a second question:
|
|
36
|
+
```ts
|
|
37
|
+
dispatch_edit_ask({
|
|
38
|
+
ask,
|
|
39
|
+
question?,
|
|
40
|
+
options?: { label, description? }[],
|
|
41
|
+
multiple?,
|
|
42
|
+
urgency?,
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
At least one field besides `ask` is required. Use this only while the same decision remains open: it keeps the prior text in the event
|
|
46
|
+
log and invalidates any answer draft against the prior `edited_at` revision, so the human sees the new wording and explicitly reconfirms.
|
|
47
|
+
An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
|
|
48
|
+
|
|
49
|
+
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
|
|
50
|
+
block along with the row, so the edit stands and the document reads the same. It changes only the fields you name: pass `urgency`
|
|
51
|
+
alone and the question's own wording, formatting, links and comment anchors are untouched. Pass `question` or `options` and that part
|
|
52
|
+
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
|
|
53
|
+
writes a new version, which on a spec awaiting approval closes the design gate until the new version is approved. Editing the block
|
|
54
|
+
with `dispatch_doc_edit` works too and is the way to change anything else about it, including adding formatting to a question.
|
|
55
|
+
Re-sending a field unchanged rewrites nothing, so retrying the whole ask is safe.
|
|
56
|
+
Text the block cannot carry back unchanged is refused outright, naming the field and writing nothing - an option label containing
|
|
57
|
+
`": "`, the separator between a label and its description, is one example of text that cannot survive the round trip. Blank lines separate paragraphs;
|
|
58
|
+
a single newline is kept as a line break.
|
|
59
|
+
|
|
60
|
+
An ask stays open until a human answers, unless its question no longer needs that answer. Retract a moot or superseded question, or
|
|
61
|
+
self-resolve one after finding the answer:
|
|
62
|
+
```ts
|
|
63
|
+
dispatch_resolve_ask({
|
|
64
|
+
ask,
|
|
65
|
+
kind: "retracted",
|
|
66
|
+
reason: "A newer ask supersedes this question.",
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the reason because the question remains
|
|
70
|
+
in its Conversation card and reply thread; a reason beginning `removed from the document in version` is refused, because that is how a
|
|
71
|
+
retraction the document's own settlement wrote is recognised. Resolving a block ask records it in the block too, so it stays resolved
|
|
72
|
+
however the document moves afterwards. `dispatch_doc_edit` refuses deleting the block of an open ask; a block removed another way, by a
|
|
73
|
+
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
|
|
74
|
+
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
|
|
75
|
+
`dispatch_comment` (mutually exclusive with `reply_to`).
|
|
76
|
+
A review comment you opened has its own closer, `dispatch_resolve_comment` — see
|
|
77
|
+
[Comments and suggestions](skill://dispatch/references/documents.md).
|
|
78
|
+
|
|
79
|
+
A human answers or asks back from the same field; a question-shaped free-text answer is offered as a clarification first. A human
|
|
80
|
+
reply while your ask is still open (the delivered `comment.created` carries `ask_state: open`) is a request for clarification, not
|
|
81
|
+
an answer: the human did not understand the question or needs more before choosing. The ask now waits on you in their Inbox. Answer
|
|
82
|
+
in the same thread with `dispatch_comment({ reply_to_ask })`, or reword the question itself with `dispatch_edit_ask` when the wording
|
|
83
|
+
was the problem; either puts the ask back in front of them. Do not open a second ask.
|
|
84
|
+
|
|
85
|
+
Every reply to an open ask says whose turn it is next, and the Inbox files the ask by that, not by who spoke last. Your plain reply
|
|
86
|
+
(`turn` omitted, or `turn: "human"`) hands the turn to the human: the ask returns to their `Waiting on you`. When you are not done
|
|
87
|
+
yet — "dispatched two auditors, back with results", "checking the release branch, back shortly", any working-on-it note — reply with
|
|
88
|
+
`turn: "agent"`: the note lands in the thread, the ask stays under `Waiting on agents`, and the human is not told to act. Use
|
|
89
|
+
`turn: "agent"` for every progress note and `turn: "human"` (the default) only when you need them. A human's reply always hands the
|
|
90
|
+
turn to you. The result names the state (`ask now waiting on agent` / `human`), the delivered `comment.created` carries it as
|
|
91
|
+
`ask_waiting_on`, and every ask read carries it as `waiting_on`.
|
|
92
|
+
|
|
93
|
+
## Following
|
|
94
|
+
|
|
95
|
+
An ask has followers: every session that wrote to it — the session that opened it and every session that replied with
|
|
96
|
+
`dispatch_comment({ reply_to_ask })` — plus any session a human adds from the ask card. The ask's answer, edits, resolution, and
|
|
97
|
+
every reply on it reach each follower's own agent topic directly, whether or not the writer was a human and whatever the issue's
|
|
98
|
+
route. The tool result says so (`You follow this ask: its answer and replies reach you directly.`) and carries `details.follows.ask`;
|
|
99
|
+
the host tells you once per ask. Leave a thread you no longer need, or rejoin one, with:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
dispatch_follow({ ask, action: "follow" | "unfollow" })
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`ask` is the full ask id or a `dispatch://KEY/ask/<id>` reference; `dispatch_follow`, `dispatch_edit_ask`, and
|
|
106
|
+
`dispatch_resolve_ask` also take an 8+ hex prefix that is unique among your own open asks, and refuse anything shorter by naming
|
|
107
|
+
those asks. A human may also remove you from the ask card; either way you are
|
|
108
|
+
told with an `ask.follower_removed` notice, and a human adding you arrives as `ask.follower_added`.
|
|
109
|
+
|
|
110
|
+
No write subscribes you to an issue or document. Following covers your own asks and the threads you joined; everything else on the
|
|
111
|
+
owner — other sessions' asks, comments, messages, status changes — reaches you only if you subscribe to the owner topic yourself.
|
|
112
|
+
Every write result names that line: `envoy_subscribe notifications.dispatch.issue.<KEY>.>` for an issue,
|
|
113
|
+
`envoy_subscribe notifications.dispatch.document.<PROJECT>.<SLUG>.>` for a project document. The owner topic carries every Dispatch
|
|
114
|
+
event; `notify` only controls agent wake and routed delivery. A human may unsubscribe you from the issue or document header; you
|
|
115
|
+
are told with a `subscription.removed` notice when that happens.
|
|
116
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Editing a document
|
|
2
|
+
|
|
3
|
+
Every document a Dispatch tool writes — an issue's spec, a project document — is edited in place
|
|
4
|
+
with `dispatch_doc_edit`, never re-uploaded. "The Spec" in `skill://dispatch` sends you here for
|
|
5
|
+
the tool's shape, how to target the text you mean, what each operation costs a block, and how to
|
|
6
|
+
reject a stale edit.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
dispatch_doc_edit({ issue?, project?, artifact, ops, precondition?, summary? })
|
|
10
|
+
```
|
|
11
|
+
It returns issue or project-document owner details plus `applied`, optional `version`, `changed`,
|
|
12
|
+
`unchanged_ops`, `lost_ops`, and the document token this edit produced, rendered as a
|
|
13
|
+
`Document token: <token>` line. `ops` is an array of this exact `EditOp` shape:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
type EditOp = {
|
|
17
|
+
op: "replace" | "delete" | "insert" | "retype" | "move" | "delete_row" | "delete_column";
|
|
18
|
+
find?: string;
|
|
19
|
+
with?: string;
|
|
20
|
+
occurrence?: number;
|
|
21
|
+
markdown?: string;
|
|
22
|
+
after?: string;
|
|
23
|
+
before?: string;
|
|
24
|
+
block?: string;
|
|
25
|
+
index?: number;
|
|
26
|
+
type?: string;
|
|
27
|
+
attributes?: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
An operation takes only these keys. A key it does not declare is refused before the call leaves
|
|
32
|
+
your process, naming the operation and its keys, because every key but `op` is optional: a
|
|
33
|
+
misspelled `with` would otherwise be dropped and the `replace` would delete the text you meant to
|
|
34
|
+
rewrite.
|
|
35
|
+
|
|
36
|
+
Target `replace`, `delete`, and quote insert anchors by a block's text as rendered: write inline
|
|
37
|
+
code without backticks, bold without asterisks, and link text without link syntax. A table-cell
|
|
38
|
+
anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay
|
|
39
|
+
within one textblock; split changes that span separate blocks into separate operations.
|
|
40
|
+
|
|
41
|
+
`replace` requires `find` and `with`; `delete` requires `find` or `block`; `insert` requires `markdown` and exactly one of `after` or
|
|
42
|
+
`before`; `move` requires `block` and exactly one of `after` or `before`; and `delete_row` / `delete_column` each require a table
|
|
43
|
+
`block` plus a zero-based `index`. An insert or move anchor is a quote, `"start"`, `"end"`, `"heading:Title"`, or `"block:<id>"`.
|
|
44
|
+
Ordinary inserts create a sibling block before or after the quote, heading, or block's enclosing document block, and a move lands the
|
|
45
|
+
block at that same boundary; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no header or
|
|
46
|
+
delimiter rows) extends that table before or after the matched row instead, decided in three steps. A fragment whose every line yields
|
|
47
|
+
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
|
|
48
|
+
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
|
|
49
|
+
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 |`)
|
|
50
|
+
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
|
|
51
|
+
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
|
|
52
|
+
delimiter row three hyphens or more a cell, becomes a second table, while one with any delimiter cell of one or two hyphens (`| - |`,
|
|
53
|
+
`| --- | - |`) passes the first step, so its header and delimiter rows are inserted as rows of the table, or refused as `TABLE_WIDTH`
|
|
54
|
+
where it is wider than the table. Deleting
|
|
55
|
+
a cell's quoted text removes only that text. `delete_row` / `delete_column` instead mutate their named table in place, keeping the
|
|
56
|
+
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
|
|
57
|
+
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
|
|
58
|
+
`INVALID_OP` on `index`, naming the supplied value and the table's actual dimensions before making any change. Markdown parsing
|
|
59
|
+
canonicalizes short ragged rows by padding missing cells, so column deletion preserves every non-selected cell in the canonical table.
|
|
60
|
+
`GET /api/v1/artifacts/<artifact UUID>/blocks` reports a table's own references plus its descendant cell anchors.
|
|
61
|
+
`GET /api/v1/artifacts/<artifact UUID>/blocks/<block id>` places one block: its path from the top-level block down, and for a cell
|
|
62
|
+
the row index (0 is the header, what `delete_row` takes), the column index (what `delete_column` takes), the text of the header cell
|
|
63
|
+
drawn above it (with colspans or rowspans, the column the cell is drawn in) and the row's cells. A row or column
|
|
64
|
+
deletion that would remove an open ask or unresolved comment anchor is `INVALID_OP` on `index`, naming the axis and anchor ids;
|
|
65
|
+
answered asks and resolved comments are history and do not block it. A `find` or quote anchor tolerates inline Markdown
|
|
66
|
+
(`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the quote and the three nearest blocks so
|
|
67
|
+
the next quote lands, and a `find` cut before a closing `**` or `` ` `` is refused as an unbalanced inline mark rather than reported
|
|
68
|
+
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
|
|
69
|
+
the nearest headings. `replace` is inline: `with` is the new text of the matched span inside its block, so a marker of a *different*
|
|
70
|
+
kind from the block's own (`4. Design` written into a heading, `# Title` into a paragraph) stays literal text and never turns the
|
|
71
|
+
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
|
|
72
|
+
is rejected (`INVALID_OP` on `with`) — including prose that merely looks like a marker (`1999. was a year` into an ordered item),
|
|
73
|
+
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`
|
|
74
|
+
plus `delete` to change the block's kind, level or number. The one exception is a heading rename whose `find` carried a heading
|
|
75
|
+
marker: `replace(find="## Old", with="## New")` gives `## New`. A different level in `with` applies only when `find` named the
|
|
76
|
+
heading's actual level — `find="## Old"`, `with="### New"` retitles and makes it an h3 — because `# ` is the level-blind selector,
|
|
77
|
+
so `find="# Old"` renames the text and keeps whatever level it selected. A task item (`- [ ]` / `- [x]`) is ticked the same way:
|
|
78
|
+
when `find` matches from the start of the item's text and `with` opens with `[x] ` or `[ ] `, the opening sets the item's
|
|
79
|
+
checkbox and the rest is its new text — `replace(find="Write it", with="[x] Write it")` ticks it, keeping its block id — since
|
|
80
|
+
the box is the item's attribute, not text, and written as text it would read `- [ ] [x] …`. The same opening over a plain list
|
|
81
|
+
item or a paragraph stays literal text, as any marker of another kind does. `with` that forms more than one
|
|
82
|
+
paragraph is rejected (`INVALID_OP` on `with`) — see the recipe for a multi-paragraph rewrite below; so is any non-empty `with` that
|
|
83
|
+
renders to no text, which a line indented four spaces or a tab does (markdown reads that as a code block), as does whitespace
|
|
84
|
+
alone. An empty `with` deletes the matched text on purpose; a list item, quote, typed block or footnote definition left
|
|
85
|
+
holding only the emptied paragraph keeps it, and reads back holding it. Emptying a task item's first paragraph while
|
|
86
|
+
another block follows it in the item is `INVALID_OP`, since the browser editor reads no such item as a task. Inside a code
|
|
87
|
+
block none of this applies: `with` is the code's literal text, written as sent, whitespace, markdown syntax and
|
|
88
|
+
references included, except that line breaks at the end of the code's text, and a line holding only whitespace in a
|
|
89
|
+
list item's code, do not survive the next read. A line of colons in code inside a typed block is kept: Dispatch writes
|
|
90
|
+
that typed block's fence longer than any such line the browser editor would end it at. Text a `replace` writes that
|
|
91
|
+
would read as block syntax at a line start is stored escaped and reads back as the characters you sent: `---` over a
|
|
92
|
+
paragraph is stored `\---`, not a rule, so to add a rule, `insert` it beside the paragraph (`insert` with markdown
|
|
93
|
+
`***`). A CR LF or a lone carriage return in any text you write - an insert's markdown, a `with` in text or in code,
|
|
94
|
+
a suggestion, an upload or a spec - is stored as a line feed, as the browser editor reads both. Use zero-based
|
|
95
|
+
`occurrence` for a
|
|
96
|
+
repeated target; re-read a missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
|
|
97
|
+
|
|
98
|
+
**Rewriting several paragraphs is one `replace` per paragraph, then a read-back.** `replace` is inline:
|
|
99
|
+
each `with` is the new text of one paragraph, and outside a code block a `with` that forms two paragraphs is
|
|
100
|
+
refused whatever the text says. Give each paragraph you rewrite its own `replace`, which keeps that paragraph's block id and every
|
|
101
|
+
anchor outside the text you rewrite. A comment or ask anchored to the text you rewrite loses its quote but keeps
|
|
102
|
+
its pin to the block, so the dashboard still shows it beside that paragraph; a delete (below) loses both. An
|
|
103
|
+
anchor that straddles the boundary keeps its mark over the words you left alone, with its quote shortened to
|
|
104
|
+
them: rewriting `charlie delta.` under a comment on `bravo charlie` leaves that comment quoting `bravo `. When
|
|
105
|
+
the new text has more paragraphs than the old, write them as **one** `insert` whose `markdown` holds them all,
|
|
106
|
+
anchored on the last paragraph you rewrote with `after` quoting it exactly as it now reads (the operations in
|
|
107
|
+
one batch apply in order): each separate `insert` at the same anchor lands directly after it, so two of them
|
|
108
|
+
come out in the reverse of the order you wrote. An insert lands after the top-level block that holds the quote,
|
|
109
|
+
so beside a paragraph inside a list item or a typed block it goes after the whole list or block. When it has fewer,
|
|
110
|
+
`delete` each leftover paragraph, with its whole text as `find` or its id as `block`. All of that holds while
|
|
111
|
+
the new text is paragraphs: `replace` keeps a block's kind, so any block that is not a paragraph — a heading,
|
|
112
|
+
list, table, blockquote, rule, code fence or typed block — cannot be replaced into place. Its marker is written
|
|
113
|
+
as literal text, except a backtick fence, which becomes an inline code span whose text is everything inside it,
|
|
114
|
+
info string and line breaks included (```` ```go\nx := 1``` ```` becomes the code span `go` + a line break +
|
|
115
|
+
`x := 1`), and a tilde fence, which stays literal.
|
|
116
|
+
|
|
117
|
+
Outside a code block, HTML is not written as text at all, and a `replace` whose HTML would open a block where it lands is refused
|
|
118
|
+
(`INVALID_OP` on `with`), because the schema carries no block HTML. `<div>x</div>` and `<!-- note -->` open one
|
|
119
|
+
at the start of a paragraph or a list item and on the line after a hard break; a tag such as `<br>` opens one
|
|
120
|
+
only when it stands alone as a paragraph or a list item. The same HTML inside a line, in a table cell or in a
|
|
121
|
+
heading is inline HTML and is kept as written.
|
|
122
|
+
|
|
123
|
+
A hard line break in `with` (two trailing spaces or a backslash before a newline) is refused in a heading or a
|
|
124
|
+
table cell (`INVALID_OP` on `with`): both are written on one line, so the break would end the block there. A line
|
|
125
|
+
break inside a code span or inline HTML there is refused the same way. Write the text without the break, or
|
|
126
|
+
`insert` a new block after this one.
|
|
127
|
+
|
|
128
|
+
When the new text adds a block that is not a paragraph beside paragraphs, `insert` it beside the
|
|
129
|
+
paragraph you replaced, which keeps that paragraph's id; only when no paragraph of the new text is left to take
|
|
130
|
+
the old block's place is it a `delete` of the old block and then an `insert` of the new one, anchored on the
|
|
131
|
+
block before or after it. The delete is what costs the id (below); a typed block keeps its id when the insert
|
|
132
|
+
carries it, which works only in that order, because an insert carrying an id the document still holds is refused.
|
|
133
|
+
The tools refuse a `delete` or `retype` that would take out an `ask` block whose ask is still open, delete-then-insert included:
|
|
134
|
+
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
|
|
135
|
+
batch that moves an open block out of a container and then deletes the container is refused; make them two calls (the check reads
|
|
136
|
+
the document as it stood before the batch).
|
|
137
|
+
Then read the document back with
|
|
138
|
+
`dispatch_doc_read` and read the passage and its neighbours, not a grep for the words you added: an empty
|
|
139
|
+
`with` deletes the matched text on purpose, so a `replace` whose `with` you meant to fill
|
|
140
|
+
empties that paragraph — the block and its id stay, holding nothing — and only a read shows what the document now says.
|
|
141
|
+
|
|
142
|
+
A batch that leaves the document's semantic identity unchanged — including its inline anchor marks, so an edit that only orphans a
|
|
143
|
+
comment or ask anchor still mints its version — mints no version, named or not: the response carries
|
|
144
|
+
`changed: false` with `unchanged_ops` naming each operation that did nothing, and the tool result says nothing changed. A `summary`
|
|
145
|
+
does not force a version for such a batch; `POST /api/v1/artifacts/<id>/versions`, which names the current state on purpose, still does.
|
|
146
|
+
|
|
147
|
+
A `delete` whose `find` is a block's entire text removes the block itself — the bullet, paragraph, or heading, not just its words — and
|
|
148
|
+
a list emptied of every item disappears with it; a partial match keeps the block with its remaining text. Deleting the text of a bullet
|
|
149
|
+
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
|
|
150
|
+
(paragraphs, code, tables) is refused with `INVALID_OP` naming `delete {block:"<item id>"}`, which removes the item with its content.
|
|
151
|
+
`delete` with `block` removes any block by id (paragraph, heading, list, list item, table, or typed block; an answered `ask` block keeps
|
|
152
|
+
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
|
|
153
|
+
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:
|
|
154
|
+
`delete` by text or by id removes the block and any container it empties; `delete_row` / `delete_column` remove their cells' ids,
|
|
155
|
+
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
|
|
156
|
+
blockquote or list item removes that emptied container, the list too when no item remains, and each enclosing container that
|
|
157
|
+
held nothing else (`> - Only.` loses the blockquote as well as the item and the list). The moved block itself keeps its id,
|
|
158
|
+
as do `replace` (an accepted empty `with` and a heading-level change included), `insert` and `retype`; `retype` carries the
|
|
159
|
+
paragraph's id onto the typed block it becomes. Block ids are the `#id` a typed block renders
|
|
160
|
+
(`:::ask{#5467e5ce-…}`) and, for every block including untyped ones, the `id` rows from
|
|
161
|
+
`GET /api/v1/artifacts/<artifact UUID>/blocks` (or `/api/v1/issues/{key}/artifacts/{slug}/blocks`), each with its `type` and byte range
|
|
162
|
+
in canonical markdown; the UUID route does not accept a slug. A later operation in the same atomic batch that names a block removed by
|
|
163
|
+
an earlier `delete {block}` fails as `INVALID_OP` naming the earlier operation and the parent block that cascaded the removal. A move
|
|
164
|
+
whose anchor lies inside the moved block, or a delete that would leave a typed block without the body its content rule requires, is
|
|
165
|
+
`INVALID_OP` naming the field and the rule.
|
|
166
|
+
|
|
167
|
+
`GET /api/v1/artifacts/<artifact UUID>/blocks` includes a full-state `token` on every block, including
|
|
168
|
+
inline marks. To reject a stale edit, pass `precondition` with exactly one of
|
|
169
|
+
`{ document: "<document token>" }` or
|
|
170
|
+
`{ blocks: [{ id: "<block id>", token: "<block token>" }] }`. A document token comes from
|
|
171
|
+
`dispatch_doc_read` or from the previous `dispatch_doc_edit`, whose result carries the token its
|
|
172
|
+
own edit produced: a run of guarded edits passes each result's token to the next one and reads the
|
|
173
|
+
document once, not once per edit. The server resolves the whole batch before
|
|
174
|
+
mutation: a block guard must cover every content block it changes, or Dispatch returns
|
|
175
|
+
`400 INVALID_PRECONDITION` without applying anything. Use a document token for insert and move because they
|
|
176
|
+
depend on document order. A block token lets other sections change concurrently; a new anchored ask or comment
|
|
177
|
+
changes the relevant token. A stale guard returns `409 PRECONDITION_FAILED` with each mismatch and current
|
|
178
|
+
token; Dispatch applies no part of that batch. It is the hashline `#TAG` property applied to stable block ids,
|
|
179
|
+
not line numbers: canonical Markdown lines shift under concurrent edits and rendering changes, while block ids
|
|
180
|
+
survive moves and retyping.
|
|
181
|
+
|
|
182
|
+
A browser editing the same document while your edit is in flight never makes your edit a silent
|
|
183
|
+
no-op. The two changes merge, and a human's deletion of the paragraph you are rewriting wins — but
|
|
184
|
+
you are told. `409 EDIT_LOST_TO_CONCURRENT_CHANGE` means the whole batch was refused and nothing
|
|
185
|
+
was written: re-read the document and decide again, as with `PRECONDITION_FAILED`. A success whose
|
|
186
|
+
`lost_ops` names operations means the version was written and the live document already lacks what
|
|
187
|
+
those operations wrote, because the deletion landed after the version: re-read before building on
|
|
188
|
+
it. `lost_ops: []` is the ordinary outcome, and a result that says it could not confirm the edit
|
|
189
|
+
survived means the room is reloading or holds a tree too deep to read — re-read, and a read that
|
|
190
|
+
answers `DOC_SCHEMA` says which. An operation that only removes text (`delete`, `delete_row`,
|
|
191
|
+
`delete_column`, a `replace` that shortens) is never reported lost: a concurrent deletion cannot
|
|
192
|
+
undo a removal.
|
|
193
|
+
|
|
194
|
+
`retype` turns the paragraph or typed block with `block` into the named typed `type` in place. It keeps the
|
|
195
|
+
block id, keeps a typed block's body, and uses `attributes` for client-owned typed attributes. Use it when
|
|
196
|
+
an existing paragraph is the question that should become a decision.
|
|
197
|
+
|
|
198
|
+
An ask block has two ids: the block id, shown as `:::ask{#<id> …}` in the rendered document and
|
|
199
|
+
taken bare by `move`/`delete` in `block` (the `block:<id>` form is only for `before`/`after`
|
|
200
|
+
anchors), and the ask id, which `dispatch_open_asks`, the dashboard's `?ask=` link, `dispatch_read`
|
|
201
|
+
and `dispatch_comment({ reply_to_ask })` use. They differ; `dispatch://KEY/ask/<block-id>` answers
|
|
202
|
+
`not found`.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Documents: typed blocks, comments, suggestions, and artifacts
|
|
2
|
+
|
|
3
|
+
`skill://dispatch` sends you here when you write a typed block (an `:::ask` or a callout), comment on
|
|
4
|
+
or suggest a change to a document, upload an artifact, request a document's approval, or get
|
|
5
|
+
`DOC_SCHEMA`, `INVALID_ASK_BLOCK` or `DOC_SERVICE_UNAVAILABLE` back. Changing a document's text,
|
|
6
|
+
tables included, is [Editing a document](skill://dispatch/references/document-edits.md).
|
|
7
|
+
|
|
8
|
+
## Typed blocks
|
|
9
|
+
|
|
10
|
+
The server declares typed document blocks at `GET /api/v1/schema/blocks`. Write one only with the
|
|
11
|
+
container-directive form `:::name{#block-id key="value"}` on its own line, ordinary block children,
|
|
12
|
+
and a closing line of as many colons at the same nesting. A typed block directly inside another needs the outer
|
|
13
|
+
one's fence a colon longer (`::::callout{…}` around a `:::callout{…}`), and so does one whose code holds a `:::` line;
|
|
14
|
+
Dispatch writes its fences that way. An unclosed typed block at document level is rejected. An
|
|
15
|
+
opening line that continues a paragraph instead of standing on its own (indented four or more
|
|
16
|
+
columns under the paragraph's text) is refused, naming the line's number and text - `INVALID_OP` on an edit's `markdown` or `with`, `INVALID_MARKDOWN` on any other write - since it
|
|
17
|
+
would be stored as the paragraph's text; start the block on its own line. An opening written inside
|
|
18
|
+
a line is stored as text, and `dispatch_issue` and `dispatch_artifact` quote it back as a
|
|
19
|
+
typed-block opening that is text, not a block. For
|
|
20
|
+
a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing typed block, retain
|
|
21
|
+
its id and every rendered attribute. Never copy an existing block's id into new markdown: an id
|
|
22
|
+
names one block, so an insert, upload or suggestion whose markdown names an id the document holds
|
|
23
|
+
outside the text it replaces is refused naming the id: `INVALID_OP` for an insert,
|
|
24
|
+
`INVALID_MARKDOWN` for any other write. To rewrite such a block whole, `delete` it and then
|
|
25
|
+
`insert` the new one carrying its id, anchored on the block before or after it, in that order and
|
|
26
|
+
in one batch: an insert carrying an id the document still holds is refused. An `ask` block whose ask
|
|
27
|
+
is still open cannot be rewritten that way: the tools refuse the `delete`, so reword it with
|
|
28
|
+
`replace`, relocate it with `move`, or change its question, options, urgency or `multiple` with
|
|
29
|
+
`dispatch_edit_ask` if you asked it ([Editing a document](skill://dispatch/references/document-edits.md)).
|
|
30
|
+
|
|
31
|
+
Use only the type names, content rule, attributes, and enum values returned by the schema. Values are
|
|
32
|
+
quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::: {.callout}`, leaf
|
|
33
|
+
`::name` directives, or text `:name` directives; those strings are literal when quoted inside a code
|
|
34
|
+
block. Do not set attributes the schema marks `server: true`; the server ignores them and reasserts
|
|
35
|
+
its authoritative value at settlement.
|
|
36
|
+
|
|
37
|
+
Questions about a document must be `ask` blocks, never an `Open questions` prose section. An ask
|
|
38
|
+
body is one or more question paragraphs followed by an optional bullet list of options, where each
|
|
39
|
+
item is `Label: description`. A spec, an uploaded document or an uploaded version holding an ask that
|
|
40
|
+
breaks that shape - a code block, heading or quote in it, a paragraph after its options, a second
|
|
41
|
+
list - is refused with `INVALID_ASK_BLOCK`; so is an edit that writes one. An ask someone left
|
|
42
|
+
unreadable in the browser refuses nothing it is carried through unchanged by. For example:
|
|
43
|
+
|
|
44
|
+
```md
|
|
45
|
+
:::ask{urgency="high" multiple="false"}
|
|
46
|
+
Today's release is blocked by a database migration. The maintenance window closes in two hours;
|
|
47
|
+
whether production data needs an index rebuild is unknown. How should we complete the migration?
|
|
48
|
+
Recommendation: rehearse on a production snapshot, then apply in the window, because it finds the
|
|
49
|
+
unknown cost before production while keeping today's release possible.
|
|
50
|
+
|
|
51
|
+
- Apply now: Meets today's release, but recovery may be slower if the index rebuild is needed.
|
|
52
|
+
- Rehearse then apply: Costs rehearsal time, but exposes the rebuild and rollback cost before production.
|
|
53
|
+
- Defer the release: Avoids migration risk today, but leaves the release and its fixes unavailable.
|
|
54
|
+
:::
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
When a human answers a decision written as an ask block, the answer lives on that ask. Use
|
|
58
|
+
`dispatch_resolve_ask` when the decision is resolved without a human response, or preserve the
|
|
59
|
+
human's answer; never rewrite the question into its answer or blank its options. An edit that leaves
|
|
60
|
+
an ask block without a question or with a blank option is rejected with `INVALID_ASK_BLOCK`.
|
|
61
|
+
|
|
62
|
+
When `dispatch_issue` or `dispatch_artifact` answers `This spec holds no ask blocks …`, read it as a
|
|
63
|
+
question: either no decision is needed and you say nothing, or you forgot to make it a block. When
|
|
64
|
+
it answers `… typed-block openings in this document are text, not blocks`, the quoted openings are
|
|
65
|
+
blocks stored as prose (inside a line, or a paste with something before every line): fix the markdown
|
|
66
|
+
and upload again; a mention on purpose belongs in code. Neither answer sees a spec wrapped whole in a
|
|
67
|
+
code fence (take the fence off), a malformed opening inside a line (`::ask{`, `:::ask {`: an ask opens
|
|
68
|
+
only as `:::ask{…}` on a line of its own), or any `dispatch_doc_edit`: after an edit that writes an
|
|
69
|
+
ask, `dispatch_doc_read` the section and check it renders as `:::ask{#<id> …}` on its own line.
|
|
70
|
+
|
|
71
|
+
## Comments and suggestions
|
|
72
|
+
|
|
73
|
+
Add feedback with:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask?, turn? })
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It returns issue or project-document owner details plus `comment`; a `reply_to_ask` reply also returns `ask` and `follows: { ask }`,
|
|
80
|
+
because replying to an ask makes you one of its followers (see [Following](skill://dispatch/references/asks.md)).
|
|
81
|
+
`ref` names the owner (an issue or project-document reference) in place of `issue`/`project`.
|
|
82
|
+
`quote` requires `artifact`; its anchor is pinned to the containing block while retaining the quote
|
|
83
|
+
for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
|
|
84
|
+
`quote`; it belongs to its parent's anchor. Reply to any comment in a thread; the server keeps
|
|
85
|
+
threads flat. A reply to a resolved thread reopens it, except a decided suggestion's: a reply joins
|
|
86
|
+
that thread and the suggestion stays accepted or rejected. Use `reply_to_ask` to reply directly under a
|
|
87
|
+
question asked with `dispatch_ask`; `turn` (only with `reply_to_ask`) says who holds the turn after
|
|
88
|
+
the reply — `agent` for a progress note that keeps the ask waiting on you, `human` (the default) when
|
|
89
|
+
the human needs to act; see "Asking" in `skill://dispatch`. Comments are edited only by their author from the
|
|
90
|
+
dashboard. A delivered `comment.created` event carries the comment `id`; reply to it with
|
|
91
|
+
`dispatch_comment({ reply_to: <id> })`.
|
|
92
|
+
|
|
93
|
+
Resolve your own review comment once you have addressed it:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
dispatch_resolve_comment({ comment })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`comment` is the comment id, or a `dispatch://KEY/comment/<id>` or `dispatch://PROJECT/artifact/<slug>/comment/<id>` reference (an
|
|
100
|
+
8+ character id prefix is resolved against the owner's comments). It returns the owner details plus `comment`, and takes no reason —
|
|
101
|
+
say what you did in a `reply_to` first if the thread needs it. The server lets any session or human resolve any open comment, so
|
|
102
|
+
resolve only threads you opened or were asked to close; reopening a resolved thread is human-only (from the dashboard), though your
|
|
103
|
+
reply to it reopens it - except a decided suggestion's, which your reply joins and leaves accepted or rejected. A reply to a pending
|
|
104
|
+
suggestion whose text an edit removed lands as well. Asks are closed with `dispatch_resolve_ask` instead.
|
|
105
|
+
|
|
106
|
+
An exact replacement for document text is a suggestion (`dispatch_suggest`), never a comment; a
|
|
107
|
+
comment is for a question or a note the human answers in words. A human accepts a suggestion with
|
|
108
|
+
one click and cannot accept a comment, so propose the replacement instead of describing it:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
dispatch_suggest({ issue?, project?, artifact, ref?, quote, replace_with, body?, occurrence? })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It returns issue or project-document owner details plus `comment`. A human accepts or rejects a suggestion.
|
|
115
|
+
Errors: `TARGET_AMBIGUOUS` (add `occurrence`), `TARGET_NOT_FOUND` (re-read first), `INVALID_ANCHOR`/`ANCHOR_MISSING`/`ANCHOR_ORPHANED`
|
|
116
|
+
(bad, unwritten, or stale quote), `INVALID_MARKDOWN`/`DOC_SCHEMA` (malformed content), `CAP_EXCEEDED`, `ISSUE_CLOSED`.
|
|
117
|
+
Suggest only what the quoted block can hold. The accept, not the suggestion, checks that: an
|
|
118
|
+
accept whose `replace_with` would break an ask block that was readable before it is refused with
|
|
119
|
+
`INVALID_ASK_BLOCK` - a question given a code block, text after an ask's options, a second option
|
|
120
|
+
list, or an emptied question; one that names an id the document holds outside the text it
|
|
121
|
+
replaces, an ask under a held id included, with `INVALID_MARKDOWN`; and one no part of the
|
|
122
|
+
document can hold where it sits (a code block over a table cell's whole text), or whose quote runs
|
|
123
|
+
into an ask or callout from the text before it, with `INVALID_OP`. Either changes nothing: the human sees the reason with no Retry, and the suggestion
|
|
124
|
+
stays open until someone rejects or replaces it.
|
|
125
|
+
|
|
126
|
+
## Artifacts
|
|
127
|
+
|
|
128
|
+
Attach an image, diagram, or local file with:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
dispatch_artifact({ issue?, project?, name, path, summary? })
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Or, when the text is already in the call, post a Markdown document directly:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
dispatch_artifact({ issue?, project?, name: "load-test-results.md", content: "# Load test\n..." })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
|
|
141
|
+
Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact` and `version`.
|
|
142
|
+
Uploading the same `name` creates its next version — so uploading `spec.md` **replaces the issue's own specification**
|
|
143
|
+
with your text. Never do that: the spec is edited in place with `dispatch_doc_edit` (see [Editing a document](skill://dispatch/references/document-edits.md)). Address an existing
|
|
144
|
+
artifact by the slug shown in the upload result or by its filename, and a project document by its artifact id, slug, or filename; the
|
|
145
|
+
slug also arrives on `artifact.created` events. Dispatch suffixes a slug two documents would share, so one document's
|
|
146
|
+
filename can be another's slug (`plan v2` takes `plan-v2`, then a document named `plan-v2` takes `plan-v2-2`): a bare
|
|
147
|
+
`artifact` that names both is refused with each one's id, while a `dispatch://` reference's document part is always the slug.
|
|
148
|
+
|
|
149
|
+
A picture meant to be seen inline in a message, comment or ask is not uploaded with `dispatch_artifact`: pass its local
|
|
150
|
+
path in that tool's `images` (PNG, JPEG, GIF or WebP by its bytes, at most 25 MiB each). Each is uploaded to the issue (a
|
|
151
|
+
project document's project; for a reply to a direct message, your own conversation) and appended after a blank line as
|
|
152
|
+
one `` line, about 60 characters that count toward the body or
|
|
153
|
+
question cap; a call the lines would carry over the cap is refused with the count before anything uploads, and again,
|
|
154
|
+
with nothing posted, in the rare case where Dispatch gives an upload a longer address than that (a later version of a
|
|
155
|
+
name the owner already holds, or a slug another file took first). The `@vN` pins the version the upload made (the same
|
|
156
|
+
file name again makes the next version). A plain `dispatch://KEY/artifact/<slug>` reference to an image stays a link
|
|
157
|
+
with a thumbnail.
|
|
158
|
+
|
|
159
|
+
`dispatch_read` hands you the pictures the messages, asks and comments it shows embed, newest first, at most 8 and
|
|
160
|
+
10 MiB per read, and its `Pictures:` section names each one shown, in order, and the rest by reference for
|
|
161
|
+
`dispatch_doc_read`, which shows one picture (at most 3,750,000 bytes, the 5 MB of base64 a model provider takes; a
|
|
162
|
+
larger one or an SVG is described). A picture in a
|
|
163
|
+
conversation on the Agents page belongs to that conversation, `dispatch://agent/<session id>/artifact/<slug>@vN`, and
|
|
164
|
+
`dispatch_doc_read` reads it by that reference alone. On Oh My Pi an Inbox delivery carries its pictures the same way; in
|
|
165
|
+
Claude Code it stays text with the reference.
|
|
166
|
+
|
|
167
|
+
A session is shown each picture once. A later read or delivery that embeds a picture you were already shown names it in
|
|
168
|
+
`Pictures:` as `shown earlier this session` and sends no bytes, since every request carries the session's whole history
|
|
169
|
+
and a model provider refuses one over 32 MB; `dispatch_doc_read` always shows the picture, so ask it for one you need
|
|
170
|
+
again, after compaction too.
|
|
171
|
+
|
|
172
|
+
Documents are CommonMark. A bare `<https://example.com|text>` is a CommonMark autolink and is normalised: the angle brackets are
|
|
173
|
+
dropped and the URL keeps `|text`. A backslash-escaped `\<https://example.com|text>` displays as `<https://example.com|text>` in the
|
|
174
|
+
document but comes back re-escaped (`\<`) from `dispatch_doc_read`. A Slack mrkdwn draft, or any other payload that is not Markdown,
|
|
175
|
+
still belongs inside a fenced code block, where it survives verbatim both ways.
|
|
176
|
+
|
|
177
|
+
A table cell ends at every `|` not written `\|`, inside inline code and links too, so write
|
|
178
|
+
`` `x: Promise<void> \| undefined` ``, never `` `x: Promise<void> | undefined` ``, in a cell. A row
|
|
179
|
+
holding text in a cell past its table's width is refused rather than stored without it, naming the
|
|
180
|
+
row: write a `|` inside a cell as `\|`, or, where the row really has more cells, give the header and
|
|
181
|
+
delimiter rows as many. Blank cells past the width are dropped, on every path. A spec, an upload or
|
|
182
|
+
a version answers `INVALID_MARKDOWN`, an insert of blocks `INVALID_OP` on `markdown`, and an insert
|
|
183
|
+
of bare table rows `TABLE_WIDTH`, which names the cell counts only.
|
|
184
|
+
|
|
185
|
+
## Approval requests
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
dispatch_request_approval({ issue?, project?, artifact?, summary })
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The call opens an approval ask with the options `Approve` and `Request changes`, its question
|
|
192
|
+
"Approve spec.md (version N)?" followed by `summary`, where N is the document's latest version. It
|
|
193
|
+
is refused, with nothing sent, while that version holds a decision block open, and the refusal
|
|
194
|
+
names each block and its ask. An answer or a `dispatch_resolve_ask` closes the ask at once but
|
|
195
|
+
reaches a version only when the document settles, about two seconds later, or with your next
|
|
196
|
+
`dispatch_doc_edit`: fold the answer into the text (or, for a waiver, write the human's decision
|
|
197
|
+
in) before you request. A block written in the last few seconds counts as open before Dispatch has
|
|
198
|
+
opened its ask. A document holds one open request. A new version moves it to that version, keeping
|
|
199
|
+
its thread and summary, and leaves it waiting on you, as a human's reply in its thread does; a move
|
|
200
|
+
your own edit made sends you no event, and `dispatch_read` and `dispatch_doc_read` show it as
|
|
201
|
+
`Approval: awaiting, waiting on agent`. Call again when "Approval of a spec" in `skill://dispatch`
|
|
202
|
+
allows: that hands the same request back to the human, reworded first when `summary` is new. Your
|
|
203
|
+
own reply in its thread hands it back too, with no call and the summary it already holds, unless
|
|
204
|
+
you post it with `reply_to_ask` and `turn: "agent"` (`reply_to` carries no turn, so its reply takes
|
|
205
|
+
the default, `human`) or a new version has moved the request since your last
|
|
206
|
+
`dispatch_request_approval`. While it waits on the human, a call with the same `summary` changes
|
|
207
|
+
nothing, and one with a different `summary` is refused, since it would rewrite the card they are
|
|
208
|
+
reading. The answer reaches you as `artifact.approved` or `artifact.changes_requested` with the
|
|
209
|
+
pinned `version` and closes the request, so the next call opens a new one; `changes_requested`
|
|
210
|
+
carries the reason, which is your next piece of work. Those reads show the document's approval
|
|
211
|
+
state; `stale` means it was approved and then edited.
|
|
212
|
+
|
|
213
|
+
## A document that is reloading
|
|
214
|
+
|
|
215
|
+
These calls can answer `DOC_SERVICE_UNAVAILABLE` (HTTP 503), because each writes a document inside its
|
|
216
|
+
transaction: `dispatch_doc_edit`; `dispatch_ask` and `dispatch_comment` on a quote; a `dispatch_comment` reply
|
|
217
|
+
in a thread whose first comment is anchored; `dispatch_suggest`; `dispatch_resolve_comment` on an anchored
|
|
218
|
+
comment; `dispatch_artifact` replacing a document that already exists; and, on an ask that lives in a `:::ask`
|
|
219
|
+
block, `dispatch_edit_ask` and `dispatch_resolve_ask`, which write that block. Creating an issue with a spec,
|
|
220
|
+
uploading a new document, `dispatch_request_approval` and `dispatch_message` never answer it, and neither do
|
|
221
|
+
`dispatch_edit_ask` and `dispatch_resolve_ask` on an ask that has no block. It means that document's live room
|
|
222
|
+
failed and is reloading from its durable copy, so the server refused rather than wait for it; your call wrote
|
|
223
|
+
nothing and the document is intact. Nothing retries it for you: the Dispatch client hands a 503 straight back.
|
|
224
|
+
Wait a few seconds and make the same call again. A second refusal in a row is worth telling your human about,
|
|
225
|
+
with the document's reference.
|
|
226
|
+
|
|
227
|
+
`dispatch_doc_read` can answer it too, though it writes nothing: a read never opens a live room, and waits out
|
|
228
|
+
a room that is reloading, so it is refused only when the document's durable copy cannot be read or decoded.
|
|
229
|
+
Retry it the same way.
|
|
230
|
+
|