@sjawhar/opencode-legion-envoy 3.13.0 → 3.14.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/dist/src/server.js +21 -0
- package/package.json +1 -1
- package/skills/AGENTS.md +2 -0
- package/skills/dispatch/SKILL.md +83 -663
- package/skills/dispatch/references/api.md +28 -0
- package/skills/dispatch/references/asks.md +109 -0
- package/skills/dispatch/references/document-edits.md +1 -1
- package/skills/dispatch/references/documents.md +148 -0
- package/skills/dispatch/references/examples.md +82 -0
- package/skills/dispatch/references/issues.md +213 -0
- package/skills/dispatch/references/messages.md +65 -0
- package/skills/dispatch/references/reading.md +55 -0
- package/skills/dispatch-first/SKILL.md +49 -0
- package/skills/legion-architect/SKILL.md +2 -2
- package/skills/legion-worker/SKILL.md +65 -365
- package/skills/legion-worker/references/conflicts-and-rewrites.md +129 -0
- package/skills/legion-worker/references/merge-gate.md +101 -0
- package/skills/legion-worker/references/pr-body.md +125 -0
- package/skills/legion-worker/references/review-threads.md +71 -0
- package/src/server.ts +22 -1
- package/skills/legion-worker/references/knowledge-injection.md +0 -98
- /package/skills/legion-worker/{resources/strategies → references}/cleanup-deletion.md +0 -0
- /package/skills/legion-worker/{resources/strategies → references}/systematic-rename.md +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Authentication and the HTTP API
|
|
2
|
+
|
|
3
|
+
`skill://dispatch` sends you here when you set up a Dispatch token, or need a route the tools do
|
|
4
|
+
not cover.
|
|
5
|
+
|
|
6
|
+
## Agent authentication
|
|
7
|
+
|
|
8
|
+
Use a personal Dispatch token: a human mints it in Dispatch **Settings → Agent tokens** and supplies
|
|
9
|
+
it to the agent through `dispatch.token` in `~/.config/opencode/envoy.json` or `DISPATCH_TOKEN`.
|
|
10
|
+
The server records the minting human as the owner of that session's writes. `DISPATCH_AGENT_TOKEN`
|
|
11
|
+
is the shared devbox fallback; do not configure it for an individual agent.
|
|
12
|
+
|
|
13
|
+
The deployed Dispatch server's browser origin is configured separately with
|
|
14
|
+
`DISPATCH_SERVER_URL` in the deployment `compose/.env`. Do not change an
|
|
15
|
+
agent's `envoy.json` to set the GitHub OAuth callback origin: the value must
|
|
16
|
+
be the exact URL humans type in their browser, and the GitHub App callback is
|
|
17
|
+
`<DISPATCH_SERVER_URL>/auth/callback`.
|
|
18
|
+
|
|
19
|
+
### Finding a route
|
|
20
|
+
|
|
21
|
+
The tools cover the everyday surface. For anything else, ask the server: `GET /api/v1` (no
|
|
22
|
+
credential) returns every route as `{method, path, auth, description}` sorted by path — `auth`
|
|
23
|
+
is `public`, `any` (a human or a bearer), `human` (a bearer gets `403 HUMAN_ONLY`), or `bearer`.
|
|
24
|
+
A path Dispatch does not serve under `/api` or `/v1` answers
|
|
25
|
+
`404 {"code":"NOT_FOUND","error":"no route for GET /v1/issues","hint":"GET /api/v1 lists every
|
|
26
|
+
route"}`; when you see that, you typed the path wrong — read the index rather than guessing. Every
|
|
27
|
+
`/api/v1` error body carries a `code`; branch on the code, never on the text.
|
|
28
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
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 login of the human 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": "<login>"}`; any authenticated caller may reassign, and an unlisted login 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 is an ordinary question: phrase the to-do as the question and give it
|
|
19
|
+
the options that name its outcomes, in the human's words - there is no fixed vocabulary and the
|
|
20
|
+
server treats no label specially. If an outcome needs a reason, say so in that option's
|
|
21
|
+
description, and the human's free-text answer carries it:
|
|
22
|
+
```ts
|
|
23
|
+
dispatch_ask({ issue: "DSP-42",
|
|
24
|
+
question: "Run the production deploy for #19125?",
|
|
25
|
+
options: [{ label: "Deployed" }, { label: "Blocked", description: "Say what is missing." }] })
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Correct or refine an open ask in place instead of opening a second question:
|
|
29
|
+
```ts
|
|
30
|
+
dispatch_edit_ask({
|
|
31
|
+
ask,
|
|
32
|
+
question?,
|
|
33
|
+
options?: { label, description? }[],
|
|
34
|
+
multiple?,
|
|
35
|
+
urgency?,
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
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
|
|
39
|
+
log and invalidates any answer draft against the prior `edited_at` revision, so the human sees the new wording and explicitly reconfirms.
|
|
40
|
+
An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
|
|
41
|
+
|
|
42
|
+
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
|
|
43
|
+
block along with the row, so the edit stands and the document reads the same. It changes only the fields you name: pass `urgency`
|
|
44
|
+
alone and the question's own wording, formatting, links and comment anchors are untouched. Pass `question` or `options` and that part
|
|
45
|
+
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
|
|
46
|
+
writes a new version, which on a spec awaiting approval closes the design gate until the new version is approved. Editing the block
|
|
47
|
+
with `dispatch_doc_edit` works too and is the way to change anything else about it, including adding formatting to a question.
|
|
48
|
+
Re-sending a field unchanged rewrites nothing, so retrying the whole ask is safe.
|
|
49
|
+
Text the block cannot carry back unchanged is refused outright, naming the field and writing nothing - an option label containing
|
|
50
|
+
`": "`, the separator between a label and its description, is one example of text that cannot survive the round trip. Blank lines separate paragraphs;
|
|
51
|
+
a single newline is kept as a line break.
|
|
52
|
+
|
|
53
|
+
An ask stays open until a human answers, unless its question no longer needs that answer. Retract a moot or superseded question, or
|
|
54
|
+
self-resolve one after finding the answer:
|
|
55
|
+
```ts
|
|
56
|
+
dispatch_resolve_ask({
|
|
57
|
+
ask,
|
|
58
|
+
kind: "retracted",
|
|
59
|
+
reason: "A newer ask supersedes this question.",
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the reason because the question remains
|
|
63
|
+
in its Conversation card and reply thread; a reason beginning `removed from the document in version` is refused, because that is how a
|
|
64
|
+
retraction the document's own settlement wrote is recognised. Resolving a block ask records it in the block too, so it stays resolved
|
|
65
|
+
however the document moves afterwards — while deleting the block from the document is the other way to close one, and putting the block
|
|
66
|
+
back reopens it. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
|
|
67
|
+
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
|
|
68
|
+
`dispatch_comment` (mutually exclusive with `reply_to`).
|
|
69
|
+
A review comment you opened has its own closer, `dispatch_resolve_comment` — see
|
|
70
|
+
[Comments and suggestions](skill://dispatch/references/documents.md).
|
|
71
|
+
|
|
72
|
+
A human answers or asks back from the same field; a question-shaped free-text answer is offered as a clarification first. A human
|
|
73
|
+
reply while your ask is still open (the delivered `comment.created` carries `ask_state: open`) is a request for clarification, not
|
|
74
|
+
an answer: the human did not understand the question or needs more before choosing. The ask now waits on you in their Inbox. Answer
|
|
75
|
+
in the same thread with `dispatch_comment({ reply_to_ask })`, or reword the question itself with `dispatch_edit_ask` when the wording
|
|
76
|
+
was the problem; either puts the ask back in front of them. Do not open a second ask.
|
|
77
|
+
|
|
78
|
+
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
|
|
79
|
+
(`turn` omitted, or `turn: "human"`) hands the turn to the human: the ask returns to their `Waiting on you`. When you are not done
|
|
80
|
+
yet — "dispatched two auditors, back with results", "checking the release branch, back shortly", any working-on-it note — reply with
|
|
81
|
+
`turn: "agent"`: the note lands in the thread, the ask stays under `Waiting on agents`, and the human is not told to act. Use
|
|
82
|
+
`turn: "agent"` for every progress note and `turn: "human"` (the default) only when you need them. A human's reply always hands the
|
|
83
|
+
turn to you. The result names the state (`ask now waiting on agent` / `human`), the delivered `comment.created` carries it as
|
|
84
|
+
`ask_waiting_on`, and every ask read carries it as `waiting_on`.
|
|
85
|
+
|
|
86
|
+
## Following
|
|
87
|
+
|
|
88
|
+
An ask has followers: every session that wrote to it — the session that opened it and every session that replied with
|
|
89
|
+
`dispatch_comment({ reply_to_ask })` — plus any session a human adds from the ask card. The ask's answer, edits, resolution, and
|
|
90
|
+
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
|
|
91
|
+
route. The tool result says so (`You follow this ask: its answer and replies reach you directly.`) and carries `details.follows.ask`;
|
|
92
|
+
the host tells you once per ask. Leave a thread you no longer need, or rejoin one, with:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
dispatch_follow({ ask, action: "follow" | "unfollow" })
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`ask` is the full ask id or a `dispatch://KEY/ask/<id>` reference; `dispatch_follow`, `dispatch_edit_ask`, and
|
|
99
|
+
`dispatch_resolve_ask` also take an 8+ hex prefix that is unique among your own open asks, and refuse anything shorter by naming
|
|
100
|
+
those asks. A human may also remove you from the ask card; either way you are
|
|
101
|
+
told with an `ask.follower_removed` notice, and a human adding you arrives as `ask.follower_added`.
|
|
102
|
+
|
|
103
|
+
No write subscribes you to an issue or document. Following covers your own asks and the threads you joined; everything else on the
|
|
104
|
+
owner — other sessions' asks, comments, messages, status changes — reaches you only if you subscribe to the owner topic yourself.
|
|
105
|
+
Every write result names that line: `envoy_subscribe notifications.dispatch.issue.<KEY>.>` for an issue,
|
|
106
|
+
`envoy_subscribe notifications.dispatch.document.<PROJECT>.<SLUG>.>` for a project document. The owner topic carries every Dispatch
|
|
107
|
+
event; `notify` only controls agent wake and routed delivery. A human may unsubscribe you from the issue or document header; you
|
|
108
|
+
are told with a `subscription.removed` notice when that happens.
|
|
109
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Editing a document
|
|
2
2
|
|
|
3
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.
|
|
4
|
+
with `dispatch_doc_edit`, never re-uploaded. "The Spec" in `skill://dispatch` sends you here for
|
|
5
5
|
the tool's shape, how to target the text you mean, what each operation costs a block, and how to
|
|
6
6
|
reject a stale edit.
|
|
7
7
|
|
|
@@ -0,0 +1,148 @@
|
|
|
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, or get `DOC_SCHEMA`, `INVALID_ASK_BLOCK` or
|
|
5
|
+
`DOC_SERVICE_UNAVAILABLE` back. Changing a document's text, tables included, is
|
|
6
|
+
[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. For
|
|
15
|
+
a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing typed block, retain
|
|
16
|
+
its id and every rendered attribute. Never copy an existing block's id into new markdown: an id
|
|
17
|
+
names one block, so an insert, upload or suggestion whose markdown names an id the document holds
|
|
18
|
+
outside the text it replaces is refused naming the id: `INVALID_OP` for an insert,
|
|
19
|
+
`INVALID_MARKDOWN` for any other write. To rewrite such a block whole, `delete` it and then
|
|
20
|
+
`insert` the new one carrying its id, anchored on the block before or after it, in that order and
|
|
21
|
+
in one batch: an insert carrying an id the document still holds is refused.
|
|
22
|
+
|
|
23
|
+
Use only the type names, content rule, attributes, and enum values returned by the schema. Values are
|
|
24
|
+
quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::: {.callout}`, leaf
|
|
25
|
+
`::name` directives, or text `:name` directives; those strings are literal when quoted inside a code
|
|
26
|
+
block. Do not set attributes the schema marks `server: true`; the server ignores them and reasserts
|
|
27
|
+
its authoritative value at settlement.
|
|
28
|
+
|
|
29
|
+
Questions about a document must be `ask` blocks, never an `Open questions` prose section. An ask
|
|
30
|
+
body is one or more question paragraphs followed by an optional bullet list of options, where each
|
|
31
|
+
item is `Label: description`. A spec, an uploaded document or an uploaded version holding an ask that
|
|
32
|
+
breaks that shape - a code block, heading or quote in it, a paragraph after its options, a second
|
|
33
|
+
list - is refused with `INVALID_ASK_BLOCK`; so is an edit that writes one. An ask someone left
|
|
34
|
+
unreadable in the browser refuses nothing it is carried through unchanged by. For example:
|
|
35
|
+
|
|
36
|
+
```md
|
|
37
|
+
:::ask{urgency="high" multiple="false"}
|
|
38
|
+
Should we ship the migration?
|
|
39
|
+
|
|
40
|
+
- Ship: Release the verified change.
|
|
41
|
+
- Hold: Wait for another review.
|
|
42
|
+
:::
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
When a human answers a decision written as an ask block, the answer lives on that ask. Use
|
|
46
|
+
`dispatch_resolve_ask` when the decision is resolved without a human response, or preserve the
|
|
47
|
+
human's answer; never rewrite the question into its answer or blank its options. An edit that leaves
|
|
48
|
+
an ask block without a question or with a blank option is rejected with `INVALID_ASK_BLOCK`.
|
|
49
|
+
|
|
50
|
+
## Comments and suggestions
|
|
51
|
+
|
|
52
|
+
Add feedback with:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask?, turn? })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It returns issue or project-document owner details plus `comment`; a `reply_to_ask` reply also returns `ask` and `follows: { ask }`,
|
|
59
|
+
because replying to an ask makes you one of its followers (see [Following](skill://dispatch/references/asks.md)).
|
|
60
|
+
`ref` names the owner (an issue or project-document reference) in place of `issue`/`project`.
|
|
61
|
+
`quote` requires `artifact`; its anchor is pinned to the containing block while retaining the quote
|
|
62
|
+
for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
|
|
63
|
+
`quote`; it belongs to its parent's anchor. Reply to any comment in a thread; the server keeps
|
|
64
|
+
threads flat. A reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a
|
|
65
|
+
question asked with `dispatch_ask`; `turn` (only with `reply_to_ask`) says who holds the turn after
|
|
66
|
+
the reply — `agent` for a progress note that keeps the ask waiting on you, `human` (the default) when
|
|
67
|
+
the human needs to act; see "Asking" in `skill://dispatch`. Comments are edited only by their author from the
|
|
68
|
+
dashboard. A delivered `comment.created` event carries the comment `id`; reply to it with
|
|
69
|
+
`dispatch_comment({ reply_to: <id> })`.
|
|
70
|
+
|
|
71
|
+
Resolve your own review comment once you have addressed it:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
dispatch_resolve_comment({ comment })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`comment` is the comment id, or a `dispatch://KEY/comment/<id>` or `dispatch://PROJECT/artifact/<slug>/comment/<id>` reference (an
|
|
78
|
+
8+ character id prefix is resolved against the owner's comments). It returns the owner details plus `comment`, and takes no reason —
|
|
79
|
+
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
|
|
80
|
+
resolve only threads you opened or were asked to close; reopening a resolved thread is human-only (from the dashboard), though your
|
|
81
|
+
reply to it reopens it. Asks are closed with `dispatch_resolve_ask` instead.
|
|
82
|
+
|
|
83
|
+
An exact replacement for document text is a suggestion (`dispatch_suggest`), never a comment; a
|
|
84
|
+
comment is for a question or a note the human answers in words. A human accepts a suggestion with
|
|
85
|
+
one click and cannot accept a comment, so propose the replacement instead of describing it:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
dispatch_suggest({ issue?, project?, artifact, ref?, quote, replace_with, body?, occurrence? })
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It returns issue or project-document owner details plus `comment`. A human accepts or rejects a suggestion.
|
|
92
|
+
Errors: `TARGET_AMBIGUOUS` (add `occurrence`), `TARGET_NOT_FOUND` (re-read first), `INVALID_ANCHOR`/`ANCHOR_MISSING`/`ANCHOR_ORPHANED`
|
|
93
|
+
(bad, unwritten, or stale quote), `INVALID_MARKDOWN`/`DOC_SCHEMA` (malformed content), `CAP_EXCEEDED`, `ISSUE_CLOSED`.
|
|
94
|
+
Suggest only what the quoted block can hold. The accept, not the suggestion, checks that: an
|
|
95
|
+
accept whose `replace_with` would break an ask block that was readable before it is refused with
|
|
96
|
+
`INVALID_ASK_BLOCK` - a question given a code block, text after an ask's options, a second option
|
|
97
|
+
list, or an emptied question; one that names an id the document holds outside the text it
|
|
98
|
+
replaces, an ask under a held id included, with `INVALID_MARKDOWN`; and one no part of the
|
|
99
|
+
document can hold where it sits (a code block over a table cell's whole text), or whose quote runs
|
|
100
|
+
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
|
|
101
|
+
stays open until someone rejects or replaces it.
|
|
102
|
+
|
|
103
|
+
## Artifacts
|
|
104
|
+
|
|
105
|
+
Attach an image, diagram, or local file with:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
dispatch_artifact({ issue?, project?, name, path, summary? })
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or, when the text is already in the call, post a Markdown document directly:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
dispatch_artifact({ issue?, project?, name: "load-test-results.md", content: "# Load test\n..." })
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
|
|
118
|
+
Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact` and `version`.
|
|
119
|
+
Uploading the same `name` creates its next version — so uploading `spec.md` **replaces the issue's own specification**
|
|
120
|
+
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
|
|
121
|
+
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
|
|
122
|
+
slug also arrives on `artifact.created` events. Dispatch suffixes a slug two documents would share, so one document's
|
|
123
|
+
filename can be another's slug (`plan v2` takes `plan-v2`, then a document named `plan-v2` takes `plan-v2-2`): a bare
|
|
124
|
+
`artifact` that names both is refused with each one's id, while a `dispatch://` reference's document part is always the slug.
|
|
125
|
+
|
|
126
|
+
Documents are CommonMark. A bare `<https://example.com|text>` is a CommonMark autolink and is normalised: the angle brackets are
|
|
127
|
+
dropped and the URL keeps `|text`. A backslash-escaped `\<https://example.com|text>` displays as `<https://example.com|text>` in the
|
|
128
|
+
document but comes back re-escaped (`\<`) from `dispatch_doc_read`. A Slack mrkdwn draft, or any other payload that is not Markdown,
|
|
129
|
+
still belongs inside a fenced code block, where it survives verbatim both ways.
|
|
130
|
+
|
|
131
|
+
## A document that is reloading
|
|
132
|
+
|
|
133
|
+
These calls can answer `DOC_SERVICE_UNAVAILABLE` (HTTP 503), because each writes a document inside its
|
|
134
|
+
transaction: `dispatch_doc_edit`; `dispatch_ask` and `dispatch_comment` on a quote; a `dispatch_comment` reply
|
|
135
|
+
in a thread whose first comment is anchored; `dispatch_suggest`; `dispatch_resolve_comment` on an anchored
|
|
136
|
+
comment; `dispatch_artifact` replacing a document that already exists; and, on an ask that lives in a `:::ask`
|
|
137
|
+
block, `dispatch_edit_ask` and `dispatch_resolve_ask`, which write that block. Creating an issue with a spec,
|
|
138
|
+
uploading a new document, `dispatch_request_approval` and `dispatch_message` never answer it, and neither do
|
|
139
|
+
`dispatch_edit_ask` and `dispatch_resolve_ask` on an ask that has no block. It means that document's live room
|
|
140
|
+
failed and is reloading from its durable copy, so the server refused rather than wait for it; your call wrote
|
|
141
|
+
nothing and the document is intact. Nothing retries it for you: the Dispatch client hands a 503 straight back.
|
|
142
|
+
Wait a few seconds and make the same call again. A second refusal in a row is worth telling your human about,
|
|
143
|
+
with the document's reference.
|
|
144
|
+
|
|
145
|
+
`dispatch_doc_read` can answer it too, though it writes nothing: a read never opens a live room, and waits out
|
|
146
|
+
a room that is reloading, so it is refused only when the document's durable copy cannot be read or decoded.
|
|
147
|
+
Retry it the same way.
|
|
148
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Before and after: an ask, a message, and a draft
|
|
2
|
+
|
|
3
|
+
`skill://dispatch` sends you here for worked examples: a decision written as clickable options, a
|
|
4
|
+
message that should not be sent, and a draft placed where the human reads it.
|
|
5
|
+
|
|
6
|
+
## Before / after
|
|
7
|
+
|
|
8
|
+
Before — a wall of text hides the decision and makes the choices unclickable:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
We need to settle the release gate because the deploy branch has the migration and the
|
|
12
|
+
dashboard changes, I checked the staging result and it is fine except the release notes are
|
|
13
|
+
not reviewed, so should we ship today, wait for docs, or cut the dashboard from this release?
|
|
14
|
+
I think waiting is safest but the customer demo is tomorrow and the list above is probably stale.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
After — anchor the decision and make each option a button:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
dispatch_ask({
|
|
21
|
+
issue: "LEGION-815",
|
|
22
|
+
question:
|
|
23
|
+
"Choose the release gate. Recommendation: ship after release-note review, since the tested deployment is otherwise ready.",
|
|
24
|
+
options: [
|
|
25
|
+
{ label: "Review notes, then ship", description: "Keeps the release intact and reviewed." },
|
|
26
|
+
{ label: "Ship now", description: "Meets the demo deadline; release notes follow later." },
|
|
27
|
+
],
|
|
28
|
+
urgency: "high",
|
|
29
|
+
anchor: { artifact: "spec", quote: "Release requires reviewed operator instructions before deployment." },
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Before — a progress note that nobody needs, posted where humans look for decisions:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
dispatch_message({ issue: "LEGION-815", body: "Merged the release PR, moving to docs next." })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
After — nothing. The merge is visible on the pull request; the docs work shows up as its own deliverable. Post a message only when
|
|
40
|
+
a human must act or a deliverable is theirs to use:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
dispatch_message({
|
|
44
|
+
issue: "LEGION-815",
|
|
45
|
+
body: "Release 1.4 is live on the devbox (dispatch://LEGION-815/artifact/release-notes). Nothing needed from you.",
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Before — a draft the human must read is uploaded as a separate file, the spec only names it, and
|
|
50
|
+
the ask does not point at it, so the reader has to go looking:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
dispatch_artifact({ issue: "OPS-52", name: "cu-update-2026-09-15.md", content: "Hi team, ..." })
|
|
54
|
+
dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
|
|
55
|
+
{ op: "insert", after: "## Context", markdown: "## Draft (artifact cu-update-2026-09-15.md)" },
|
|
56
|
+
]})
|
|
57
|
+
dispatch_ask({ issue: "OPS-52", question: "Send the customer update as drafted?", options: [...] })
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
After — the draft is a section of the spec, and the ask anchors there. If it really must be a
|
|
61
|
+
file (something to send as-is), the spec and the ask both link the slug from the upload result:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
|
|
65
|
+
{ op: "insert", after: "## Context", markdown: "## Draft\n\nHi team, ..." },
|
|
66
|
+
]})
|
|
67
|
+
dispatch_ask({
|
|
68
|
+
issue: "OPS-52",
|
|
69
|
+
question: "Send the customer update as drafted?",
|
|
70
|
+
options: [...],
|
|
71
|
+
anchor: { artifact: "spec", quote: "Hi team," },
|
|
72
|
+
})
|
|
73
|
+
// or, for a real file — the spec links it where the reader needs it, and so does the ask:
|
|
74
|
+
dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
|
|
75
|
+
{ op: "insert", after: "## Context", markdown: "## Draft\n\nThe update to send as-is: dispatch://OPS-52/artifact/cu-update-2026-09-15-md" },
|
|
76
|
+
]})
|
|
77
|
+
dispatch_ask({
|
|
78
|
+
issue: "OPS-52",
|
|
79
|
+
question: "Send this customer update as-is? dispatch://OPS-52/artifact/cu-update-2026-09-15-md",
|
|
80
|
+
options: [...],
|
|
81
|
+
})
|
|
82
|
+
```
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Working an issue: choosing, claiming, status, priority, and the backlog
|
|
2
|
+
|
|
3
|
+
`skill://dispatch` sends you here when you pick your next issue, claim or release one, get
|
|
4
|
+
`409 ISSUE_CLAIMED`, move an issue's status or priority, reorder a board, reassign an issue, sync a
|
|
5
|
+
project's architecture model, or list or audit a project's backlog.
|
|
6
|
+
|
|
7
|
+
## Choosing what to work on
|
|
8
|
+
|
|
9
|
+
When you finish an issue, or are told to work on the next thing, take the top ready issue of the
|
|
10
|
+
whole backlog, across every project: status `todo`, highest priority first, then board rank. There
|
|
11
|
+
are no areas: a standing role, a product owner and a lane each take the top issue like everyone
|
|
12
|
+
else (Sami, 2026-09-27, dispatch://AGENTC-34/ask/01ed2956-73cc-48d2-8ed4-7a86c6d439b1). `todo`
|
|
13
|
+
means ready: specced, unblocked, and waiting on neither a deploy nor a decision. An issue that
|
|
14
|
+
waits on one belongs in `backlog`, with what it waits on said on the issue.
|
|
15
|
+
|
|
16
|
+
Hold at most three issues in flight (`in_progress`, `testing`, `needs_review` or `retro`), of any
|
|
17
|
+
kind (Sami, 2026-09-27, answering dispatch://AGENTC-34/ask/1aeb8f2e-0950-4eaa-aaac-24286c9dd3ca;
|
|
18
|
+
the question proposed two, and his answer set three). The limit is per agent and has nothing to do
|
|
19
|
+
with the week's priorities (Sami, 2026-09-28, reply a7647eb0 on
|
|
20
|
+
dispatch://AGENTC-393/ask/b773d9f6): the priorities decide only what you pull next. Past three:
|
|
21
|
+
push any unfinished work, say where in one comment on the issue, move it to `backlog` and clear
|
|
22
|
+
its route. Each issue counts on its own; a child does not ride under its parent's slot.
|
|
23
|
+
In-flight issues with no owner at all go
|
|
24
|
+
back to `backlog` as well: no claim or route held by a live session, no Dispatch activity in the
|
|
25
|
+
last day, and no pull request moving on GitHub (an owner working there leaves no Dispatch trace).
|
|
26
|
+
The order keeper sweeps those. Never write the status of an issue that carries the `legion`
|
|
27
|
+
label, or of any issue under one: the Legion daemon writes those statuses, and moving one of its
|
|
28
|
+
admitted roots out of its flow parks the tree and stops its workers.
|
|
29
|
+
|
|
30
|
+
One agent keeps the backlog's order against those priorities, with Sami
|
|
31
|
+
(dispatch://AGENTC-34/ask/f6780f9e-8b96-49eb-9be7-7c7f2036d5cc). Setting an issue's priority
|
|
32
|
+
stays yours ([Priority is yours to set](#priority-is-yours-to-set)); reordering the board does not.
|
|
33
|
+
When the top of the backlog looks wrong, or a priority's next step is not yet a ready issue,
|
|
34
|
+
publish it to `notifications.role.backlog-order`, which the order keeper holds, instead of
|
|
35
|
+
reordering the board yourself.
|
|
36
|
+
|
|
37
|
+
## Claim the issue before you work it
|
|
38
|
+
|
|
39
|
+
Two sessions once spent a night implementing the same issue, because nothing on it said who was
|
|
40
|
+
on it (Sami, 2026-09-24, verbatim: "It seems like we need a better way of tracking what's already
|
|
41
|
+
in progress"). So before you start implementing an issue, claim it:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
dispatch_claim({ issue: "LEGION-234" }) // I am implementing this
|
|
45
|
+
dispatch_claim({ issue: "LEGION-234", release: true }) // I have stopped; it is free
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A claim records **your** session — the one making the call, never another — and shows on every
|
|
49
|
+
read of the issue: the dashboard header, the issue list and board, `dispatch_read` (a
|
|
50
|
+
`Claimed by:` line) and `dispatch_issues` (a claim on the row). A session's claim there ends
|
|
51
|
+
`· not running` when the live agent registry does not list that session, as the dashboard's chip
|
|
52
|
+
does — that claim is free to take — and `· liveness unknown` when the registry could not be read,
|
|
53
|
+
which says nothing either way. `dispatch_issues` plus the dashboard's **Unclaimed** filter is how
|
|
54
|
+
you find work nobody is on.
|
|
55
|
+
|
|
56
|
+
- **`409 ISSUE_CLAIMED` means someone else holds this issue.** When it is another session, the
|
|
57
|
+
refusal names it and says it is still running: do not work the issue in parallel — message
|
|
58
|
+
that session (its id is in the message; `envoy_send` reaches it) or pick up something else,
|
|
59
|
+
and tell the human if you believe the work should be yours. When a **human** holds it, the
|
|
60
|
+
refusal names the person and says nothing about a session running, because there is none to
|
|
61
|
+
message: ask them with `dispatch_ask` instead, so the open ask appears in their Inbox, and
|
|
62
|
+
never assume their claim has lapsed — only a human releases or forces a human's claim.
|
|
63
|
+
- **`409 CLAIM_CONTENDED` means the issue changed hands twice while your call ran**, so nothing
|
|
64
|
+
was applied and nobody's liveness was checked. Read the issue and decide again; it is not a
|
|
65
|
+
refusal by a live holder.
|
|
66
|
+
- **A claim whose session has ended is yours to take.** If the Envoy listener no longer lists
|
|
67
|
+
the holder, your claim simply succeeds; the takeover is recorded on the issue and the session
|
|
68
|
+
that lost it is told.
|
|
69
|
+
- **Release it when you stop** — finished, handing over, or moving to something else. A claim is
|
|
70
|
+
released by its holder or any human — and by any agent once the holder's session is no longer
|
|
71
|
+
running, the same rule that lets you take it. Closing the issue releases it for you.
|
|
72
|
+
- A claim is intent to implement, not contact: reading the issue, commenting, asking, or gating
|
|
73
|
+
its pull request claims nothing, so a coordinator never collides with an implementer.
|
|
74
|
+
|
|
75
|
+
**Claiming and moving the status are two separate actions, and you do both.** A claim says which
|
|
76
|
+
session is on the work; the status says where the work has got to, and humans use it to track
|
|
77
|
+
that too (Sami, 2026-09-24, verbatim: "Keep them separate — Separate because humans might be
|
|
78
|
+
using them to keep track of work"). So when you start: `dispatch_claim({ issue })` **and**
|
|
79
|
+
`dispatch_issue_update({ issue, status: "in_progress" })`.
|
|
80
|
+
|
|
81
|
+
## Issue status is yours to move
|
|
82
|
+
|
|
83
|
+
The issue's status is how a human sees delivery without asking a session. Outside Legion (where
|
|
84
|
+
the daemon writes it), the session doing the work moves it, the way a person moves a card:
|
|
85
|
+
`in_progress` when implementation starts, `testing` when the change is being proven on a
|
|
86
|
+
production-like surface, `needs_review` when its pull request is open and waiting on the merge
|
|
87
|
+
queue, `done` when the change has been driven in production (a merge is not `done`). Move child
|
|
88
|
+
issues you own as well as the root. An issue left at `triage` while work is underway is a defect:
|
|
89
|
+
Sami, 2026-09-15, on the roadmap he could not read — "I'm not even sure what their development
|
|
90
|
+
status is." Waiting for the deploy lane is not a status and is never announced.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// PATCH /api/v1/issues/{key} — status, title, labels, priority, external_links (merged by URL), route, parent
|
|
94
|
+
dispatch_issue_update({ issue: "AGENTC-175", status: "testing" })
|
|
95
|
+
dispatch_issue_update({ issue: "AGENTC-175", status: "done", reason: "Shipped in owner/repo#7; verified on the production dashboard." })
|
|
96
|
+
dispatch_issue_update({ issue: "AGENTC-175", priority: 1 }) // 0–3; see Priority is yours to set
|
|
97
|
+
dispatch_issue_update({ issue: "AGENTC-175", external_links: ["https://github.com/owner/repo/pull/7"] })
|
|
98
|
+
dispatch_issue_update({ issue: "AGENTC-175", parent: "AGENTC-170" }) // same-project key; "" clears the parent
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Closing takes a `reason`, and the tool refuses `status: "done"` without one: it posts the reason on
|
|
102
|
+
the issue as a message, then closes it, because a closed issue refuses messages, comments, and
|
|
103
|
+
artifacts, so a reason left for later has nowhere to go. When the close fails after the post, the
|
|
104
|
+
error names the posted message; after a timeout or a server error it also says the close may have
|
|
105
|
+
landed, so read the issue's status first. A retry points its reason at the posted message rather
|
|
106
|
+
than repeating it.
|
|
107
|
+
|
|
108
|
+
The two clears differ: `priority` clears with `null`, while `parent` and `route` clear with `""`.
|
|
109
|
+
Guessing the other one is a refusal either way.
|
|
110
|
+
|
|
111
|
+
Link the pull request that delivers the issue in `external_links` when you open it; the issue page
|
|
112
|
+
renders its state and checks from that link, and `dispatch_read` lists it under `External links:`.
|
|
113
|
+
The call is authenticated with the same bearer as every
|
|
114
|
+
other `dispatch_*` tool: a Legion pane reads it from the `DISPATCH_TOKEN_FILE` path the daemon sets on
|
|
115
|
+
the pane; an OMP session outside Legion reads `dispatch.token` from `~/.config/opencode/envoy.json`.
|
|
116
|
+
|
|
117
|
+
A write to an issue still in `triage` answers once with `… is still in triage …`; move the status
|
|
118
|
+
when work has started.
|
|
119
|
+
|
|
120
|
+
## Priority is yours to set
|
|
121
|
+
|
|
122
|
+
Priority is the coarse bucket a backlog is read by: `0` is P0, the highest, through `3`, P3, the
|
|
123
|
+
lowest, and `null` clears it. Sami ruled on 2026-09-24, answering "may agents set issue priority
|
|
124
|
+
(P0–P3), or only propose it for you?" on `dispatch://LEGION/artifact/issue-status-conventions-md`:
|
|
125
|
+
**"Agents may set"**. So set it — on creation, and on a grooming pass over issues that have none —
|
|
126
|
+
and say what you set and why; he overrides anything he disagrees with from the dashboard. A closed
|
|
127
|
+
issue takes only `rank`, `components`, and a reopening `status` (any status but `done`);
|
|
128
|
+
everything else, `priority` included, waits for the reopen (`409 ISSUE_CLOSED`). So reopen it
|
|
129
|
+
first, then set the priority — the two cannot go in one call. `rank` itself is not a tool field:
|
|
130
|
+
reorder it the way [Issue reads](#board-rank-priority-and-assignee-on-a-read) describes, through `PATCH /api/v1/issues/{key}`.
|
|
131
|
+
|
|
132
|
+
## Board rank, priority, and assignee on a read
|
|
133
|
+
|
|
134
|
+
Issue reads include `rank`, the server-owned ordering key used by project boards; reorder through `PATCH /api/v1/issues/{key}` with `{"rank": {"before": "<key>", "after": "<key>"}}`, either neighbor optional and both in the issue's project. A bearer caller also names its own session in that body, `"actor": {"kind": "session", "id": "<your session id>"}`, or the server refuses with `ACTOR_KIND`. They also include nullable coarse priority (`P0` highest through `P3` lowest) and `assignee`: the lowercase GitHub login of the human who answers the issue's asks, or `null` when nobody holds it. `dispatch_read` of an issue prints it as `Assignee: <login>` or `Assignee: unassigned`.
|
|
135
|
+
|
|
136
|
+
## Reading a project's backlog
|
|
137
|
+
|
|
138
|
+
To see the shape of a project rather than find a phrase, list its issues:
|
|
139
|
+
```ts
|
|
140
|
+
dispatch_issues({ project, status?, parent?, label?, priority?, route_status?, updated_since?, limit?, offset? })
|
|
141
|
+
```
|
|
142
|
+
Each row carries the issue key, title, status, priority, parent, labels, its open-ask count, and
|
|
143
|
+
when it last changed — a roadmap or backlog pass without opening every issue. Filter with `status`
|
|
144
|
+
(a lifecycle status), `parent` (one issue's children), `label`, `priority` (a list of `0`–`3`, with
|
|
145
|
+
`null` for an issue with no priority: `[0, 1]` is every P0 and P1), `route_status` (below), or
|
|
146
|
+
`updated_since` (an RFC3339 timestamp, for "what moved this week"). `limit` is the page size, 50 by
|
|
147
|
+
default and 250 at most, and `offset` is where the page starts: repeat with the next offset until a
|
|
148
|
+
page comes back short to list every matching issue.
|
|
149
|
+
|
|
150
|
+
This is not search: it matches no text. Use `dispatch_search` for a keyword or phrase, and
|
|
151
|
+
`dispatch_issues` when you want every issue in a project and its current state.
|
|
152
|
+
|
|
153
|
+
### The owner audit
|
|
154
|
+
|
|
155
|
+
As the owner of a surface, list the project's P0 and P1 issues and staff or close each one nobody
|
|
156
|
+
has started:
|
|
157
|
+
```ts
|
|
158
|
+
dispatch_issues({ project, priority: [0, 1], limit: 250 })
|
|
159
|
+
```
|
|
160
|
+
Every unclaimed row in `triage`, `icebox`, `backlog` or `todo` is a decision: someone takes it and
|
|
161
|
+
builds it, or it closes. A row in `in_progress`, `testing`, `needs_review` or `retro`, or one that
|
|
162
|
+
carries a claim, is work under way ([Issue status is yours to move](#issue-status-is-yours-to-move))
|
|
163
|
+
and is not re-staffed. A todo with a finished spec reads as queued work that nobody is doing
|
|
164
|
+
(LEGION-173 sat in todo for two weeks with a complete spec; AGENTC-1010's v4 plan sat in backlog
|
|
165
|
+
with nobody building it).
|
|
166
|
+
|
|
167
|
+
A close that says the defect cannot happen cites the code that makes it impossible. An issue
|
|
168
|
+
closed because a rewrite forecloses it names the file and line in the rewrite that does so; a
|
|
169
|
+
close that cannot name one is not foreclosed, it is unread. The cheapest way for a rewrite to reach
|
|
170
|
+
parity is to port the code, defect included: LEGION-211's bare `git worktree prune`, filed against
|
|
171
|
+
the TypeScript daemon, had been ported into the Go coordinator and was live in production.
|
|
172
|
+
|
|
173
|
+
The audit finds four shapes:
|
|
174
|
+
|
|
175
|
+
- **Unstaffed work.** A plan or measurement exists, and no one is building it.
|
|
176
|
+
- **Unrecorded delivery.** An issue not yet in `testing` or `done`, claimed or not, has a merged PR
|
|
177
|
+
naming it. Check the change live, then move the issue (AGENTC-1033 sat at `triage` after its fix,
|
|
178
|
+
agent-c #20367, merged).
|
|
179
|
+
- **Unrecorded practice.** Someone does the issue's work by hand, more than once, while the issue
|
|
180
|
+
sits in backlog (OPS-132, done by hand on every migration merge). It leaves no plan and no PR to
|
|
181
|
+
find; the tell is your own messages. Doing something by hand more than once means an issue is
|
|
182
|
+
wearing the wrong status.
|
|
183
|
+
- **Unreachable route.** An open issue whose route names a role nobody holds, or a session that is
|
|
184
|
+
not running, reaches nobody, whatever its priority, and the priority filter above never finds
|
|
185
|
+
it. List it on its own:
|
|
186
|
+
```ts
|
|
187
|
+
dispatch_issues({ project, route_status: "no_holder", limit: 250 })
|
|
188
|
+
```
|
|
189
|
+
Each row reads `route role:sre (nobody holds it right now)` or `route session:<id> (that session
|
|
190
|
+
is not running right now)`. That is one read of the listener, and one read is a restart gap as
|
|
191
|
+
often as a vacancy: an agent box that restarts or resumes keeps the session id, but the session
|
|
192
|
+
is absent from the listener for minutes, and its role with it. On 2026-09-27, 58 of 63 session
|
|
193
|
+
routes one read showed as unreachable pointed at a single session that was moving between boxes.
|
|
194
|
+
So a route is unowned only when it is `no_holder` on two reads at least ten minutes apart: list
|
|
195
|
+
again after ten minutes and act on the issues both lists name. Confirm with the second
|
|
196
|
+
`dispatch_issues` read, not `envoy_role_get`: a role lookup releases the claim of a holder whose
|
|
197
|
+
session is absent from the registry as it answers. Then staff the role, re-route the
|
|
198
|
+
issue to a live holder, or clear the route and assign it (AGENTC-1065, a P2 production listener
|
|
199
|
+
503, sat routed to an unheld `role:sre` with no assignee). `route_status: "unknown"` means the
|
|
200
|
+
listener did not answer, so a route could not be judged; a `no_holder` filter refuses rather
|
|
201
|
+
than answer an empty list then.
|
|
202
|
+
|
|
203
|
+
Run the audit as a step of a coordinator's loop, at each checkpoint, not as a habit: these shapes
|
|
204
|
+
are found by running the check, not by noticing them.
|
|
205
|
+
|
|
206
|
+
## Syncing a project's architecture model
|
|
207
|
+
|
|
208
|
+
Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
|
|
209
|
+
```ts
|
|
210
|
+
dispatch_architecture_sync({ project: "CORE" })
|
|
211
|
+
```
|
|
212
|
+
It returns the imported commit, or the recorded error when the model was rejected — the previous model stays up. Without a configured source it answers 404 `SOURCE_NOT_FOUND`.
|
|
213
|
+
|