@sjawhar/opencode-legion-envoy 3.1.3 → 3.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "3.1.3",
3
+ "version": "3.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -298,10 +298,22 @@ production import). Every `dispatch_ask` passes four gates first:
298
298
  deletion or exposure of production data, anything that reaches a customer). The PO takes those
299
299
  to Sami as a Dispatch ask; you do not open one yourself, even as a permission ask under gate 2
300
300
  (Sami, 2026-09-25, AGENTC-34 §12).
301
- 2. **Is there genuine uncertainty?** If not, it is a plan you execute. The one legitimate ask
302
- without uncertainty is permission for an action only a human can authorise — a production
303
- write, an external send, a console action — and then the question is that action in one
304
- sentence, with options that name its outcomes (below).
301
+ 2. **Is there genuine uncertainty, and have you measured what you can?** If there is none, it is
302
+ a plan you execute. The one legitimate ask without uncertainty is permission for an action
303
+ only a human can authorise — a production write, an external send, a console action — and then
304
+ the question is that action in one sentence, with options that name its outcomes (below).
305
+ Measure before you write: how many are affected, whether anything reaches the path, what the
306
+ current state already is. The measurement decides whether a human is needed at all, and when
307
+ one is, it turns a research request he cannot answer into a decision he can — an ask whose
308
+ lead was "accept this or build a workaround", with nobody knowing whether anyone was affected,
309
+ was unanswerable until a measurement showed the usage could not be observed at all; the same
310
+ ask, carrying that and the size of the affected population, was answered at once, and another
311
+ lost an option outright when the measurement showed it could not repair most of the affected.
312
+ Report what the measurement could **not** establish, with its own control: "I found no
313
+ evidence" and "there is no evidence to find" read alike and mean opposite things, and a
314
+ control that shares the query's blind spot proves neither. Before you say you are waiting on
315
+ him, run `dispatch_open_asks` (below) — and the test for a new ask is not whether you asked on
316
+ this issue before, but whether it asks him to re-report something he has already answered.
305
317
  3. **Can someone who has not read the code answer it on a phone?** Write it as
306
318
  [Writing for the human](#writing-for-the-human) says — who can do what today and what changes
307
319
  for them, then two options with what each costs and your recommendation — and no slice or
@@ -315,10 +327,10 @@ production import). Every `dispatch_ask` passes four gates first:
315
327
  it nor build it — fixing the cause of the thing he named is delivering what he asked for, and
316
328
  is not what this forbids; changing something else is. An ask that turns his complaint about
317
329
  one control into a choice about another does not address what he asked, and changing that
318
- other control is a change he never asked for. One thing is still an ask the moment you know
319
- it, even before delivery: a
320
- credential, an approval, a setting or a console action only he can take that the delivery
321
- waits on — see "Anything that needs the human is an ask", further down.
330
+ other control is a change he never asked for. What is still an ask the moment you know it,
331
+ even before delivery, is anything "Anything that needs the human is an ask" (further down)
332
+ lists that the delivery waits on — including a conflict between what he asked for and another
333
+ of his rules, which this gate would otherwise bury as settled.
322
334
 
323
335
  The platform PO audits open asks. One that fails a gate — or that points at another message in
324
336
  prose instead of carrying its content (below) — is retracted, with the PO's answer as the record.
@@ -541,124 +553,11 @@ It returns live or versioned markdown with open marks. A live read ends with a d
541
553
  omitted `artifact` reads the issue specification; a project needs `artifact`; and a
542
554
  `dispatch://PROJECT/artifact/<document-ref>` ref supplies both, where `document-ref` is the id, slug, or filename.
543
555
 
544
- ```ts
545
- dispatch_doc_edit({ issue?, project?, artifact, ops, precondition?, summary? })
546
- ```
547
- It returns issue or project-document owner details plus `applied`, optional `version`, `changed`, and
548
- `unchanged_ops`. `ops` is an array of this exact `EditOp` shape:
549
-
550
- ```ts
551
- type EditOp = {
552
- op: "replace" | "delete" | "insert" | "retype" | "move" | "delete_row" | "delete_column";
553
- find?: string;
554
- with?: string;
555
- occurrence?: number;
556
- markdown?: string;
557
- after?: string;
558
- before?: string;
559
- block?: string;
560
- index?: number;
561
- type?: string;
562
- attributes?: Record<string, unknown>;
563
- };
564
- ```
556
+ Editing one is [Editing a document](references/document-edits.md): the shape of `dispatch_doc_edit`,
557
+ how to quote the text you mean, one `replace` per paragraph, preconditions against a stale edit, and
558
+ what each operation costs a block's id and its anchors.
565
559
 
566
- An operation takes only these keys. A key it does not declare is refused before the call leaves
567
- your process, naming the operation and its keys, because every key but `op` is optional: a
568
- misspelled `with` would otherwise be dropped and the `replace` would delete the text you meant to
569
- rewrite.
570
-
571
- Target `replace`, `delete`, and quote insert anchors by a block's text as rendered: write inline
572
- code without backticks, bold without asterisks, and link text without link syntax. A table-cell
573
- anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay
574
- within one textblock; split changes that span separate blocks into separate operations.
575
-
576
- `replace` requires `find` and `with`; `delete` requires `find` or `block`; `insert` requires `markdown` and exactly one of `after` or
577
- `before`; `move` requires `block` and exactly one of `after` or `before`; and `delete_row` / `delete_column` each require a table
578
- `block` plus a zero-based `index`. An insert or move anchor is a quote, `"start"`, `"end"`, `"heading:Title"`, or `"block:<id>"`.
579
- Ordinary inserts create a sibling block before or after the quote, heading, or block's enclosing document block, and a move lands the
580
- block at that same boundary; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no header or
581
- delimiter rows) extends that table before or after the matched row instead; short rows are padded, wider rows are rejected, and deleting
582
- a cell's quoted text removes only that text. `delete_row` / `delete_column` instead mutate their named table in place, keeping the
583
- 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
584
- 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
585
- `INVALID_OP` on `index`, naming the supplied value and the table's actual dimensions before making any change. Markdown parsing
586
- canonicalizes short ragged rows by padding missing cells, so column deletion preserves every non-selected cell in the canonical table.
587
- `GET /api/v1/artifacts/<artifact UUID>/blocks` reports a table's own references plus its descendant cell anchors. A row or column
588
- deletion that would remove an open ask or unresolved comment anchor is `INVALID_OP` on `index`, naming the axis and anchor ids;
589
- answered asks and resolved comments are history and do not block it. A `find` or quote anchor tolerates inline Markdown
590
- (`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the quote and the three nearest blocks so
591
- the next quote lands, and a `find` cut before a closing `**` or `` ` `` is refused as an unbalanced inline mark rather than reported
592
- 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
593
- the nearest headings. `replace` is inline: `with` is the new text of the matched span inside its block, so a marker of a *different*
594
- kind from the block's own (`4. Design` written into a heading, `# Title` into a paragraph) stays literal text and never turns the
595
- 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
596
- is rejected (`INVALID_OP` on `with`) — including prose that merely looks like a marker (`1999. was a year` into an ordered item),
597
- 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`
598
- plus `delete` to change the block's kind, level or number. The one exception is a heading rename whose `find` carried a heading
599
- marker: `replace(find="## Old", with="## New")` gives `## New`. A different level in `with` applies only when `find` named the
600
- heading's actual level — `find="## Old"`, `with="### New"` retitles and makes it an h3 — because `# ` is the level-blind selector,
601
- so `find="# Old"` renames the text and keeps whatever level it selected. `with` that forms more than one
602
- paragraph is rejected (`INVALID_OP` on `with`) — see the recipe for a multi-paragraph rewrite below; so is any non-empty `with` that
603
- renders to no text, which a line indented four spaces or a tab does (markdown reads that as a code block), as does whitespace
604
- alone. An empty `with` is the one that deletes the matched text on purpose. Use zero-based `occurrence` for a
605
- repeated target; re-read a missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
606
-
607
- **Rewriting several paragraphs is one `replace` per paragraph, then a read-back.** `replace` is inline:
608
- each `with` is the new text of one paragraph, and a `with` that forms two paragraphs is refused whatever the
609
- text says. Give each paragraph you rewrite its own `replace`, which keeps that paragraph's block id and every
610
- anchor outside the text you rewrite. A comment or ask anchored to the text you rewrite loses its quote but keeps
611
- its pin to the block, so the dashboard still shows it beside that paragraph; a delete (below) loses both. When
612
- the new text has more paragraphs than the old, `insert` the extra ones
613
- with `after` quoting the last paragraph you rewrote exactly as it now reads (the operations in one batch
614
- apply in order); they land after the top-level block that holds the quote, so beside a paragraph inside a
615
- list item or a typed block they go after the whole list or block. When it has fewer, `delete` each leftover
616
- paragraph, with its whole text as `find` or its id as `block`. All of that holds while the new text is
617
- paragraphs: `replace` keeps a block's kind, so a heading, list, table or code fence cannot be replaced into place —
618
- a heading, list or table marker is written as literal text, and a code fence becomes an inline code span with the
619
- fence gone. When the new text adds one beside paragraphs, `insert` it beside the paragraph you replaced, which keeps
620
- that paragraph's id; only when no paragraph of the new text is left to take the old block's place is it an `insert`
621
- of the new block plus a `delete` of the old, and a delete is what costs a block its id. Then read the document back with
622
- `dispatch_doc_read` and read the passage and its neighbours, not a grep for the words you added: an empty
623
- `with` deletes the matched text on purpose, so a `replace` whose `with` you meant to fill empties that
624
- paragraph — the block and its id stay, holding nothing — and only a read shows what the document now says.
625
-
626
- A batch that leaves the document's semantic identity unchanged — including its inline anchor marks, so an edit that only orphans a
627
- comment or ask anchor still mints its version — mints no version, named or not: the response carries
628
- `changed: false` with `unchanged_ops` naming each operation that did nothing, and the tool result says nothing changed. A `summary`
629
- does not force a version for such a batch; `POST /api/v1/artifacts/<id>/versions`, which names the current state on purpose, still does.
630
-
631
- A `delete` whose `find` is a block's entire text removes the block itself — the bullet, paragraph, or heading, not just its words — and
632
- 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
633
- 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
634
- (paragraphs, code, tables) is refused with `INVALID_OP` naming `delete {block:"<item id>"}`, which removes the item with its content.
635
- `delete` with `block` removes any block by id (paragraph, heading, list, list item, table, or typed block; deleting an open `ask` block
636
- retracts its ask, while an answered one keeps its answer as the record), and `move` with `block` relocates one, keeping its id and
637
- attributes — a moved `ask` keeps its ask and answer. Block ids are the `#id` a typed block renders
638
- (`:::ask{#5467e5ce-…}`) and, for every block including untyped ones, the `id` rows from
639
- `GET /api/v1/artifacts/<artifact UUID>/blocks` (or `/api/v1/issues/{key}/artifacts/{slug}/blocks`), each with its `type` and byte range
640
- 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
641
- an earlier `delete {block}` fails as `INVALID_OP` naming the earlier operation and the parent block that cascaded the removal. A move
642
- whose anchor lies inside the moved block, or a delete that would leave a typed block without the body its content rule requires, is
643
- `INVALID_OP` naming the field and the rule.
644
-
645
- `GET /api/v1/artifacts/<artifact UUID>/blocks` includes a full-state `token` on every block, including
646
- inline marks. To reject a stale edit, pass `precondition` with exactly one of
647
- `{ document: "<token from dispatch_doc_read>" }` or
648
- `{ blocks: [{ id: "<block id>", token: "<block token>" }] }`. The server resolves the whole batch before
649
- mutation: a block guard must cover every content block it changes, or Dispatch returns
650
- `400 INVALID_PRECONDITION` without applying anything. Use a document token for insert and move because they
651
- depend on document order. A block token lets other sections change concurrently; a new anchored ask or comment
652
- changes the relevant token. A stale guard returns `409 PRECONDITION_FAILED` with each mismatch and current
653
- token; Dispatch applies no part of that batch. It is the hashline `#TAG` property applied to stable block ids,
654
- not line numbers: canonical Markdown lines shift under concurrent edits and rendering changes, while block ids
655
- survive moves and retyping.
656
-
657
- `retype` turns the paragraph or typed block with `block` into the named typed `type` in place. It keeps the
658
- block id, keeps a typed block's body, and uses `attributes` for client-owned typed attributes. Use it when
659
- an existing paragraph is the question that should become a decision.
660
-
661
- ### A document that is reloading
560
+ ## A document that is reloading
662
561
 
663
562
  These calls can answer `DOC_SERVICE_UNAVAILABLE` (HTTP 503), because each writes a document inside its
664
563
  transaction: `dispatch_doc_edit`; `dispatch_ask` and `dispatch_comment` on a quote; a `dispatch_comment` reply
@@ -678,7 +577,10 @@ The server declares typed document blocks at `GET /api/v1/schema/blocks`. Write
678
577
  container-directive form `:::name{#block-id key="value"}` on its own line, ordinary block children,
679
578
  and a closing `:::` at the same nesting. An unclosed typed block at document level is rejected. For
680
579
  a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing typed block, retain
681
- its id and every rendered attribute.
580
+ its id and every rendered attribute. Never copy an existing block's id into new markdown: an id
581
+ names one block, so an insert, upload or suggestion whose markdown names an id the document holds
582
+ outside the text it replaces is refused naming the id: `INVALID_OP` for an insert,
583
+ `INVALID_MARKDOWN` for any other write.
682
584
 
683
585
  Use only the type names, content rule, attributes, and enum values returned by the schema. Values are
684
586
  quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::: {.callout}`, leaf
@@ -769,7 +671,7 @@ dispatch_artifact({ issue?, project?, name: "load-test-results.md", content: "#
769
671
  Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
770
672
  Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact` and `version`.
771
673
  Uploading the same `name` creates its next version — so uploading `spec.md` **replaces the issue's own specification**
772
- with your text. Never do that: the spec is edited in place with `dispatch_doc_edit` (see [The Spec](#the-spec)). Address an existing
674
+ with your text. Never do that: the spec is edited in place with `dispatch_doc_edit` (see [Editing a document](references/document-edits.md)). Address an existing
773
675
  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
774
676
  slug also arrives on `artifact.created` events.
775
677
 
@@ -0,0 +1,142 @@
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](../SKILL.md#the-spec) 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`, and
12
+ `unchanged_ops`. `ops` is an array of this exact `EditOp` shape:
13
+
14
+ ```ts
15
+ type EditOp = {
16
+ op: "replace" | "delete" | "insert" | "retype" | "move" | "delete_row" | "delete_column";
17
+ find?: string;
18
+ with?: string;
19
+ occurrence?: number;
20
+ markdown?: string;
21
+ after?: string;
22
+ before?: string;
23
+ block?: string;
24
+ index?: number;
25
+ type?: string;
26
+ attributes?: Record<string, unknown>;
27
+ };
28
+ ```
29
+
30
+ An operation takes only these keys. A key it does not declare is refused before the call leaves
31
+ your process, naming the operation and its keys, because every key but `op` is optional: a
32
+ misspelled `with` would otherwise be dropped and the `replace` would delete the text you meant to
33
+ rewrite.
34
+
35
+ Target `replace`, `delete`, and quote insert anchors by a block's text as rendered: write inline
36
+ code without backticks, bold without asterisks, and link text without link syntax. A table-cell
37
+ anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay
38
+ within one textblock; split changes that span separate blocks into separate operations.
39
+
40
+ `replace` requires `find` and `with`; `delete` requires `find` or `block`; `insert` requires `markdown` and exactly one of `after` or
41
+ `before`; `move` requires `block` and exactly one of `after` or `before`; and `delete_row` / `delete_column` each require a table
42
+ `block` plus a zero-based `index`. An insert or move anchor is a quote, `"start"`, `"end"`, `"heading:Title"`, or `"block:<id>"`.
43
+ Ordinary inserts create a sibling block before or after the quote, heading, or block's enclosing document block, and a move lands the
44
+ block at that same boundary; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no header or
45
+ delimiter rows) extends that table before or after the matched row instead; short rows are padded, wider rows are rejected, and deleting
46
+ a cell's quoted text removes only that text. `delete_row` / `delete_column` instead mutate their named table in place, keeping the
47
+ 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
48
+ 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
49
+ `INVALID_OP` on `index`, naming the supplied value and the table's actual dimensions before making any change. Markdown parsing
50
+ canonicalizes short ragged rows by padding missing cells, so column deletion preserves every non-selected cell in the canonical table.
51
+ `GET /api/v1/artifacts/<artifact UUID>/blocks` reports a table's own references plus its descendant cell anchors. A row or column
52
+ deletion that would remove an open ask or unresolved comment anchor is `INVALID_OP` on `index`, naming the axis and anchor ids;
53
+ answered asks and resolved comments are history and do not block it. A `find` or quote anchor tolerates inline Markdown
54
+ (`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the quote and the three nearest blocks so
55
+ the next quote lands, and a `find` cut before a closing `**` or `` ` `` is refused as an unbalanced inline mark rather than reported
56
+ 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
57
+ the nearest headings. `replace` is inline: `with` is the new text of the matched span inside its block, so a marker of a *different*
58
+ kind from the block's own (`4. Design` written into a heading, `# Title` into a paragraph) stays literal text and never turns the
59
+ 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
60
+ is rejected (`INVALID_OP` on `with`) — including prose that merely looks like a marker (`1999. was a year` into an ordered item),
61
+ 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`
62
+ plus `delete` to change the block's kind, level or number. The one exception is a heading rename whose `find` carried a heading
63
+ marker: `replace(find="## Old", with="## New")` gives `## New`. A different level in `with` applies only when `find` named the
64
+ heading's actual level — `find="## Old"`, `with="### New"` retitles and makes it an h3 — because `# ` is the level-blind selector,
65
+ so `find="# Old"` renames the text and keeps whatever level it selected. `with` that forms more than one
66
+ paragraph is rejected (`INVALID_OP` on `with`) — see the recipe for a multi-paragraph rewrite below; so is any non-empty `with` that
67
+ renders to no text, which a line indented four spaces or a tab does (markdown reads that as a code block), as does whitespace
68
+ alone. An empty `with` is the one that deletes the matched text on purpose. Use zero-based `occurrence` for a
69
+ repeated target; re-read a missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
70
+
71
+ **Rewriting several paragraphs is one `replace` per paragraph, then a read-back.** `replace` is inline:
72
+ each `with` is the new text of one paragraph, and a `with` that forms two paragraphs is refused whatever the
73
+ text says. Give each paragraph you rewrite its own `replace`, which keeps that paragraph's block id and every
74
+ anchor outside the text you rewrite. A comment or ask anchored to the text you rewrite loses its quote but keeps
75
+ its pin to the block, so the dashboard still shows it beside that paragraph; a delete (below) loses both. An
76
+ anchor that straddles the boundary keeps its mark over the words you left alone, with its quote shortened to
77
+ them: rewriting `charlie delta.` under a comment on `bravo charlie` leaves that comment quoting `bravo `. When
78
+ the new text has more paragraphs than the old, write them as **one** `insert` whose `markdown` holds them all,
79
+ anchored on the last paragraph you rewrote with `after` quoting it exactly as it now reads (the operations in
80
+ one batch apply in order): each separate `insert` at the same anchor lands directly after it, so two of them
81
+ come out in the reverse of the order you wrote. An insert lands after the top-level block that holds the quote,
82
+ so beside a paragraph inside a list item or a typed block it goes after the whole list or block. When it has fewer,
83
+ `delete` each leftover paragraph, with its whole text as `find` or its id as `block`. All of that holds while
84
+ the new text is paragraphs: `replace` keeps a block's kind, so any block that is not a paragraph — a heading,
85
+ list, table, blockquote, rule, code fence or typed block — cannot be replaced into place. Its marker is written
86
+ as literal text, except a backtick fence, which becomes an inline code span whose text is everything inside it,
87
+ info string and line breaks included (```` ```go\nx := 1``` ```` becomes the code span `go` + a line break +
88
+ `x := 1`), and a tilde fence, which stays literal.
89
+
90
+ HTML is not written as text at all. A `with` whose HTML markdown reads as a block (`<div>x</div>`,
91
+ `<!-- note -->`) is stored as inline HTML, and `dispatch_doc_read` then returns it raw, in markdown Dispatch
92
+ will not take back: the schema carries no block HTML, so an `insert` or an upload of that markdown is refused.
93
+ Keep HTML inside a line.
94
+
95
+ When the new text adds a block that is not a paragraph beside paragraphs, `insert` it beside the
96
+ paragraph you replaced, which keeps that paragraph's id; only when no paragraph of the new text is left to take
97
+ the old block's place is it an `insert` of the new block plus a `delete` of the old, and the delete is what
98
+ costs the id (below). Then read the document back with
99
+ `dispatch_doc_read` and read the passage and its neighbours, not a grep for the words you added: an empty
100
+ `with` deletes the matched text on purpose, so a `replace` whose `with` you meant to fill empties that
101
+ paragraph — the block and its id stay, holding nothing — and only a read shows what the document now says.
102
+
103
+ A batch that leaves the document's semantic identity unchanged — including its inline anchor marks, so an edit that only orphans a
104
+ comment or ask anchor still mints its version — mints no version, named or not: the response carries
105
+ `changed: false` with `unchanged_ops` naming each operation that did nothing, and the tool result says nothing changed. A `summary`
106
+ does not force a version for such a batch; `POST /api/v1/artifacts/<id>/versions`, which names the current state on purpose, still does.
107
+
108
+ A `delete` whose `find` is a block's entire text removes the block itself — the bullet, paragraph, or heading, not just its words — and
109
+ 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
110
+ 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
111
+ (paragraphs, code, tables) is refused with `INVALID_OP` naming `delete {block:"<item id>"}`, which removes the item with its content.
112
+ `delete` with `block` removes any block by id (paragraph, heading, list, list item, table, or typed block; deleting an open `ask` block
113
+ retracts its ask, while an answered one keeps its answer as the record), and `move` with `block` relocates one, keeping its id and
114
+ 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:
115
+ `delete` by text or by id removes the block and any container it empties; `delete_row` / `delete_column` remove their cells' ids,
116
+ 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
117
+ blockquote or list item removes that emptied container, the list too when no item remains, and each enclosing container that
118
+ held nothing else (`> - Only.` loses the blockquote as well as the item and the list). The moved block itself keeps its id,
119
+ as do `replace` (an empty `with` and a heading-level change included), `insert` and `retype`; `retype` carries the paragraph's id
120
+ onto the typed block it becomes. Block ids are the `#id` a typed block renders
121
+ (`:::ask{#5467e5ce-…}`) and, for every block including untyped ones, the `id` rows from
122
+ `GET /api/v1/artifacts/<artifact UUID>/blocks` (or `/api/v1/issues/{key}/artifacts/{slug}/blocks`), each with its `type` and byte range
123
+ 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
124
+ an earlier `delete {block}` fails as `INVALID_OP` naming the earlier operation and the parent block that cascaded the removal. A move
125
+ whose anchor lies inside the moved block, or a delete that would leave a typed block without the body its content rule requires, is
126
+ `INVALID_OP` naming the field and the rule.
127
+
128
+ `GET /api/v1/artifacts/<artifact UUID>/blocks` includes a full-state `token` on every block, including
129
+ inline marks. To reject a stale edit, pass `precondition` with exactly one of
130
+ `{ document: "<token from dispatch_doc_read>" }` or
131
+ `{ blocks: [{ id: "<block id>", token: "<block token>" }] }`. The server resolves the whole batch before
132
+ mutation: a block guard must cover every content block it changes, or Dispatch returns
133
+ `400 INVALID_PRECONDITION` without applying anything. Use a document token for insert and move because they
134
+ depend on document order. A block token lets other sections change concurrently; a new anchored ask or comment
135
+ changes the relevant token. A stale guard returns `409 PRECONDITION_FAILED` with each mismatch and current
136
+ token; Dispatch applies no part of that batch. It is the hashline `#TAG` property applied to stable block ids,
137
+ not line numbers: canonical Markdown lines shift under concurrent edits and rendering changes, while block ids
138
+ survive moves and retyping.
139
+
140
+ `retype` turns the paragraph or typed block with `block` into the named typed `type` in place. It keeps the
141
+ block id, keeps a typed block's body, and uses `attributes` for client-owned typed attributes. Use it when
142
+ an existing paragraph is the question that should become a decision.
@@ -611,16 +611,22 @@ do:
611
611
  reported. The issue has moved to a newer run since your task was given, so the work you just
612
612
  reported belongs to a run that is over. Nothing you can repeat changes that: stop, push nothing
613
613
  further, and tell the architect what you completed and that its run has been superseded. A task
614
- for the current run arrives in this same session if the phase still needs you.
614
+ for the current run arrives in this same session if the phase still needs you. The same answer
615
+ comes when your turn started before the daemon recorded the current run's task as yours, so it
616
+ still holds you to the earlier run: that task is sent again. When a task arrives, do what it
617
+ asks; if the work it asks for is already committed, call `handoff_complete` again, and never redo
618
+ the work or write a second handoff.
615
619
  - `HANDOFF_NOT_CURRENT_PHASE` — names your role, the issue, and the phase it is in now. The issue
616
620
  has left your phase; report to the architect rather than completing again.
617
621
  - `HANDOFF_NO_RUN` — names neither: it says this claim has taken no task, so the daemon cannot
618
622
  tell which run you are reporting. Your pane is completing outside any assignment. Say so to the
619
623
  architect; do not re-run the phase.
620
624
  - `HANDOFF_ALREADY_RECORDED` — names your role, the phase, the review round and the commit. This
621
- exact call was received before, and its first answer stands — accepted, or one of the refusals
622
- above. Sending it again changes nothing; if you did not see that first answer, tell the
623
- architect so and quote this one.
625
+ exact call was received before, and its first answer stands: accepted, or a refusal the daemon
626
+ records with the call — `HANDOFF_STALE_GENERATION`, `HANDOFF_NOT_CURRENT_PHASE`,
627
+ `READY_REQUIRED` or `HANDOFF_NOT_NEW`. `HANDOFF_NO_RUN` is never that first answer, since it is
628
+ given before anything is recorded. Sending it again changes nothing; if you did not see that
629
+ first answer, tell the architect so and quote this one.
624
630
 
625
631
  Quote the answer verbatim in what you tell the architect: with the run and phase it names, the
626
632
  difference between "my work is lost" and "my work belongs to the previous run" is visible.