@sjawhar/opencode-legion-envoy 11.0.0 → 11.2.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/dispatch.js
CHANGED
|
@@ -14418,11 +14418,12 @@ var DOC_EDIT_OPS = [
|
|
|
14418
14418
|
"delete_row",
|
|
14419
14419
|
"delete_column"
|
|
14420
14420
|
];
|
|
14421
|
+
var RELATED_CHECK_RESULT = "Dispatch then runs the related-item check (waiting at most five seconds; dispatch related runs " + "it without writing), and the result says, right after the line naming what was created, " + "which items it found and how it searched, that it searched and found nothing, or that no " + "check was made, why, and the dispatch search to run instead.";
|
|
14421
14422
|
var dispatchToolSpecs = [
|
|
14422
14423
|
{
|
|
14423
14424
|
name: "dispatch_issue",
|
|
14424
14425
|
example: { project: "DSP", title: "Native workspace" },
|
|
14425
|
-
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "When passing a spec, before calling, have a fresh read-only reader check it against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + "Create a project only when a person asks for one; work that spans projects goes in the project that owns most of it. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
14426
|
+
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "When passing a spec, before calling, have a fresh read-only reader check it against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + "Create a project only when a person asks for one; work that spans projects goes in the project that owns most of it. " + `${RELATED_CHECK_RESULT} With --force, the result also names each issue whose title you forced past. ` + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
14426
14427
|
arguments: (z) => ({
|
|
14427
14428
|
project: z.string().describe("Project key for the new issue."),
|
|
14428
14429
|
title: z.string({ max: ISSUE_TITLE_MAX }).describe("Concise issue title, at most 1,000 characters."),
|
|
@@ -14495,7 +14496,7 @@ var dispatchToolSpecs = [
|
|
|
14495
14496
|
}
|
|
14496
14497
|
]
|
|
14497
14498
|
},
|
|
14498
|
-
description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch doc-edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch request-approval. Do not use this tool for a status " + "update or discussion; use dispatch message instead. Never create an issue to park a " + "question: the question is an ask on the issue it concerns. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. Measure what you can first, and put the measurement and the size of the " + "population it covers in the question; drop an option that cannot fix most of that " + "population, never offer it cut down, and keep an option not to act (Hold). Set urgency high " + "when your work halts on the answer, and keep working on everything else. " + "Anchor a to-do about a document passage, 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, ` + `the lines images appends included, and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
14499
|
+
description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch doc-edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch request-approval. Do not use this tool for a status " + "update or discussion; use dispatch message instead. Never create an issue to park a " + "question: the question is an ask on the issue it concerns. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. Measure what you can first, and put the measurement and the size of the " + "population it covers in the question; drop an option that cannot fix most of that " + "population, never offer it cut down, and keep an option not to act (Hold). Set urgency high " + "when your work halts on the answer, and keep working on everything else. " + "Anchor a to-do about a document passage, 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, ` + `the lines images appends included, and has at most 8 options. ${RELATED_CHECK_RESULT} ` + `An ask among the related items says whether it is answered, by whom and with which options. ${OWNER_REFERENCE}`,
|
|
14499
14500
|
arguments: (z) => ({
|
|
14500
14501
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
14501
14502
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -14645,7 +14646,7 @@ var dispatchToolSpecs = [
|
|
|
14645
14646
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
14646
14647
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
14647
14648
|
},
|
|
14648
|
-
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. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. 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 - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "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. In a table cell, with is the cell's text: write a | in it bare, in code too, since code whose text has an odd run of backslashes right before a | cannot be stored in a cell and is INVALID_OP, naming the cell. " + "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. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. 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>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --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. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14649
|
+
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. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. 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 - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "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. In a table cell, with is the cell's text: write a | in it bare, in code too, since code whose text has an odd run of backslashes right before a | cannot be stored in a cell and is INVALID_OP, naming the cell. " + "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. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. 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>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --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. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "Each decision block the edit adds gets the related-item check dispatch ask gets, waiting at most five seconds for all of them together: the result prints, after its first line, `Decision block <id>:` and what that check found, that it found nothing, or that it was not made and why. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14649
14650
|
arguments: (z) => ({
|
|
14650
14651
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
14651
14652
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -14759,6 +14760,25 @@ var dispatchToolSpecs = [
|
|
|
14759
14760
|
message: `project is not a project key: ${PROJECT_KEY_RULE}`
|
|
14760
14761
|
}
|
|
14761
14762
|
},
|
|
14763
|
+
{
|
|
14764
|
+
name: "dispatch_related",
|
|
14765
|
+
example: { project: "CORE", title: "Retry failed webhook deliveries" },
|
|
14766
|
+
description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issue leaves that issue's own items out, " + "as a check on that issue's own write does; keywords_only skips meaning search.",
|
|
14767
|
+
arguments: (z) => ({
|
|
14768
|
+
project: z.string().describe("Project key to search within, such as CORE."),
|
|
14769
|
+
title: z.string({ max: ISSUE_TITLE_MAX }).describe("Optional title to check, as an issue title; at most 1,000 characters.").optional(),
|
|
14770
|
+
text: z.string().describe("Optional text to check, such as a draft spec or a question.").optional(),
|
|
14771
|
+
exclude_issue: z.string().describe("Optional issue key in the project whose own items are left out.").optional(),
|
|
14772
|
+
keywords_only: z.boolean().describe("Search by keywords only, skipping meaning search.").optional()
|
|
14773
|
+
}),
|
|
14774
|
+
validation: {
|
|
14775
|
+
check: (value) => {
|
|
14776
|
+
const { project } = value;
|
|
14777
|
+
return typeof project !== "string" || PROJECT_KEY_PATTERN.test(project);
|
|
14778
|
+
},
|
|
14779
|
+
message: `project is not a project key: ${PROJECT_KEY_RULE}`
|
|
14780
|
+
}
|
|
14781
|
+
},
|
|
14762
14782
|
{
|
|
14763
14783
|
name: "dispatch_issues",
|
|
14764
14784
|
example: { project: "PROJ", limit: 250, offset: 250 },
|
|
@@ -15791,7 +15811,8 @@ class DispatchClient {
|
|
|
15791
15811
|
this.#signal = requestSignal(signal);
|
|
15792
15812
|
}
|
|
15793
15813
|
async issue(input) {
|
|
15794
|
-
|
|
15814
|
+
const answer = await this.#jsonAnswer("POST", ["api", "v1", "issues"], input);
|
|
15815
|
+
return { ...answer.payload, created: answer.status === 201 };
|
|
15795
15816
|
}
|
|
15796
15817
|
async resolveIssue(issueReference) {
|
|
15797
15818
|
return this.#resolveIssue(issueReference);
|
|
@@ -15841,6 +15862,9 @@ class DispatchClient {
|
|
|
15841
15862
|
async search(query, options = {}) {
|
|
15842
15863
|
return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
|
|
15843
15864
|
}
|
|
15865
|
+
async related(project, input) {
|
|
15866
|
+
return this.#json("POST", ["api", "v1", "projects", project, "related"], input);
|
|
15867
|
+
}
|
|
15844
15868
|
async getIssue(issue) {
|
|
15845
15869
|
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue)]);
|
|
15846
15870
|
}
|
|
@@ -17035,6 +17059,134 @@ function searchAnswer(search, query, offset, configUrl) {
|
|
|
17035
17059
|
};
|
|
17036
17060
|
}
|
|
17037
17061
|
|
|
17062
|
+
// ../envoy-client/src/related-check.ts
|
|
17063
|
+
function relatedCheckLines(suggestions, context) {
|
|
17064
|
+
if (suggestions === null || suggestions === undefined) {
|
|
17065
|
+
return [notMadeLine("the server sent no check result", context)];
|
|
17066
|
+
}
|
|
17067
|
+
let lines;
|
|
17068
|
+
try {
|
|
17069
|
+
lines = relatedCheckForm(suggestions, context);
|
|
17070
|
+
} catch (error) {
|
|
17071
|
+
lines = [
|
|
17072
|
+
notMadeLine(`this client could not read the check result (${messageFor(error)})`, context)
|
|
17073
|
+
];
|
|
17074
|
+
}
|
|
17075
|
+
try {
|
|
17076
|
+
const { decision } = suggestions;
|
|
17077
|
+
if (decision !== undefined && decision !== null) {
|
|
17078
|
+
const when = decision.answered_at === undefined ? "" : ` on ${decision.answered_at}`;
|
|
17079
|
+
lines.push(`A past decision may already answer this: ${suggestionLabel(decision)}, answered by ${decision.answered_by}${when} \u2192 ${absoluteHref(decision.href, context.configUrl)}`);
|
|
17080
|
+
lines.push(...askLines(decision.ask));
|
|
17081
|
+
}
|
|
17082
|
+
for (const match of suggestions.title_matches ?? []) {
|
|
17083
|
+
lines.push(`Title overlaps you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
|
|
17084
|
+
}
|
|
17085
|
+
} catch (error) {
|
|
17086
|
+
lines.push(`The check's past decision or title overlaps could not be shown: ${messageFor(error)}`);
|
|
17087
|
+
}
|
|
17088
|
+
return lines;
|
|
17089
|
+
}
|
|
17090
|
+
function decisionBlockLines(advice, context) {
|
|
17091
|
+
try {
|
|
17092
|
+
const checks = advice?.decision_block_checks;
|
|
17093
|
+
if (checks === undefined || checks === null) {
|
|
17094
|
+
const added = advice?.decision_blocks_added ?? 0;
|
|
17095
|
+
if (added === 0)
|
|
17096
|
+
return [];
|
|
17097
|
+
return [
|
|
17098
|
+
`${added} new decision ${added === 1 ? "block" : "blocks"}:`,
|
|
17099
|
+
notMadeLine("the server sent no check result", context)
|
|
17100
|
+
];
|
|
17101
|
+
}
|
|
17102
|
+
return checks.flatMap((check) => [
|
|
17103
|
+
`Decision block ${check.block_id}:`,
|
|
17104
|
+
...relatedCheckLines(check.suggestions, { ...context, text: check.question ?? "" })
|
|
17105
|
+
]);
|
|
17106
|
+
} catch (error) {
|
|
17107
|
+
return [
|
|
17108
|
+
notMadeLine(`this client could not read the decision blocks' checks (${messageFor(error)})`, context)
|
|
17109
|
+
];
|
|
17110
|
+
}
|
|
17111
|
+
}
|
|
17112
|
+
function relatedCheckForm(suggestions, context) {
|
|
17113
|
+
const { outcome, missing } = suggestions;
|
|
17114
|
+
if (outcome === "not_made" || outcome === undefined && missing !== undefined) {
|
|
17115
|
+
return [notMadeLine(missing ?? "the server gave no reason", context, suggestions.search_query)];
|
|
17116
|
+
}
|
|
17117
|
+
const related = suggestions.related ?? [];
|
|
17118
|
+
const mode = searchModeWords(suggestions);
|
|
17119
|
+
if (related.length > 0) {
|
|
17120
|
+
const header = mode === undefined ? "Related items:" : `Related items (searched by ${mode}):`;
|
|
17121
|
+
return [header, ...related.flatMap((item) => relatedItemLines(item, context.configUrl))];
|
|
17122
|
+
}
|
|
17123
|
+
if (outcome === undefined) {
|
|
17124
|
+
return [
|
|
17125
|
+
notMadeLine("the server did not say whether it searched", context, suggestions.search_query)
|
|
17126
|
+
];
|
|
17127
|
+
}
|
|
17128
|
+
if (outcome === "found" || outcome === "none") {
|
|
17129
|
+
return [
|
|
17130
|
+
mode === undefined ? "Related-item check searched and found nothing." : `Related-item check searched (${mode}) and found nothing.`
|
|
17131
|
+
];
|
|
17132
|
+
}
|
|
17133
|
+
return [
|
|
17134
|
+
notMadeLine(`the server answered an outcome this client does not know (${String(outcome)})`, context, suggestions.search_query)
|
|
17135
|
+
];
|
|
17136
|
+
}
|
|
17137
|
+
function searchModeWords(suggestions) {
|
|
17138
|
+
const { search, search_note: note } = suggestions;
|
|
17139
|
+
if (search === undefined)
|
|
17140
|
+
return;
|
|
17141
|
+
if (search === "meaning_and_keywords")
|
|
17142
|
+
return "meaning and keywords";
|
|
17143
|
+
const words = search === "keywords_only" ? "keywords only" : String(search);
|
|
17144
|
+
return note === undefined || note === "" ? words : `${words}: ${note}`;
|
|
17145
|
+
}
|
|
17146
|
+
function relatedItemLines(item, configUrl) {
|
|
17147
|
+
try {
|
|
17148
|
+
const lines = [`- ${suggestionLabel(item)} \u2192 ${absoluteHref(item.href, configUrl)}`];
|
|
17149
|
+
if (item.kind === "ask") {
|
|
17150
|
+
lines.push(...askLines(item.ask));
|
|
17151
|
+
}
|
|
17152
|
+
return lines;
|
|
17153
|
+
} catch (error) {
|
|
17154
|
+
const href = item?.href;
|
|
17155
|
+
return [`- ${String(href)} (this client could not show it: ${messageFor(error)})`];
|
|
17156
|
+
}
|
|
17157
|
+
}
|
|
17158
|
+
function askLines(ask) {
|
|
17159
|
+
try {
|
|
17160
|
+
const lines = askStateLines(ask ?? undefined);
|
|
17161
|
+
if (!Array.isArray(lines))
|
|
17162
|
+
throw new Error(`unknown ask state ${String(ask?.state)}`);
|
|
17163
|
+
return lines;
|
|
17164
|
+
} catch (error) {
|
|
17165
|
+
return [` This client could not show where the ask stands: ${messageFor(error)}`];
|
|
17166
|
+
}
|
|
17167
|
+
}
|
|
17168
|
+
function suggestionLabel(item) {
|
|
17169
|
+
if (item.owner.kind === "issue") {
|
|
17170
|
+
return `${item.owner.key} [${item.owner.status}] ${item.owner.title}`;
|
|
17171
|
+
}
|
|
17172
|
+
return `${item.owner.name} (${item.owner.project}/${item.owner.slug})`;
|
|
17173
|
+
}
|
|
17174
|
+
function absoluteHref(href, configUrl) {
|
|
17175
|
+
try {
|
|
17176
|
+
return new URL(href, configUrl).toString();
|
|
17177
|
+
} catch {
|
|
17178
|
+
return String(href);
|
|
17179
|
+
}
|
|
17180
|
+
}
|
|
17181
|
+
function notMadeLine(reason, context, searchQuery) {
|
|
17182
|
+
const words = searchQuery?.trim() || searchWords(context.text);
|
|
17183
|
+
const query = words === "" ? " with two or three of its words as --query" : ` --query '${words.replaceAll("'", "'\\''")}'`;
|
|
17184
|
+
return `No related-item check was made: ${reason}. Search before you rely on this being new: dispatch search --project ${context.project}${query}`;
|
|
17185
|
+
}
|
|
17186
|
+
function searchWords(text) {
|
|
17187
|
+
return (text.match(/[\p{L}\p{N}_]+/gu) ?? []).filter((word) => (word.match(/\p{L}/gu) ?? []).length >= 4).slice(0, 3).join(" ");
|
|
17188
|
+
}
|
|
17189
|
+
|
|
17038
17190
|
// ../envoy-client/src/dispatch-execute.ts
|
|
17039
17191
|
function documentTopic(artifact) {
|
|
17040
17192
|
return documentTopicOf(artifact.project, artifact.slug);
|
|
@@ -17094,31 +17246,12 @@ function renderAdvice(tool, key, advice, opts) {
|
|
|
17094
17246
|
}
|
|
17095
17247
|
return lines;
|
|
17096
17248
|
}
|
|
17097
|
-
function
|
|
17098
|
-
|
|
17099
|
-
return
|
|
17100
|
-
|
|
17101
|
-
return [`
|
|
17102
|
-
}
|
|
17103
|
-
const lines = [];
|
|
17104
|
-
if (suggestions.related.length > 0) {
|
|
17105
|
-
lines.push("Possibly related, found by search:");
|
|
17106
|
-
for (const item of suggestions.related) {
|
|
17107
|
-
lines.push(`- ${suggestionLabel(item)} \u2192 ${new URL(item.href, configUrl).toString()}`);
|
|
17108
|
-
}
|
|
17109
|
-
}
|
|
17110
|
-
if (suggestions.decision !== undefined) {
|
|
17111
|
-
const item = suggestions.decision;
|
|
17112
|
-
const when = item.answered_at === undefined ? "" : ` on ${item.answered_at}`;
|
|
17113
|
-
lines.push(`A past decision may already answer this: ${suggestionLabel(item)}, answered by ${item.answered_by}${when} \u2192 ${new URL(item.href, configUrl).toString()}`);
|
|
17114
|
-
}
|
|
17115
|
-
return lines;
|
|
17116
|
-
}
|
|
17117
|
-
function suggestionLabel(item) {
|
|
17118
|
-
if (item.owner.kind === "issue") {
|
|
17119
|
-
return `${item.owner.key} [${item.owner.status}] ${item.owner.title}`;
|
|
17249
|
+
async function otherAdviceLines(render) {
|
|
17250
|
+
try {
|
|
17251
|
+
return await render();
|
|
17252
|
+
} catch (error) {
|
|
17253
|
+
return [`Other advice could not be shown: ${messageFor(error)}`];
|
|
17120
17254
|
}
|
|
17121
|
-
return `${item.owner.name} (${item.owner.project}/${item.owner.slug})`;
|
|
17122
17255
|
}
|
|
17123
17256
|
function documentResultDetails(artifact) {
|
|
17124
17257
|
return {
|
|
@@ -17171,6 +17304,7 @@ var issueFreeTools = {
|
|
|
17171
17304
|
dispatch_resolve_comment: true,
|
|
17172
17305
|
dispatch_follow: true,
|
|
17173
17306
|
dispatch_search: true,
|
|
17307
|
+
dispatch_related: true,
|
|
17174
17308
|
dispatch_issues: true,
|
|
17175
17309
|
dispatch_project: true,
|
|
17176
17310
|
dispatch_projects: true,
|
|
@@ -18730,20 +18864,27 @@ async function executeDispatchTool(input) {
|
|
|
18730
18864
|
...Array.isArray(labels) ? { labels } : {},
|
|
18731
18865
|
actor
|
|
18732
18866
|
});
|
|
18733
|
-
refuseUnsavedRepository("created", created, repo);
|
|
18734
|
-
const
|
|
18735
|
-
|
|
18736
|
-
|
|
18737
|
-
|
|
18738
|
-
|
|
18739
|
-
|
|
18740
|
-
|
|
18741
|
-
|
|
18867
|
+
refuseUnsavedRepository(created.created ? "created" : "already holds", created, repo);
|
|
18868
|
+
const adviceLines = await otherAdviceLines(async () => {
|
|
18869
|
+
const componentGuidance = await architectureGuidance(client, created.project, created.components);
|
|
18870
|
+
return [
|
|
18871
|
+
...renderAdvice(input.tool, created.key, created.advice, {
|
|
18872
|
+
isPrimarySpec: spec !== undefined
|
|
18873
|
+
}),
|
|
18874
|
+
...componentGuidance === undefined ? [] : [componentGuidance]
|
|
18875
|
+
];
|
|
18876
|
+
});
|
|
18742
18877
|
const href = new URL(`/issues/${encodeURIComponent(created.key)}`, linkBase).toString();
|
|
18743
18878
|
const assigned = created.assignee === undefined ? "" : created.assignee === null ? "; unassigned" : `; assigned to ${created.assignee}`;
|
|
18879
|
+
const suffixes = `${assigned}${await notSubscribed(issueTopic(created.key), input.subscriptions)}`;
|
|
18744
18880
|
return {
|
|
18745
18881
|
text: [
|
|
18746
|
-
`Created ${created.key}: ${created.title} \u2192 ${href}${
|
|
18882
|
+
created.created ? `Created ${created.key}: ${created.title} \u2192 ${href}${suffixes}` : `Dispatch already holds ${created.key}: ${created.title} \u2192 ${href}; nothing was created${suffixes}`,
|
|
18883
|
+
...created.created ? relatedCheckLines(created.advice?.suggestions, {
|
|
18884
|
+
project: created.project,
|
|
18885
|
+
text: title,
|
|
18886
|
+
configUrl: linkBase
|
|
18887
|
+
}) : [],
|
|
18747
18888
|
...adviceLines
|
|
18748
18889
|
].join(`
|
|
18749
18890
|
`),
|
|
@@ -18931,6 +19072,29 @@ async function executeDispatchTool(input) {
|
|
|
18931
19072
|
});
|
|
18932
19073
|
return searchAnswer(search, query, offset, linkBase);
|
|
18933
19074
|
}
|
|
19075
|
+
case "dispatch_related": {
|
|
19076
|
+
const project = stringArg(args, "project");
|
|
19077
|
+
const title = optionalString(args, "title");
|
|
19078
|
+
const text = optionalString(args, "text");
|
|
19079
|
+
const excludeIssue = optionalString(args, "exclude_issue");
|
|
19080
|
+
const keywordsOnly = optionalBoolean(args, "keywords_only");
|
|
19081
|
+
const suggestions = await client.related(project, {
|
|
19082
|
+
...title === undefined ? {} : { title },
|
|
19083
|
+
...text === undefined ? {} : { text },
|
|
19084
|
+
...excludeIssue === undefined ? {} : { exclude_issue: excludeIssue },
|
|
19085
|
+
...keywordsOnly === undefined ? {} : { keywords_only: keywordsOnly }
|
|
19086
|
+
});
|
|
19087
|
+
return {
|
|
19088
|
+
text: relatedCheckLines(suggestions, {
|
|
19089
|
+
project,
|
|
19090
|
+
text: [title, text].filter((part) => part !== undefined).join(`
|
|
19091
|
+
`),
|
|
19092
|
+
configUrl: linkBase
|
|
19093
|
+
}).join(`
|
|
19094
|
+
`),
|
|
19095
|
+
details: { project, suggestions }
|
|
19096
|
+
};
|
|
19097
|
+
}
|
|
18934
19098
|
case "dispatch_issues": {
|
|
18935
19099
|
const project = stringArg(args, "project");
|
|
18936
19100
|
const status = optionalString(args, "status");
|
|
@@ -19093,14 +19257,17 @@ async function executeDispatchTool(input) {
|
|
|
19093
19257
|
};
|
|
19094
19258
|
const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue(), askInput);
|
|
19095
19259
|
const askOwner = ask.issue_key !== null ? issueTopic(ask.issue_key) : resolved === undefined ? issueTopic(issue()) : documentTopic(resolved.artifact);
|
|
19096
|
-
const
|
|
19097
|
-
|
|
19098
|
-
|
|
19099
|
-
|
|
19260
|
+
const checkLines = relatedCheckLines(ask.advice?.suggestions, {
|
|
19261
|
+
project: resolved?.artifact.project ?? (ask.issue_key ?? "").replace(/-[0-9]+$/, ""),
|
|
19262
|
+
text: stringArg(args, "question"),
|
|
19263
|
+
configUrl: linkBase
|
|
19264
|
+
});
|
|
19265
|
+
const adviceLines = await otherAdviceLines(() => renderAdvice(input.tool, askOwner.label, ask.advice, {}));
|
|
19100
19266
|
return {
|
|
19101
19267
|
text: [
|
|
19102
|
-
`Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
|
|
19103
|
-
|
|
19268
|
+
`Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}`,
|
|
19269
|
+
...checkLines,
|
|
19270
|
+
await followsAsk(askOwner, input.subscriptions),
|
|
19104
19271
|
...adviceLines
|
|
19105
19272
|
].join(`
|
|
19106
19273
|
`),
|
|
@@ -19297,11 +19464,18 @@ ${await followsAsk(askOwner, input.subscriptions)}`,
|
|
|
19297
19464
|
const lostOps = edited.lost_ops;
|
|
19298
19465
|
const lostText = lostOps === undefined || lostOps !== null && lostOps.length === 0 ? "" : lostOps === null ? "; could not confirm this edit survived, because the live document is being reloaded or holds a tree too deep to read \u2014 re-read it" : `; ${versionText} carries text the live document no longer has: a concurrent change removed what ${lostOps.length === 1 ? "operation" : "operations"} ${lostOps.join(", ")} wrote \u2014 re-read the document`;
|
|
19299
19466
|
const applied = `${head}${unchangedText}${lostText}`;
|
|
19300
|
-
const
|
|
19467
|
+
const editAskText = ops.map((operation) => /^:{3,}ask\{.*\n([\s\S]*)/mu.exec(operation.markdown ?? operation.with ?? "")?.[1]).find((text) => text !== undefined) ?? resolved.issue?.title ?? "";
|
|
19468
|
+
const checkLines = decisionBlockLines(edited.advice, {
|
|
19469
|
+
project: resolved.artifact.project,
|
|
19470
|
+
text: editAskText,
|
|
19471
|
+
configUrl: linkBase
|
|
19472
|
+
});
|
|
19473
|
+
const adviceLines = await otherAdviceLines(() => renderAdvice(input.tool, resolvedTopic(resolved).label, edited.advice, {}));
|
|
19301
19474
|
const tokenTrailer = edited.token === undefined ? [] : [`Document token: ${edited.token}`];
|
|
19302
19475
|
return {
|
|
19303
19476
|
text: [
|
|
19304
19477
|
`${applied}${await notSubscribed(resolvedTopic(resolved), input.subscriptions)}`,
|
|
19478
|
+
...checkLines,
|
|
19305
19479
|
...tokenTrailer,
|
|
19306
19480
|
...adviceLines
|
|
19307
19481
|
].join(`
|
package/dist/src/server.js
CHANGED
|
@@ -13936,11 +13936,12 @@ var DOC_EDIT_OPS = [
|
|
|
13936
13936
|
"delete_row",
|
|
13937
13937
|
"delete_column"
|
|
13938
13938
|
];
|
|
13939
|
+
var RELATED_CHECK_RESULT = "Dispatch then runs the related-item check (waiting at most five seconds; dispatch related runs " + "it without writing), and the result says, right after the line naming what was created, " + "which items it found and how it searched, that it searched and found nothing, or that no " + "check was made, why, and the dispatch search to run instead.";
|
|
13939
13940
|
var dispatchToolSpecs = [
|
|
13940
13941
|
{
|
|
13941
13942
|
name: "dispatch_issue",
|
|
13942
13943
|
example: { project: "DSP", title: "Native workspace" },
|
|
13943
|
-
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "When passing a spec, before calling, have a fresh read-only reader check it against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + "Create a project only when a person asks for one; work that spans projects goes in the project that owns most of it. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
13944
|
+
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "When passing a spec, before calling, have a fresh read-only reader check it against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + "Create a project only when a person asks for one; work that spans projects goes in the project that owns most of it. " + `${RELATED_CHECK_RESULT} With --force, the result also names each issue whose title you forced past. ` + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
13944
13945
|
arguments: (z) => ({
|
|
13945
13946
|
project: z.string().describe("Project key for the new issue."),
|
|
13946
13947
|
title: z.string({ max: ISSUE_TITLE_MAX }).describe("Concise issue title, at most 1,000 characters."),
|
|
@@ -14013,7 +14014,7 @@ var dispatchToolSpecs = [
|
|
|
14013
14014
|
}
|
|
14014
14015
|
]
|
|
14015
14016
|
},
|
|
14016
|
-
description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch doc-edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch request-approval. Do not use this tool for a status " + "update or discussion; use dispatch message instead. Never create an issue to park a " + "question: the question is an ask on the issue it concerns. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. Measure what you can first, and put the measurement and the size of the " + "population it covers in the question; drop an option that cannot fix most of that " + "population, never offer it cut down, and keep an option not to act (Hold). Set urgency high " + "when your work halts on the answer, and keep working on everything else. " + "Anchor a to-do about a document passage, 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, ` + `the lines images appends included, and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
14017
|
+
description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch doc-edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch request-approval. Do not use this tool for a status " + "update or discussion; use dispatch message instead. Never create an issue to park a " + "question: the question is an ask on the issue it concerns. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. Measure what you can first, and put the measurement and the size of the " + "population it covers in the question; drop an option that cannot fix most of that " + "population, never offer it cut down, and keep an option not to act (Hold). Set urgency high " + "when your work halts on the answer, and keep working on everything else. " + "Anchor a to-do about a document passage, 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, ` + `the lines images appends included, and has at most 8 options. ${RELATED_CHECK_RESULT} ` + `An ask among the related items says whether it is answered, by whom and with which options. ${OWNER_REFERENCE}`,
|
|
14017
14018
|
arguments: (z) => ({
|
|
14018
14019
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
14019
14020
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -14163,7 +14164,7 @@ var dispatchToolSpecs = [
|
|
|
14163
14164
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
14164
14165
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
14165
14166
|
},
|
|
14166
|
-
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. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. 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 - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "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. In a table cell, with is the cell's text: write a | in it bare, in code too, since code whose text has an odd run of backslashes right before a | cannot be stored in a cell and is INVALID_OP, naming the cell. " + "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. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. 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>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --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. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14167
|
+
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. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. 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 - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "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. In a table cell, with is the cell's text: write a | in it bare, in code too, since code whose text has an odd run of backslashes right before a | cannot be stored in a cell and is INVALID_OP, naming the cell. " + "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. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. 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>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --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. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "Each decision block the edit adds gets the related-item check dispatch ask gets, waiting at most five seconds for all of them together: the result prints, after its first line, `Decision block <id>:` and what that check found, that it found nothing, or that it was not made and why. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14167
14168
|
arguments: (z) => ({
|
|
14168
14169
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
14169
14170
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -14277,6 +14278,25 @@ var dispatchToolSpecs = [
|
|
|
14277
14278
|
message: `project is not a project key: ${PROJECT_KEY_RULE}`
|
|
14278
14279
|
}
|
|
14279
14280
|
},
|
|
14281
|
+
{
|
|
14282
|
+
name: "dispatch_related",
|
|
14283
|
+
example: { project: "CORE", title: "Retry failed webhook deliveries" },
|
|
14284
|
+
description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issue leaves that issue's own items out, " + "as a check on that issue's own write does; keywords_only skips meaning search.",
|
|
14285
|
+
arguments: (z) => ({
|
|
14286
|
+
project: z.string().describe("Project key to search within, such as CORE."),
|
|
14287
|
+
title: z.string({ max: ISSUE_TITLE_MAX }).describe("Optional title to check, as an issue title; at most 1,000 characters.").optional(),
|
|
14288
|
+
text: z.string().describe("Optional text to check, such as a draft spec or a question.").optional(),
|
|
14289
|
+
exclude_issue: z.string().describe("Optional issue key in the project whose own items are left out.").optional(),
|
|
14290
|
+
keywords_only: z.boolean().describe("Search by keywords only, skipping meaning search.").optional()
|
|
14291
|
+
}),
|
|
14292
|
+
validation: {
|
|
14293
|
+
check: (value) => {
|
|
14294
|
+
const { project } = value;
|
|
14295
|
+
return typeof project !== "string" || PROJECT_KEY_PATTERN.test(project);
|
|
14296
|
+
},
|
|
14297
|
+
message: `project is not a project key: ${PROJECT_KEY_RULE}`
|
|
14298
|
+
}
|
|
14299
|
+
},
|
|
14280
14300
|
{
|
|
14281
14301
|
name: "dispatch_issues",
|
|
14282
14302
|
example: { project: "PROJ", limit: 250, offset: 250 },
|
|
@@ -15278,7 +15298,7 @@ var envoyToolSpecs = [
|
|
|
15278
15298
|
},
|
|
15279
15299
|
{
|
|
15280
15300
|
name: "envoy_publish",
|
|
15281
|
-
description: `Publish an Envoy message to a notifications. topic. A role or built-in topic is delivered as its guide says;
|
|
15301
|
+
description: `Publish an Envoy message to a notifications. topic. A role or built-in topic is delivered as its guide says; a role message errs when its holder did not confirm it within the listener's window. Any other notifications. topic you name (a team topic such as notifications.team.deploy-squad.all) reaches every session subscribed to it when you publish, and is not kept for a session that subscribes later. The listener confirms it with each other session that holds its own NATS connection and registered the topic, and errs naming each session that did not confirm; a session that stopped without unregistering stays registered for up to 5 minutes and is named in that time, and a named session is not a failed delivery to the other sessions. A session that holds its own NATS connection receives it directly, yours included; a session the listener delivers to over a serve port, such as an OpenCode agent, is not confirmed and receives it through its machine's listener when that listener subscribes to every topic (ENVOY_SUBSCRIBE_ALL_TOPICS=1; the shared hub's listener does not). ${DELIVERY_CONTRACT}`,
|
|
15282
15302
|
arguments: (schema) => ({ topic: schema.string(), ...messageArguments(schema) }),
|
|
15283
15303
|
operation: EnvoyToolOperation.publish
|
|
15284
15304
|
},
|
|
@@ -15590,7 +15610,7 @@ function sourceSession(sessionID) {
|
|
|
15590
15610
|
// src/server.ts
|
|
15591
15611
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
15592
15612
|
// package.json
|
|
15593
|
-
var version2 = "11.
|
|
15613
|
+
var version2 = "11.2.0";
|
|
15594
15614
|
|
|
15595
15615
|
// src/log.ts
|
|
15596
15616
|
import { appendFile, mkdir } from "fs/promises";
|
package/package.json
CHANGED
package/skills/envoy/SKILL.md
CHANGED
|
@@ -65,7 +65,7 @@ Every topic starts with `notifications.`.
|
|
|
65
65
|
## Roles and team topics
|
|
66
66
|
|
|
67
67
|
- Send to one session with `envoy_send`, to the one session that holds a named duty with a role, and to a group with a team topic.
|
|
68
|
-
- Publish to a role with `envoy_publish(topic="notifications.role.<role>", ...)`; never subscribe to a role topic, because the listener forwards a role message to its holder only. A publish returns the live `holder
|
|
69
|
-
- For a team, the group subscribes to `notifications.team.<name>.>`, and any member publishes to `notifications.team.<name>.all`. Envoy treats any other `notifications.` topic that no source in the table uses the same way. It reaches
|
|
68
|
+
- Publish to a role with `envoy_publish(topic="notifications.role.<role>", ...)`; never subscribe to a role topic, because the listener forwards a role message to its holder only. A publish returns once the holder's session confirmed it, with the live `holder`. An unheld role returns an error, and so does a holder that did not confirm the message within about 4 seconds; that error names the holder, and you see it after about 8 seconds, because the client tries once more. `envoy_role_get(role=...)` finds the holder first.
|
|
69
|
+
- For a team, the group subscribes to `notifications.team.<name>.>`, and any member publishes to `notifications.team.<name>.all`. Envoy treats any other `notifications.` topic that no source in the table uses the same way. It reaches every session subscribed when it is published (yours too, when you subscribe, unless you are a serve-port session, which receives it only through a listener run with `ENVOY_SUBSCRIBE_ALL_TOPICS=1` and never its own publish). The listener also asks each other session that holds its own NATS connection and registered the topic (every session that follows it with `envoy_subscribe`) to confirm the message; your own session is never asked. A publish returns an error naming each such session that did not confirm within about 4 seconds, and you see it after about 8 seconds, because the client tries once more. A session named with `receipt_timeout` still gets the message: the listener kept a copy in that session's inbox, which the session reads back once its connection returns. A session that stopped without unregistering stays registered for up to 5 minutes and is named in that time; a named session is not a failed delivery to the others. A team topic is not kept for a session that subscribes later. A topic outside `notifications.` is refused.
|
|
70
70
|
- How a role claim survives a resume, a fork or a dead process: `skill://envoy/references/roles.md`.
|
|
71
71
|
- In a Legion pane, the plugin claims your role at session start; read `skill://envoy/references/legion.md` before you claim, release or publish to a role, or answer a role delivery failure there.
|
|
@@ -53,7 +53,7 @@ envoy:
|
|
|
53
53
|
|
|
54
54
|
It depends on where a session takes its deliveries, not on whether the topic is built in or your own.
|
|
55
55
|
|
|
56
|
-
- A session that holds its own NATS connection (every Oh My Pi session, and a Claude Code one) receives every message published on a subject it subscribes to, whoever sent it: its own publish included, and a subagent's publish to a topic the parent subscribes to. This holds for a topic the stream keeps and for a live team topic alike while it is connected. After an outage
|
|
56
|
+
- A session that holds its own NATS connection (every Oh My Pi session, and a Claude Code one) receives every message published on a subject it subscribes to, whoever sent it: its own publish included, and a subagent's publish to a topic the parent subscribes to. This holds for a topic the stream keeps and for a live team topic alike while it is connected. After an outage, once its connection or subscription is back, the session recovers from the stream what was published meanwhile on its own inbox and on each kept topic it follows, up to 72 hours back, and is shown each message once, marked as published while its connection was down. That includes a team topic it registered with the listener (every topic it follows with `envoy_subscribe`): the listener forwards each team topic message to the inbox of every session registered on it but the sender, and the stream keeps that copy. A team topic it subscribes to without registering it is gone for the outage.
|
|
57
57
|
- A session the listener delivers to by push, over a serve port (such as the OpenCode plugin), does not receive its own publish, and receives a team topic only through the listener that owns it when that listener runs with `ENVOY_SUBSCRIBE_ALL_TOPICS=1` (the shared hub's listener does not).
|
|
58
58
|
- A role is not a subject its holder subscribes to: the listener finds the live holder and forwards the message to it, and drops the message when that holder is the message's source session.
|
|
59
59
|
|
|
@@ -4,7 +4,7 @@ Read this when a role you held is gone after a resume, fork or restart, when `en
|
|
|
4
4
|
|
|
5
5
|
- A claim belongs to the session, not the process. `omp --resume` re-claims the role recorded in the session transcript, also after the listener removed the dead session's old row (a session's registration lapses 5 minutes after its last heartbeat).
|
|
6
6
|
- `/fork`, `/branch`, `/handoff` and any other switch that carries the transcript read the role the previous session id held and move it to the new id while the new session starts. `/new` and `/resume` install an unrelated transcript and carry nothing.
|
|
7
|
-
- A process that dies and is never resumed stops being a live holder.
|
|
7
|
+
- A process that dies and is never resumed stops being a live holder. Until its registration lapses, role publishes return an error that names the holder and says it did not confirm the message (`receipt_timeout`); once it lapses, they return `no holder` until that transcript resumes or another session claims the role.
|
|
8
8
|
- An automatic reclaim never steals. It is a soft claim, which the listener grants only when the role is unheld, held by a session that is no longer live, or held by the id this session continues (a fork's parent). When a different live session holds it (the parent of a `/fork` whose role moved to the child, or a second process on the same transcript), the listener refuses atomically, and the resumed session logs a warning and holds nothing.
|
|
9
9
|
- Only an explicit `envoy_role_set` is last-claim-wins. Running it again for a role this session already holds is harmless.
|
|
10
10
|
- If another plugin made subscriptions that depend on your current role, `envoy_role_set` to a different role is refused, and the refusal names those subscriptions. `envoy_unsubscribe` keeps those subscriptions and lists them as kept.
|