@sjawhar/opencode-legion-envoy 1.27.4 → 1.28.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 +72 -22
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +37 -13
package/dist/src/server.js
CHANGED
|
@@ -13779,7 +13779,7 @@ var SPEC_SECTIONS = [
|
|
|
13779
13779
|
];
|
|
13780
13780
|
var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Write for a reader who has not seen the code: plain sentences, every identifier expanded on " + "first use, no coined shorthand; see skills/dispatch Writing for the human and Writing a spec.";
|
|
13781
13781
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
13782
|
-
var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype"];
|
|
13782
|
+
var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype", "move"];
|
|
13783
13783
|
var dispatchToolSpecs = [
|
|
13784
13784
|
{
|
|
13785
13785
|
name: "dispatch_issue",
|
|
@@ -13901,7 +13901,7 @@ var dispatchToolSpecs = [
|
|
|
13901
13901
|
},
|
|
13902
13902
|
{
|
|
13903
13903
|
name: "dispatch_doc_edit",
|
|
13904
|
-
description: "Apply deterministic document edits,
|
|
13904
|
+
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, and delete or move a whole block by its id. " + "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; a leading '# ' matches a heading. replace is inline: with is the new text of the matched span, so a leading list or heading marker stays literal text. " + "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. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids are the #id of a typed block or a row of GET /api/v1/artifacts/{id}/blocks. ' + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
13905
13905
|
arguments: (z) => ({
|
|
13906
13906
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
13907
13907
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -13909,13 +13909,13 @@ var dispatchToolSpecs = [
|
|
|
13909
13909
|
ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
|
|
13910
13910
|
ops: z.array(z.object({
|
|
13911
13911
|
op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
|
|
13912
|
-
find: z.string().describe("Text of the target
|
|
13913
|
-
with: z.string().describe("Replacement text for replace.").optional(),
|
|
13912
|
+
find: z.string().describe("Text of the target as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading. A delete of a block's entire text removes the block.").optional(),
|
|
13913
|
+
with: z.string().describe("Replacement text for replace, parsed as inline markdown within the matched block; a leading list or heading marker is literal text.").optional(),
|
|
13914
13914
|
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
|
|
13915
13915
|
markdown: z.string().describe("Markdown to insert.").optional(),
|
|
13916
|
-
after: z.string().describe(`Insert after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>".`).optional(),
|
|
13917
|
-
before: z.string().describe(`Insert before this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>".`).optional(),
|
|
13918
|
-
block: z.string().describe("Block id
|
|
13916
|
+
after: z.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(),
|
|
13917
|
+
before: z.string().describe(`Insert or move before this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>", "block:<id>".`).optional(),
|
|
13918
|
+
block: z.string().describe("Block id for retype, delete, or move: the #id of a typed block, or an id from GET /api/v1/artifacts/{id}/blocks.").optional(),
|
|
13919
13919
|
type: z.string().describe("Typed block name for retype.").optional(),
|
|
13920
13920
|
attributes: z.unknown().describe("Typed block attributes for retype.").optional()
|
|
13921
13921
|
})).describe("Flat tagged edits; the server validates fields required for each operation."),
|
|
@@ -13966,7 +13966,7 @@ var dispatchToolSpecs = [
|
|
|
13966
13966
|
},
|
|
13967
13967
|
{
|
|
13968
13968
|
name: "dispatch_read",
|
|
13969
|
-
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain. Do not use it for document " + "contents; use dispatch_doc_read instead. Supply ref, issue, or project plus artifact. " + OWNER_REFERENCE,
|
|
13969
|
+
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain. Do not use it for document " + "contents; use dispatch_doc_read instead. Supply ref, issue, or project plus artifact. " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
|
|
13970
13970
|
arguments: (z) => ({
|
|
13971
13971
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
13972
13972
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -14914,6 +14914,13 @@ class DispatchClient {
|
|
|
14914
14914
|
"references"
|
|
14915
14915
|
]);
|
|
14916
14916
|
}
|
|
14917
|
+
async getReferences(query) {
|
|
14918
|
+
return this.#json("GET", ["api", "v1", "references"], undefined, {
|
|
14919
|
+
..."to" in query ? { to: query.to } : { from: query.from },
|
|
14920
|
+
...query.kind === undefined || query.kind.length === 0 ? {} : { kind: query.kind.join(",") },
|
|
14921
|
+
...query.since === undefined ? {} : { since: query.since }
|
|
14922
|
+
});
|
|
14923
|
+
}
|
|
14917
14924
|
async ensureIssue(issueReference, actor) {
|
|
14918
14925
|
if (!issueReference.includes("#"))
|
|
14919
14926
|
return issueReference;
|
|
@@ -15593,7 +15600,7 @@ function approvalLine(artifact) {
|
|
|
15593
15600
|
return `Approval: changes requested on v${approval.version} by ${approval.by?.id ?? "unknown"}: ${approval.reason ?? ""}`;
|
|
15594
15601
|
}
|
|
15595
15602
|
}
|
|
15596
|
-
function issueSummary(issue, events, references) {
|
|
15603
|
+
function issueSummary(issue, events, references, graph) {
|
|
15597
15604
|
const asks = issue.open_asks;
|
|
15598
15605
|
const spec = issue.artifacts?.find((artifact) => artifact.primary);
|
|
15599
15606
|
const specApproval = spec === undefined ? undefined : approvalLine(spec);
|
|
@@ -15613,10 +15620,35 @@ function issueSummary(issue, events, references) {
|
|
|
15613
15620
|
...typeof references === "string" ? [`- ${references}`] : references.members.length === 0 ? ["- none"] : references.members.map(({ artifact, depth, via }) => `- ${artifact.project}/${artifact.slug} \xB7 depth ${depth} via ${via.kind} ${via.id}`),
|
|
15614
15621
|
...typeof references === "string" || !references.truncated ? [] : ["- more references beyond 8 hops"],
|
|
15615
15622
|
"Events:",
|
|
15616
|
-
...events.length === 0 ? ["- none"] : events.map(eventLine)
|
|
15623
|
+
...events.length === 0 ? ["- none"] : events.map(eventLine),
|
|
15624
|
+
...graph
|
|
15617
15625
|
].join(`
|
|
15618
15626
|
`);
|
|
15619
15627
|
}
|
|
15628
|
+
function referenceLines(edges) {
|
|
15629
|
+
if (typeof edges === "string")
|
|
15630
|
+
return [`- ${edges}`];
|
|
15631
|
+
if (edges.length === 0)
|
|
15632
|
+
return ["- none"];
|
|
15633
|
+
return edges.map((edge) => {
|
|
15634
|
+
const excerpt = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
|
|
15635
|
+
return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt}${edge.created_at})`;
|
|
15636
|
+
});
|
|
15637
|
+
}
|
|
15638
|
+
async function graphEdges(client, query) {
|
|
15639
|
+
try {
|
|
15640
|
+
return (await client.getReferences(query)).edges;
|
|
15641
|
+
} catch (error) {
|
|
15642
|
+
return error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
15643
|
+
}
|
|
15644
|
+
}
|
|
15645
|
+
async function graphSections(client, ref) {
|
|
15646
|
+
const [incoming, outgoing] = await Promise.all([
|
|
15647
|
+
graphEdges(client, { to: ref }),
|
|
15648
|
+
graphEdges(client, { from: ref })
|
|
15649
|
+
]);
|
|
15650
|
+
return ["Referenced by:", ...referenceLines(incoming), "Links:", ...referenceLines(outgoing)];
|
|
15651
|
+
}
|
|
15620
15652
|
function logSummary(issue, events) {
|
|
15621
15653
|
return [
|
|
15622
15654
|
`Key: ${issue.key}`,
|
|
@@ -15664,7 +15696,7 @@ function childrenSummary(issue) {
|
|
|
15664
15696
|
].join(`
|
|
15665
15697
|
`);
|
|
15666
15698
|
}
|
|
15667
|
-
function askSummary({ ask, replies }) {
|
|
15699
|
+
function askSummary({ ask, replies }, graph) {
|
|
15668
15700
|
const answer = ask.answer;
|
|
15669
15701
|
const chain = replies.flatMap((reply) => [
|
|
15670
15702
|
`${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
|
|
@@ -15688,7 +15720,8 @@ function askSummary({ ask, replies }) {
|
|
|
15688
15720
|
`- Reason: ${ask.resolution.reason}`
|
|
15689
15721
|
],
|
|
15690
15722
|
"Replies:",
|
|
15691
|
-
...chain.length === 0 ? ["- none"] : chain
|
|
15723
|
+
...chain.length === 0 ? ["- none"] : chain,
|
|
15724
|
+
...graph
|
|
15692
15725
|
].join(`
|
|
15693
15726
|
`);
|
|
15694
15727
|
}
|
|
@@ -15731,7 +15764,7 @@ function formatOpenAsksSummary(response, baseUrl) {
|
|
|
15731
15764
|
].join(`
|
|
15732
15765
|
`);
|
|
15733
15766
|
}
|
|
15734
|
-
function commentSummary({ comment, replies }) {
|
|
15767
|
+
function commentSummary({ comment, replies }, graph) {
|
|
15735
15768
|
const root = [
|
|
15736
15769
|
`${comment.id} \xB7 ${comment.author.kind} ${comment.author.id}`,
|
|
15737
15770
|
...comment.anchor?.quote === undefined ? [] : [`> ${comment.anchor.quote}`],
|
|
@@ -15742,10 +15775,16 @@ function commentSummary({ comment, replies }) {
|
|
|
15742
15775
|
...reply.anchor?.quote === undefined ? [] : [`> ${reply.anchor.quote}`],
|
|
15743
15776
|
`Body: ${reply.body}`
|
|
15744
15777
|
]);
|
|
15745
|
-
return [
|
|
15778
|
+
return [
|
|
15779
|
+
"Comment:",
|
|
15780
|
+
...root,
|
|
15781
|
+
"Reply chain:",
|
|
15782
|
+
...chain.length === 0 ? ["- none"] : chain,
|
|
15783
|
+
...graph
|
|
15784
|
+
].join(`
|
|
15746
15785
|
`);
|
|
15747
15786
|
}
|
|
15748
|
-
function messageSummary({ message, replies }) {
|
|
15787
|
+
function messageSummary({ message, replies }, graph) {
|
|
15749
15788
|
const root = [
|
|
15750
15789
|
`${message.id} \xB7 ${message.author.kind} ${message.author.id}`,
|
|
15751
15790
|
`Body: ${message.body}`
|
|
@@ -15754,7 +15793,13 @@ function messageSummary({ message, replies }) {
|
|
|
15754
15793
|
`${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
|
|
15755
15794
|
`Body: ${reply.body}`
|
|
15756
15795
|
]);
|
|
15757
|
-
return [
|
|
15796
|
+
return [
|
|
15797
|
+
"Message:",
|
|
15798
|
+
...root,
|
|
15799
|
+
"Reply chain:",
|
|
15800
|
+
...chain.length === 0 ? ["- none"] : chain,
|
|
15801
|
+
...graph
|
|
15802
|
+
].join(`
|
|
15758
15803
|
`);
|
|
15759
15804
|
}
|
|
15760
15805
|
async function openArtifactMarks(client, resolved) {
|
|
@@ -16134,8 +16179,9 @@ ${trailer.join(`
|
|
|
16134
16179
|
const ref = ownerArguments.ref;
|
|
16135
16180
|
const id = await resolveIdPrefix(input.tool, "ask", ref, async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id, "all"));
|
|
16136
16181
|
const askRead = await client.getAsk(id);
|
|
16182
|
+
const askRef = ref.owner.kind === "issue" ? `dispatch://${ref.owner.issue}/ask/${id}` : `dispatch://${ref.owner.project}/artifact/${ref.artifact}/ask/${id}`;
|
|
16137
16183
|
return {
|
|
16138
|
-
text: askSummary(askRead),
|
|
16184
|
+
text: askSummary(askRead, await graphSections(client, askRef)),
|
|
16139
16185
|
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue }
|
|
16140
16186
|
};
|
|
16141
16187
|
}
|
|
@@ -16143,8 +16189,9 @@ ${trailer.join(`
|
|
|
16143
16189
|
const ref = ownerArguments.ref;
|
|
16144
16190
|
const id = await resolveIdPrefix(input.tool, "comment", ref, async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id));
|
|
16145
16191
|
const comment = await client.getComment(id);
|
|
16192
|
+
const commentRef = ref.owner.kind === "issue" ? `dispatch://${ref.owner.issue}/comment/${id}` : `dispatch://${ref.owner.project}/artifact/${ref.artifact}/comment/${id}`;
|
|
16146
16193
|
return {
|
|
16147
|
-
text: commentSummary(comment),
|
|
16194
|
+
text: commentSummary(comment, await graphSections(client, commentRef)),
|
|
16148
16195
|
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key }
|
|
16149
16196
|
};
|
|
16150
16197
|
}
|
|
@@ -16153,19 +16200,22 @@ ${trailer.join(`
|
|
|
16153
16200
|
throw new Error("message references are issue-scoped");
|
|
16154
16201
|
}
|
|
16155
16202
|
const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
|
|
16203
|
+
const messageRef = `dispatch://${ownerArguments.ref.owner.issue}/message/${ownerArguments.ref.id}`;
|
|
16156
16204
|
return {
|
|
16157
|
-
text: messageSummary(messageRead),
|
|
16205
|
+
text: messageSummary(messageRead, await graphSections(client, messageRef)),
|
|
16158
16206
|
details: { issue: messageRead.message.issue_key }
|
|
16159
16207
|
};
|
|
16160
16208
|
}
|
|
16161
16209
|
if (documentOwner().kind === "project") {
|
|
16162
16210
|
const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
|
|
16211
|
+
const documentRef = `dispatch://${resolved.artifact.project}/artifact/${resolved.artifact.slug}`;
|
|
16163
16212
|
return {
|
|
16164
16213
|
text: [
|
|
16165
16214
|
`Document: ${resolved.artifact.project} / ${resolved.artifact.name}`,
|
|
16166
|
-
`Reference:
|
|
16215
|
+
`Reference: ${documentRef}`,
|
|
16167
16216
|
`Versions: ${resolved.artifact.versions.length}`,
|
|
16168
|
-
...approvalLine(resolved.artifact) === undefined ? [] : [approvalLine(resolved.artifact)]
|
|
16217
|
+
...approvalLine(resolved.artifact) === undefined ? [] : [approvalLine(resolved.artifact)],
|
|
16218
|
+
...await graphSections(client, documentRef)
|
|
16169
16219
|
].join(`
|
|
16170
16220
|
`),
|
|
16171
16221
|
details: {
|
|
@@ -16194,7 +16244,7 @@ ${trailer.join(`
|
|
|
16194
16244
|
references = error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
16195
16245
|
}
|
|
16196
16246
|
return {
|
|
16197
|
-
text: issueSummary(read.issue, read.events, references),
|
|
16247
|
+
text: issueSummary(read.issue, read.events, references, await graphSections(client, `dispatch://${read.issue.key}`)),
|
|
16198
16248
|
details: { issue: read.issue.key }
|
|
16199
16249
|
};
|
|
16200
16250
|
}
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -253,7 +253,7 @@ exact `EditOp` shape:
|
|
|
253
253
|
|
|
254
254
|
```ts
|
|
255
255
|
type EditOp = {
|
|
256
|
-
op: "replace" | "delete" | "insert" | "retype";
|
|
256
|
+
op: "replace" | "delete" | "insert" | "retype" | "move";
|
|
257
257
|
find?: string;
|
|
258
258
|
with?: string;
|
|
259
259
|
occurrence?: number;
|
|
@@ -271,18 +271,34 @@ code without backticks, bold without asterisks, and link text without link synta
|
|
|
271
271
|
anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay
|
|
272
272
|
within one textblock; split changes that span separate blocks into separate operations.
|
|
273
273
|
|
|
274
|
-
`replace` requires `find` and `with`; `delete` requires `find`; `insert` requires `markdown` and exactly one of `after` or
|
|
275
|
-
insert anchor is a quote, `"start"`, `"end"`,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
`
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
`replace` requires `find` and `with`; `delete` requires `find` or `block`; `insert` requires `markdown` and exactly one of `after` or
|
|
275
|
+
`before`; `move` requires `block` and exactly one of `after` or `before`. An insert or move anchor is a quote, `"start"`, `"end"`,
|
|
276
|
+
`"heading:Title"`, or `"block:<id>"`. Ordinary inserts create a sibling block before or after the quote, heading, or block's enclosing
|
|
277
|
+
document block, and a move lands the block at that same boundary; `"start"` and `"end"` select the document edges. At a table-cell
|
|
278
|
+
quote, a body-row fragment (no header or delimiter rows) extends that table before or after the matched row instead; short rows are
|
|
279
|
+
padded, wider rows are rejected, and deleting a cell's quoted text removes only that text. A `find` or quote anchor tolerates inline
|
|
280
|
+
Markdown (`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the three nearest blocks so the next
|
|
281
|
+
quote lands.
|
|
282
|
+
|
|
283
|
+
`replace` is inline: `with` is the new text of the matched span inside its block, so a leading list or heading marker (`4. Design`,
|
|
284
|
+
`# Title`) stays literal text and never turns the block into a list or heading; `with` that forms more than one paragraph is rejected
|
|
285
|
+
(`INVALID_OP` on `with`) — delete the block and insert new blocks instead. Use zero-based `occurrence` for a repeated target; re-read a
|
|
286
|
+
missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
|
|
287
|
+
|
|
288
|
+
A `delete` whose `find` is a block's entire text removes the block itself — the bullet, paragraph, or heading, not just its words — and
|
|
289
|
+
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
|
|
290
|
+
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
|
|
291
|
+
(paragraphs, code, tables) is refused with `INVALID_OP` naming `delete {block:"<item id>"}`, which removes the item with its content.
|
|
292
|
+
`delete` with `block` removes any block by id (paragraph, heading, list, list item, table, or typed block; deleting an open `ask` block
|
|
293
|
+
retracts its ask, while an answered one keeps its answer as the record), and `move` with `block` relocates one, keeping its id and
|
|
294
|
+
attributes — a moved `ask` keeps its ask and answer. Block ids are the `#id` a typed block renders (`:::ask{#5467e5ce-…}`) and, for
|
|
295
|
+
every block including untyped ones, the `id` rows of `GET /api/v1/artifacts/{id}/blocks` (or `/api/v1/issues/{key}/artifacts/{slug}/blocks`),
|
|
296
|
+
each with its `type` and byte range in the canonical markdown. A move whose anchor lies inside the moved block, or a delete that would
|
|
297
|
+
leave a typed block without the body its content rule requires, is `INVALID_OP` naming the field and the rule.
|
|
298
|
+
|
|
299
|
+
`retype` turns the paragraph or typed block with `block` into the named typed `type` in place. It keeps the
|
|
300
|
+
block id, keeps a typed block's body, and uses `attributes` for client-owned typed attributes. Use it when
|
|
301
|
+
an existing paragraph is the question that should become a decision.
|
|
286
302
|
|
|
287
303
|
## Typed blocks
|
|
288
304
|
|
|
@@ -459,6 +475,14 @@ document owner or ref, it returns a document summary with `details` `{ project,
|
|
|
459
475
|
question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. With a
|
|
460
476
|
message ref, it returns that message and its reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
|
|
461
477
|
|
|
478
|
+
Every read ends with two sections from the reference graph. `Referenced by:` lists what points at the node — every document, ask,
|
|
479
|
+
comment, or message that cites it, plus its structure: child issues, attached documents, anchored and owned asks and comments, replies,
|
|
480
|
+
followers — and `Links:` lists what it cites. Each row is `- <edge kind> <node kind> dispatch://… (<excerpt> · <when>)`; for a
|
|
481
|
+
document source the excerpt is the block containing the mention. Cross-project, always: a message on another project's issue that
|
|
482
|
+
cites an ask shows up under that ask. So "what led to this decision" is one `dispatch_read` on the ask, and "who relies on this
|
|
483
|
+
document" one read on the document. Cite with `dispatch://` references (below) whenever you name a node in a body — a bare id or
|
|
484
|
+
title is invisible to the graph.
|
|
485
|
+
|
|
462
486
|
## References
|
|
463
487
|
|
|
464
488
|
Use these in document, ask, comment, and message bodies. In the dashboard, a reference renders
|