@sjawhar/opencode-legion-envoy 3.2.3 → 3.2.5
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/src/server.js
CHANGED
|
@@ -14005,7 +14005,7 @@ var dispatchToolSpecs = [
|
|
|
14005
14005
|
{
|
|
14006
14006
|
name: "dispatch_ask",
|
|
14007
14007
|
example: { issue: "DSP-1", question: "Ship this?" },
|
|
14008
|
-
description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
14008
|
+
description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anything you are blocked on a human for, including a credential or grant to renew, an approval, or a decision, is an ask, never a message. " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
14009
14009
|
arguments: (z2) => ({
|
|
14010
14010
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14011
14011
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14126,7 +14126,7 @@ var dispatchToolSpecs = [
|
|
|
14126
14126
|
{
|
|
14127
14127
|
name: "dispatch_message",
|
|
14128
14128
|
example: { issue: "DSP-1", body: "Implementation started." },
|
|
14129
|
-
description: "Post a note humans must read now: a reply to a human's message
|
|
14129
|
+
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation, and a second call with the same in_reply_to posts nothing because " + "Dispatch keeps the one reply per message. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
|
|
14130
14130
|
arguments: (z2) => ({
|
|
14131
14131
|
issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
|
|
14132
14132
|
body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
|
|
@@ -14142,7 +14142,7 @@ var dispatchToolSpecs = [
|
|
|
14142
14142
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
14143
14143
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
14144
14144
|
},
|
|
14145
|
-
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch_doc_read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
14145
|
+
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - where the block holding it cannot be written without that paragraph, the replace is INVALID_OP and the refusal names the delete that removes it instead. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch_doc_read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
14146
14146
|
arguments: (z2) => ({
|
|
14147
14147
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14148
14148
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14151,7 +14151,7 @@ var dispatchToolSpecs = [
|
|
|
14151
14151
|
ops: z2.array(z2.object({
|
|
14152
14152
|
op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
|
|
14153
14153
|
find: z2.string().describe("Text of the target as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated and must be balanced; a leading '# ' matches a heading. A delete of a block's entire text removes the block.").optional(),
|
|
14154
|
-
with: z2.string().describe("Replacement text for replace, parsed as inline markdown within the matched block; a marker of a different kind from the block's own is literal text, one of the same kind is refused unless it is a heading rename (where a level named by find is what lets with change it), a backslash escape keeps prose that merely looks like a marker, a block marker after a hard line break is refused because replace cannot open a new block, and any non-empty value that renders to no text is refused - only an empty value deletes the match.").optional(),
|
|
14154
|
+
with: z2.string().describe("Replacement text for replace: inside a code block, the code's literal text as sent (line breaks at its end do not survive a read, and a line of three or more colons in code inside a typed block, indented less than four columns from where the typed block's lines start, is refused, since the browser editor ends the typed block there: indent it four or more spaces, or move the code block out); text that would read as block syntax at a line start, such as '---' over a paragraph, is stored escaped and reads back as those characters, so a rule is added with insert beside the paragraph; elsewhere parsed as inline markdown within the matched block; a marker of a different kind from the block's own is literal text, one of the same kind is refused unless it is a heading rename (where a level named by find is what lets with change it), a backslash escape keeps prose that merely looks like a marker, a block marker after a hard line break is refused because replace cannot open a new block, and any non-empty value that renders to no text is refused - only an empty value deletes the match.").optional(),
|
|
14155
14155
|
occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
|
|
14156
14156
|
markdown: z2.string().describe("Markdown to insert.").optional(),
|
|
14157
14157
|
after: z2.string().describe(`Insert or move after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>", "block:<id>".`).optional(),
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -189,8 +189,8 @@ dashboard's **Unclaimed** filter is how you find work nobody is on.
|
|
|
189
189
|
that session (its id is in the message; `envoy_send` reaches it) or pick up something else,
|
|
190
190
|
and tell the human if you believe the work should be yours. When a **human** holds it, the
|
|
191
191
|
refusal names the person and says nothing about a session running, because there is none to
|
|
192
|
-
message: ask them
|
|
193
|
-
lapsed — only a human releases or forces a human's claim.
|
|
192
|
+
message: ask them with `dispatch_ask` instead, so the open ask appears in their Inbox, and
|
|
193
|
+
never assume their claim has lapsed — only a human releases or forces a human's claim.
|
|
194
194
|
- **`409 CLAIM_CONTENDED` means the issue changed hands twice while your call ran**, so nothing
|
|
195
195
|
was applied and nobody's liveness was checked. Read the issue and decide again; it is not a
|
|
196
196
|
refusal by a live holder.
|
|
@@ -421,17 +421,16 @@ Before saying you are waiting for human input, call `dispatch_open_asks`. With n
|
|
|
421
421
|
|
|
422
422
|
**Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, send a one-line ask before the first implementation commit. A lane's schema decision or a platform-PO contract ruling does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request; it is inferred from AGENTC-186's 2026-09-16 retro (platform PO, 2026-09-17). A control or behaviour the human asked for in words is settled by those words, together with every choice inside it that his words do not make (where it sits, its defaults, its options): build it without an ask, as gate 4 of [Before you ask](#before-you-ask) says. This rule covers only product shape outside what he asked for, and its ask comes before the commit that sets that shape.
|
|
423
423
|
|
|
424
|
-
**Anything
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
exception is a halt condition from [Before you
|
|
428
|
-
platform PO over Envoy instead.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
working on everything that is not.
|
|
424
|
+
**Anything you are blocked on a human for is an open ask.** An agent waits on a human only through
|
|
425
|
+
an open ask. An approval, a credential or grant to renew, a setting only they can change, a review
|
|
426
|
+
click, a decision, or a conflict between two of their own rules: open a `dispatch_ask` the moment
|
|
427
|
+
you know, the action as the question. The exception is a halt condition from [Before you
|
|
428
|
+
ask](#before-you-ask) gate 1, which goes to the platform PO over Envoy instead. Never write it
|
|
429
|
+
into a spec, a comment reply, a message, or a pull-request body: nothing in those paths reaches
|
|
430
|
+
the human's Inbox, and a human who is not reading your document does not know they are the
|
|
431
|
+
blocker. Before asking, try to remove the step: a value already on the machine, a permission you
|
|
432
|
+
already hold, an API that replaces the click. One ask per item, `urgency: "high"` when work is
|
|
433
|
+
stopped on it; while it is open, keep working on everything that is not.
|
|
435
434
|
|
|
436
435
|
A to-do handed to a human is an ordinary question: phrase the to-do as the question and give it
|
|
437
436
|
the options that name its outcomes, in the human's words - there is no fixed vocabulary and the
|
|
@@ -580,7 +579,9 @@ a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing
|
|
|
580
579
|
its id and every rendered attribute. Never copy an existing block's id into new markdown: an id
|
|
581
580
|
names one block, so an insert, upload or suggestion whose markdown names an id the document holds
|
|
582
581
|
outside the text it replaces is refused naming the id: `INVALID_OP` for an insert,
|
|
583
|
-
`INVALID_MARKDOWN` for any other write.
|
|
582
|
+
`INVALID_MARKDOWN` for any other write. To rewrite such a block whole, `delete` it and then
|
|
583
|
+
`insert` the new one carrying its id, anchored on the block before or after it, in that order and
|
|
584
|
+
in one batch: an insert carrying an id the document still holds is refused.
|
|
584
585
|
|
|
585
586
|
Use only the type names, content rule, attributes, and enum values returned by the schema. Values are
|
|
586
587
|
quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::: {.callout}`, leaf
|
|
@@ -653,6 +654,14 @@ dispatch_suggest({ issue?, project?, artifact, ref?, quote, replace_with, body?,
|
|
|
653
654
|
It returns issue or project-document owner details plus `comment`. A human accepts or rejects a suggestion.
|
|
654
655
|
Errors: `TARGET_AMBIGUOUS` (add `occurrence`), `TARGET_NOT_FOUND` (re-read first), `INVALID_ANCHOR`/`ANCHOR_MISSING`/`ANCHOR_ORPHANED`
|
|
655
656
|
(bad, unwritten, or stale quote), `INVALID_MARKDOWN`/`DOC_SCHEMA` (malformed content), `CAP_EXCEEDED`, `ISSUE_CLOSED`.
|
|
657
|
+
Suggest only what the quoted block can hold. The accept, not the suggestion, checks that: an
|
|
658
|
+
accept whose `replace_with` would break an ask block that was readable before it is refused with
|
|
659
|
+
`INVALID_ASK_BLOCK` - a question given a code block, text after an ask's options, a second option
|
|
660
|
+
list, or an emptied question; one that names an id the document holds outside the text it
|
|
661
|
+
replaces, an ask under a held id included, with `INVALID_MARKDOWN`; and one no part of the
|
|
662
|
+
document can hold where it sits (a code block over a table cell's whole text), or whose quote runs
|
|
663
|
+
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
|
|
664
|
+
stays open until someone rejects or replaces it.
|
|
656
665
|
|
|
657
666
|
## Artifacts
|
|
658
667
|
|
|
@@ -714,7 +723,7 @@ once.
|
|
|
714
723
|
## Messages
|
|
715
724
|
|
|
716
725
|
Dispatch is a high-signal record for humans, not a log of what you are doing. A message is a reply to a human's message, or a
|
|
717
|
-
change a human must know about now: a deliverable landed
|
|
726
|
+
change a human must know about now: a deliverable landed. Nothing else — no progress updates, no
|
|
718
727
|
"starting X", no "still working", no restating the spec, no status on a timer. Your transcript is where work is narrated; the
|
|
719
728
|
pull request is where it is summarised. One message that a human reads beats ten that train them to skip you.
|
|
720
729
|
|
|
@@ -66,12 +66,23 @@ heading's actual level — `find="## Old"`, `with="### New"` retitles and makes
|
|
|
66
66
|
so `find="# Old"` renames the text and keeps whatever level it selected. `with` that forms more than one
|
|
67
67
|
paragraph is rejected (`INVALID_OP` on `with`) — see the recipe for a multi-paragraph rewrite below; so is any non-empty `with` that
|
|
68
68
|
renders to no text, which a line indented four spaces or a tab does (markdown reads that as a code block), as does whitespace
|
|
69
|
-
alone. An empty `with`
|
|
69
|
+
alone. An empty `with` deletes the matched text on purpose; where the block holding it cannot be written without that
|
|
70
|
+
paragraph, the replace is `INVALID_OP`, and the refusal names the `delete` that removes it instead. Inside a code
|
|
71
|
+
block none of this applies: `with` is the code's literal text, written as sent, whitespace, markdown syntax and
|
|
72
|
+
references included, except that line breaks at the end of the code's text, and a line holding only whitespace in a
|
|
73
|
+
list item's code, do not survive the next read; and a line of three or more colons in code inside a typed block,
|
|
74
|
+
indented less than four columns from where the typed block's lines start, is `INVALID_OP`, since the browser editor
|
|
75
|
+
ends the typed block there - indent it four or more spaces (a tab reaches only the next tab stop, which inside a list
|
|
76
|
+
item or a blockquote can be two columns away), or move the code block out of the typed block. Text a `replace` writes
|
|
77
|
+
that would read as block syntax at a line start is stored escaped and reads back as the characters you sent: `---` over
|
|
78
|
+
a paragraph is stored `\---`, not a rule, so to add a rule, `insert` it beside the paragraph (`insert` with markdown
|
|
79
|
+
`***`). Use zero-based
|
|
80
|
+
`occurrence` for a
|
|
70
81
|
repeated target; re-read a missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
|
|
71
82
|
|
|
72
83
|
**Rewriting several paragraphs is one `replace` per paragraph, then a read-back.** `replace` is inline:
|
|
73
|
-
each `with` is the new text of one paragraph, and a `with` that forms two paragraphs is
|
|
74
|
-
text says. Give each paragraph you rewrite its own `replace`, which keeps that paragraph's block id and every
|
|
84
|
+
each `with` is the new text of one paragraph, and outside a code block a `with` that forms two paragraphs is
|
|
85
|
+
refused whatever the text says. Give each paragraph you rewrite its own `replace`, which keeps that paragraph's block id and every
|
|
75
86
|
anchor outside the text you rewrite. A comment or ask anchored to the text you rewrite loses its quote but keeps
|
|
76
87
|
its pin to the block, so the dashboard still shows it beside that paragraph; a delete (below) loses both. An
|
|
77
88
|
anchor that straddles the boundary keeps its mark over the words you left alone, with its quote shortened to
|
|
@@ -88,18 +99,26 @@ as literal text, except a backtick fence, which becomes an inline code span whos
|
|
|
88
99
|
info string and line breaks included (```` ```go\nx := 1``` ```` becomes the code span `go` + a line break +
|
|
89
100
|
`x := 1`), and a tilde fence, which stays literal.
|
|
90
101
|
|
|
91
|
-
HTML is not written as text at all
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
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
|
|
103
|
+
(`INVALID_OP` on `with`), because the schema carries no block HTML. `<div>x</div>` and `<!-- note -->` open one
|
|
104
|
+
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
|
|
105
|
+
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
|
|
106
|
+
heading is inline HTML and is kept as written.
|
|
107
|
+
|
|
108
|
+
A hard line break in `with` (two trailing spaces or a backslash before a newline) is refused in a heading or a
|
|
109
|
+
table cell (`INVALID_OP` on `with`): both are written on one line, so the break would end the block there. A line
|
|
110
|
+
break inside a code span or inline HTML there is refused the same way. Write the text without the break, or
|
|
111
|
+
`insert` a new block after this one.
|
|
95
112
|
|
|
96
113
|
When the new text adds a block that is not a paragraph beside paragraphs, `insert` it beside the
|
|
97
114
|
paragraph you replaced, which keeps that paragraph's id; only when no paragraph of the new text is left to take
|
|
98
|
-
the old block's place is it
|
|
99
|
-
costs the id (below)
|
|
115
|
+
the old block's place is it a `delete` of the old block and then an `insert` of the new one, anchored on the
|
|
116
|
+
block before or after it. The delete is what costs the id (below); a typed block keeps its id when the insert
|
|
117
|
+
carries it, which works only in that order, because an insert carrying an id the document still holds is refused.
|
|
118
|
+
Then read the document back with
|
|
100
119
|
`dispatch_doc_read` and read the passage and its neighbours, not a grep for the words you added: an empty
|
|
101
|
-
`with` deletes the matched text on purpose, so a `replace` whose `with` you meant to fill
|
|
102
|
-
paragraph — the block and its id stay, holding nothing — and only a read shows what the document now says.
|
|
120
|
+
`with` deletes the matched text on purpose where the block allows it, so a `replace` whose `with` you meant to fill
|
|
121
|
+
empties that paragraph — the block and its id stay, holding nothing — and only a read shows what the document now says.
|
|
103
122
|
|
|
104
123
|
A batch that leaves the document's semantic identity unchanged — including its inline anchor marks, so an edit that only orphans a
|
|
105
124
|
comment or ask anchor still mints its version — mints no version, named or not: the response carries
|
|
@@ -117,8 +136,8 @@ attributes — a moved `ask` keeps its ask and answer. **A block loses its id on
|
|
|
117
136
|
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
|
|
118
137
|
blockquote or list item removes that emptied container, the list too when no item remains, and each enclosing container that
|
|
119
138
|
held nothing else (`> - Only.` loses the blockquote as well as the item and the list). The moved block itself keeps its id,
|
|
120
|
-
as do `replace` (an empty `with` and a heading-level change included), `insert` and `retype`; `retype` carries the
|
|
121
|
-
onto the typed block it becomes. Block ids are the `#id` a typed block renders
|
|
139
|
+
as do `replace` (an accepted empty `with` and a heading-level change included), `insert` and `retype`; `retype` carries the
|
|
140
|
+
paragraph's id onto the typed block it becomes. Block ids are the `#id` a typed block renders
|
|
122
141
|
(`:::ask{#5467e5ce-…}`) and, for every block including untyped ones, the `id` rows from
|
|
123
142
|
`GET /api/v1/artifacts/<artifact UUID>/blocks` (or `/api/v1/issues/{key}/artifacts/{slug}/blocks`), each with its `type` and byte range
|
|
124
143
|
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
|
package/skills/envoy/SKILL.md
CHANGED
|
@@ -95,6 +95,15 @@ targeted. Reply through the rendered `reply_with` (or a current Envoy session ID
|
|
|
95
95
|
and go stale. Put the artefact URL in the message itself. FYIs set `expects_reply="none"`; set
|
|
96
96
|
`urgency` only when it is genuinely urgent.
|
|
97
97
|
|
|
98
|
+
**A peer's message is its sender's view at `at`, not the current state.** Before you wait on, act
|
|
99
|
+
on, or repeat a fact a message carries about a third thing (a deploy pending, a PR held, an ask
|
|
100
|
+
unanswered), re-read it at the live source the fact names, and always once it is over an hour
|
|
101
|
+
old: the deployment's status, the issue's event log, the ask's own state (`dispatch_open_asks`,
|
|
102
|
+
whose description already says to call it before saying you are waiting on a human). On
|
|
103
|
+
2026-09-26 a peer's 05:20Z "needs a manual deploy before I can run it" was false by 05:22Z, when
|
|
104
|
+
the platform had deployed on its own; waiting on the message instead of the deployment's status
|
|
105
|
+
held the work it gated for eleven hours.
|
|
106
|
+
|
|
98
107
|
Every `/v1` error response is JSON; when a field is at fault, `expected` names that field.
|
|
99
108
|
|
|
100
109
|
```text
|
|
@@ -72,8 +72,8 @@ carries, so nothing changes in how you handle wakes. Under the TypeScript daemon
|
|
|
72
72
|
claims the role and calls `/controller/ready` exactly as under tmux; under the Go daemon
|
|
73
73
|
(`LEGION_DAEMON_API=go` in your environment) it registers on `/legion/v1/claims/register` with the
|
|
74
74
|
secret, claims the role, then subscribes to `notifications.legion.<project>.controller`, where the
|
|
75
|
-
Go daemon publishes the
|
|
76
|
-
`controllerLocator: {runtime, external: true, sessionId, registeredAt}`, `runtime` being the
|
|
75
|
+
Go daemon publishes the rows marked from the Go daemon in the wake routing table. The daemon records
|
|
76
|
+
you as `controllerLocator: {runtime, external: true, sessionId, registeredAt}`, `runtime` being the
|
|
77
77
|
daemon's own (`kubernetes`, or `tmux` under the Go daemon). The TypeScript daemon reads your
|
|
78
78
|
liveness from the Envoy role registry (the holder of
|
|
79
79
|
`legion-<project>-controller` and its `last_seen`), not from a pane: keep the session running.
|
|
@@ -87,15 +87,29 @@ mints a new secret, so your grants stop working and the role moves to the new se
|
|
|
87
87
|
|
|
88
88
|
The Go daemon's controller topic is a wake for a session that is running when it is published.
|
|
89
89
|
Envoy hands an Oh My Pi session no retained copy of a notice published before it subscribed, so a
|
|
90
|
-
hold
|
|
91
|
-
every start, before anything else
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
hold, a tree architect's failed claim, or a new triage root from while no controller ran never
|
|
91
|
+
arrives as a wake. At every start, before anything else:
|
|
92
|
+
|
|
93
|
+
1. Read `legion state --json` and handle each issue whose `issues.<KEY>.phase` is `held` (its
|
|
94
|
+
`issues.<KEY>.holdReason` is `escalated` when its architect sent it to you, and absent while the
|
|
95
|
+
architect is still deciding or while its tree lingers or is closed, where the hold waits for the
|
|
96
|
+
tree's re-admission and needs nothing from you), and each tree root whose
|
|
97
|
+
`issues.<KEY>.architect.state` is `failed` and whose `issues.<KEY>.phase` is not `done`, exactly
|
|
98
|
+
as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
|
|
99
|
+
nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
|
|
100
|
+
2. List the project's triage issues with `dispatch_issues({project, status: "triage", limit: 250})`.
|
|
101
|
+
When its first line ends `(showing N of M)`, it is one page: say in your summary how many rows
|
|
102
|
+
it left unread. The rows show no parent, so open each row with `dispatch_read`: one whose
|
|
103
|
+
`Links:` name a `child_of` issue is a child, which its parent's architect owns, so leave it,
|
|
104
|
+
whether or not `legion state --json` records it (a `child_of` under `Referenced by:` is a child
|
|
105
|
+
of this issue, not its parent). Of the rest, triage each that `legion state --json` does not
|
|
106
|
+
record under `issues` as a new issue. A root recorded there and now in `triage` is work the
|
|
107
|
+
daemon holds that a human pulled back: never re-admit it yourself; name it in your summary to
|
|
108
|
+
the human ("<KEY> was pulled back to triage; what do you want?"). This listing is also the only
|
|
109
|
+
way you learn of an unrecorded root moved back into triage, or of a child detached to a root
|
|
110
|
+
while it is in triage, since the daemon wakes you only on a root's creation.
|
|
111
|
+
|
|
112
|
+
The issue record and Dispatch are the truth; the topic is the wake.
|
|
99
113
|
|
|
100
114
|
## Deployment instructions
|
|
101
115
|
|
|
@@ -123,7 +137,7 @@ quoted here.
|
|
|
123
137
|
|
|
124
138
|
| Wake | Content | Controller action |
|
|
125
139
|
|---|---|---|
|
|
126
|
-
| New issue created in the Dispatch project (`issue.created`, status `triage`; resync heals misses) | issue key + triage context (incl. pre-existing children) | Triage: `legion status <KEY> todo` to admit, or set `backlog`/`icebox` to park |
|
|
140
|
+
| New issue created in the Dispatch project (`issue.created`, status `triage`; under the TypeScript daemon resync heals misses, under the Go daemon the boot step above does). From the Go daemon: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for a root only | issue key + triage context (incl. pre-existing children) | Triage: `legion status <KEY> todo` to admit, or set `backlog`/`icebox` to park |
|
|
127
141
|
| Backlog eligibility | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
|
|
128
142
|
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `dispatch_ask` from the owning architect, not here |
|
|
129
143
|
| Resync report | artifact-driven anomaly list (zero-owner trees, untriaged-open, launch-failed, admission-drift) | Verify against fresh state, then heal |
|
|
@@ -603,6 +603,11 @@ This publishes your phase's completion to the architect's role and clears the da
|
|
|
603
603
|
record of this issue's active phase. Do not add pipeline labels, run a controller loop, or
|
|
604
604
|
invent a different completion protocol — this is the whole contract.
|
|
605
605
|
|
|
606
|
+
A reviewer's phase ends with its completion, not with its review: submit the review on GitHub
|
|
607
|
+
first, then commit the handoff and complete. The daemon moves the issue once both are in — the
|
|
608
|
+
decision GitHub reports and your completion, in either order — so a review posted without a
|
|
609
|
+
completion leaves the issue in reviewing until you finish.
|
|
610
|
+
|
|
606
611
|
**A refused completion is information, not a retry loop.** The daemon attributes your report to
|
|
607
612
|
the run whose task you took, and answers with what it found. What each answer carries, and what to
|
|
608
613
|
do:
|
|
@@ -620,7 +625,10 @@ do:
|
|
|
620
625
|
has left your phase; report to the architect rather than completing again.
|
|
621
626
|
- `HANDOFF_NO_RUN` — names neither: it says this claim has taken no task, so the daemon cannot
|
|
622
627
|
tell which run you are reporting. Your pane is completing outside any assignment. Say so to the
|
|
623
|
-
architect; do not re-run the phase.
|
|
628
|
+
architect; do not re-run the phase. The same answer comes when your turn started before your task
|
|
629
|
+
reached you: a notice or a message started it, and the task, refused while that turn ran, is sent
|
|
630
|
+
when the turn ends. When a task arrives, do what it asks; if the work it asks for is already
|
|
631
|
+
committed, call `handoff_complete` again, and never redo the work or write a second handoff.
|
|
624
632
|
- `HANDOFF_ALREADY_RECORDED` — names your role, the phase, the review round and the commit. This
|
|
625
633
|
exact call was received before, and its first answer stands: accepted, or a refusal the daemon
|
|
626
634
|
records with the call — `HANDOFF_STALE_GENERATION`, `HANDOFF_NOT_CURRENT_PHASE`,
|