@sjawhar/opencode-legion-envoy 3.16.4 → 3.17.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 +92 -18
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +84 -106
- package/skills/dispatch/references/asks.md +2 -2
- package/skills/dispatch/references/document-edits.md +6 -2
- package/skills/dispatch/references/documents.md +27 -4
- package/skills/dispatch/references/issues.md +20 -33
- package/skills/legion-architect/SKILL.md +38 -26
- package/skills/legion-worker/SKILL.md +16 -8
- package/skills/legion-worker/references/conflicts-and-rewrites.md +5 -8
- package/skills/legion-worker/references/merge-gate.md +3 -5
- package/skills/legion-worker/references/pr-body.md +8 -17
package/dist/src/server.js
CHANGED
|
@@ -13947,17 +13947,7 @@ function componentsArgument(z2) {
|
|
|
13947
13947
|
reason: z2.string({ min: 1 }).describe("For mode none: why this issue is not architectural (process, hiring, ops).").optional()
|
|
13948
13948
|
}).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
|
|
13949
13949
|
}
|
|
13950
|
-
var
|
|
13951
|
-
"Summary",
|
|
13952
|
-
"New since we talked",
|
|
13953
|
-
"Acceptance",
|
|
13954
|
-
"Requirements",
|
|
13955
|
-
"Design",
|
|
13956
|
-
"Errors",
|
|
13957
|
-
"Testing",
|
|
13958
|
-
"Rejected"
|
|
13959
|
-
];
|
|
13960
|
-
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.";
|
|
13950
|
+
var SPEC_WRITING_POINTER = 'Write a spec as the "Writing a spec" section of skill://dispatch says.';
|
|
13961
13951
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
13962
13952
|
var ASK_QUESTION_MAX = 800;
|
|
13963
13953
|
var SEARCH_QUERY_MAX = 1000;
|
|
@@ -13995,7 +13985,7 @@ var dispatchToolSpecs = [
|
|
|
13995
13985
|
parent: z2.string().describe("Optional parent issue.").optional(),
|
|
13996
13986
|
external: z2.string().describe("Optional external issue reference.").optional(),
|
|
13997
13987
|
force: z2.boolean().describe("Create even though POSSIBLE_DUPLICATE listed similar issues; pass it only after reading them.").optional(),
|
|
13998
|
-
spec: z2.string().describe(`Optional initial primary-document markdown. ${
|
|
13988
|
+
spec: z2.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_POINTER}`).optional(),
|
|
13999
13989
|
labels: z2.array(z2.string({ min: 1, max: 40 }), { max: 20 }).describe("Optional initial labels, at most 20 labels of up to 40 characters.").optional(),
|
|
14000
13990
|
priority: z2.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
|
|
14001
13991
|
assignee: z2.string().describe("GitHub login of the human who answers this issue's asks; defaults to your owner when you act for a person, else the parent's assignee, else unassigned.").optional(),
|
|
@@ -14182,7 +14172,7 @@ var dispatchToolSpecs = [
|
|
|
14182
14172
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
14183
14173
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
14184
14174
|
},
|
|
14185
|
-
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - 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. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch_doc_read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "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. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${
|
|
14175
|
+
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - 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. " + "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>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch_doc_read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "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. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14186
14176
|
arguments: (z2) => ({
|
|
14187
14177
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14188
14178
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14227,12 +14217,13 @@ var dispatchToolSpecs = [
|
|
|
14227
14217
|
},
|
|
14228
14218
|
{
|
|
14229
14219
|
name: "dispatch_request_approval",
|
|
14230
|
-
example: { issue: "DSP-1" },
|
|
14231
|
-
description: "Ask a human to approve a document at its current version
|
|
14220
|
+
example: { issue: "DSP-1", summary: "Proposes a live sync in place of the nightly export." },
|
|
14221
|
+
description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. A later version makes an approval stale, " + "and writing it retracts an open request for an older version; request again for the new " + "one. A repeat at the version an open request names returns that request unchanged. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
|
|
14232
14222
|
arguments: (z2) => ({
|
|
14233
14223
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14234
14224
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
14235
|
-
artifact: z2.string().describe("Project document artifact id, slug, or filename; primary document by default for an issue.").optional()
|
|
14225
|
+
artifact: z2.string().describe("Project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
|
|
14226
|
+
summary: z2.string({ min: 1 }).describe("The proposals in this version the human hasn't already agreed to, in one to three sentences.")
|
|
14236
14227
|
}),
|
|
14237
14228
|
validation: documentOwnerValidation(true)
|
|
14238
14229
|
},
|
|
@@ -16534,6 +16525,84 @@ async function openArtifactMarks(client, resolved) {
|
|
|
16534
16525
|
...commentsResult.value.filter((comment) => !comment.resolved && comment.anchor?.artifact_id === resolved.artifact.id).map((comment) => `comment ${comment.id}`)
|
|
16535
16526
|
];
|
|
16536
16527
|
}
|
|
16528
|
+
async function blockAsks(client, resolved, state) {
|
|
16529
|
+
const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
|
|
16530
|
+
return asks.filter((ask) => typeof ask.block_id === "string" && ask.block_artifact?.id === resolved.artifact.id);
|
|
16531
|
+
}
|
|
16532
|
+
async function refuseOpenDecisionBlocks(client, tool, resolved) {
|
|
16533
|
+
const artifact = resolved.artifact;
|
|
16534
|
+
const latest = artifact.approval?.latest_version;
|
|
16535
|
+
if (latest === undefined || latest < 1 || artifact.approval?.state === "approved")
|
|
16536
|
+
return;
|
|
16537
|
+
const blocks = (await client.artifactBlocks(artifact.id)).filter((block) => block.type === "ask");
|
|
16538
|
+
if (blocks.length === 0)
|
|
16539
|
+
return;
|
|
16540
|
+
const [documentAsks, version2] = await Promise.all([
|
|
16541
|
+
blockAsks(client, resolved),
|
|
16542
|
+
client.docRead(artifact.id, latest)
|
|
16543
|
+
]);
|
|
16544
|
+
const asks = new Map(documentAsks.map((ask) => [ask.block_id, ask]));
|
|
16545
|
+
const lines = version2.markdown.split(`
|
|
16546
|
+
`);
|
|
16547
|
+
const open = blocks.flatMap((block) => {
|
|
16548
|
+
const ask = asks.get(block.id);
|
|
16549
|
+
const named = ask === undefined ? `block ${block.id}` : `${JSON.stringify(ask.question)} (block ${block.id}, ask ${ask.id})`;
|
|
16550
|
+
const states = lines.filter((line) => line.includes(`ask{#${block.id} `) || line.includes(`ask{#${block.id}}`)).map((line) => /\bstate="(\w+)"/.exec(line)?.[1]);
|
|
16551
|
+
if (states.length === 0)
|
|
16552
|
+
return [`${named}, which version ${latest} does not hold yet`];
|
|
16553
|
+
if (!states.includes("open") && states.some((state) => state !== undefined))
|
|
16554
|
+
return [];
|
|
16555
|
+
if (ask === undefined)
|
|
16556
|
+
return [`${named}, whose ask Dispatch has not opened yet`];
|
|
16557
|
+
if (ask.state === "open")
|
|
16558
|
+
return [named];
|
|
16559
|
+
const next = ask.state === "answered" ? "fold the answer into the text" : "write the decision into the text";
|
|
16560
|
+
return [
|
|
16561
|
+
`${named}, ${ask.state} but still open in version ${latest}: ${next} with dispatch_doc_edit, which writes a version that carries it`
|
|
16562
|
+
];
|
|
16563
|
+
});
|
|
16564
|
+
if (open.length === 0)
|
|
16565
|
+
return;
|
|
16566
|
+
const count = open.length === 1 ? "1 open decision block" : `${open.length} open decision blocks`;
|
|
16567
|
+
throw new Error([
|
|
16568
|
+
`${tool} was not called: ${artifact.name} (version ${latest}) has ${count}. Answering one writes a new version, which would retract this request.`,
|
|
16569
|
+
...open.map((line) => `- ${line}`),
|
|
16570
|
+
"Do not request approval over an open block, even when a human asked for it. Tell the human which block is open and ask them to answer it or to waive it. Once it is answered, fold the answer into the text with dispatch_doc_edit and request approval again. If they waive it, close the block with dispatch_resolve_ask (kind resolved, their words as the reason), write their decision into the text with dispatch_doc_edit, and request approval again."
|
|
16571
|
+
].join(`
|
|
16572
|
+
`));
|
|
16573
|
+
}
|
|
16574
|
+
async function refuseRemovingOpenDecisionBlocks(client, tool, resolved, ops) {
|
|
16575
|
+
const removing = ops.filter((operation) => operation.block !== undefined && (operation.op === "delete" || operation.op === "retype" && operation.type !== "ask"));
|
|
16576
|
+
if (removing.length === 0)
|
|
16577
|
+
return;
|
|
16578
|
+
const artifact = resolved.artifact;
|
|
16579
|
+
const blocks = await client.artifactBlocks(artifact.id);
|
|
16580
|
+
const askBlocks = blocks.filter((block) => block.type === "ask");
|
|
16581
|
+
const removed = new Set;
|
|
16582
|
+
for (const operation of removing) {
|
|
16583
|
+
const target = blocks.find((block) => block.id === operation.block);
|
|
16584
|
+
if (target === undefined)
|
|
16585
|
+
continue;
|
|
16586
|
+
for (const block of askBlocks) {
|
|
16587
|
+
if (block.id === target.id || operation.op === "delete" && block.from >= target.from && block.to <= target.to) {
|
|
16588
|
+
removed.add(block.id);
|
|
16589
|
+
}
|
|
16590
|
+
}
|
|
16591
|
+
}
|
|
16592
|
+
if (removed.size === 0)
|
|
16593
|
+
return;
|
|
16594
|
+
const asks = await blockAsks(client, resolved, "open");
|
|
16595
|
+
const open = asks.filter((ask) => removed.has(ask.block_id));
|
|
16596
|
+
if (open.length === 0)
|
|
16597
|
+
return;
|
|
16598
|
+
const [what, question] = open.length === 1 ? ["a decision block whose ask is", "question"] : [`${open.length} decision blocks whose asks are`, "questions"];
|
|
16599
|
+
throw new Error([
|
|
16600
|
+
`${tool} was not called: it would remove ${what} still open, and the human's ${question} would leave their Inbox unanswered.`,
|
|
16601
|
+
...open.map((ask) => `- ${JSON.stringify(ask.question)} (block ${ask.block_id}, ask ${ask.id})`),
|
|
16602
|
+
"A decision block leaves the document once its ask is answered or resolved. Until then, reword it with replace, relocate it with move, or change its question, options, urgency or multiple with dispatch_edit_ask if you asked it; each keeps it."
|
|
16603
|
+
].join(`
|
|
16604
|
+
`));
|
|
16605
|
+
}
|
|
16537
16606
|
function refusalWithCode(error48, suffix = "") {
|
|
16538
16607
|
if (!(error48 instanceof DispatchServiceError))
|
|
16539
16608
|
return error48;
|
|
@@ -17094,6 +17163,7 @@ ${followsAsk(askOwner)}`,
|
|
|
17094
17163
|
const summary = optionalString(args, "summary");
|
|
17095
17164
|
const { precondition: rawPrecondition } = args;
|
|
17096
17165
|
const precondition = rawPrecondition;
|
|
17166
|
+
await refuseRemovingOpenDecisionBlocks(client, input.tool, resolved, ops);
|
|
17097
17167
|
const edited = await client.docEdit(resolved.artifact.id, {
|
|
17098
17168
|
ops,
|
|
17099
17169
|
...summary === undefined ? {} : { summary },
|
|
@@ -17161,7 +17231,11 @@ ${trailer.join(`
|
|
|
17161
17231
|
case "dispatch_request_approval": {
|
|
17162
17232
|
const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
|
|
17163
17233
|
const resolved = await resolveDocument(documentOwner(), artifactReference);
|
|
17164
|
-
|
|
17234
|
+
await refuseOpenDecisionBlocks(client, input.tool, resolved);
|
|
17235
|
+
const result = await client.requestApproval(resolved.artifact.id, {
|
|
17236
|
+
actor,
|
|
17237
|
+
summary: stringArg(args, "summary")
|
|
17238
|
+
});
|
|
17165
17239
|
if (result.ask === null) {
|
|
17166
17240
|
return {
|
|
17167
17241
|
text: `${resolved.artifact.name} (document id ${resolved.artifact.id}) is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. An edit after approval makes it stale, so request again only for a new version.`,
|
|
@@ -17174,7 +17248,7 @@ ${trailer.join(`
|
|
|
17174
17248
|
}
|
|
17175
17249
|
const details = await followedAskDetails(client, result.ask, resolved.artifact);
|
|
17176
17250
|
return {
|
|
17177
|
-
text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
|
|
17251
|
+
text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
|
|
17178
17252
|
details: { ...details, artifact: resolved.artifact.id, version: result.version }
|
|
17179
17253
|
};
|
|
17180
17254
|
}
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -34,34 +34,24 @@ after `skill://dispatch/` is relative to this skill's base directory.
|
|
|
34
34
|
|
|
35
35
|
## Design changes are brainstormed here
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
([Writing a spec](#writing-a-spec), [Typed blocks](#typed-blocks)). He answers in place and the document
|
|
42
|
-
grows into the record. A finished spec dropped after a chat-only design, or a set of one-line issue asks
|
|
43
|
-
pointing at a document, is not brainstorming with him.
|
|
44
|
-
Sami, 2026-09-17, verbatim: "Can you please stop doing this thing where you have these one-off,
|
|
45
|
-
shorthand, compressed decision asks that are completely disconnected from any discussion of the
|
|
46
|
-
design or the trade-offs? This is just very obviously not the most effective way to have a design
|
|
47
|
-
communication." A question lives beside the options and trade-offs it depends on, in the spec or
|
|
48
|
-
discussion it came from — never as a compressed standalone ask.
|
|
37
|
+
For a major design change the design conversation happens in Dispatch, in the spec: write it
|
|
38
|
+
early, while it is still a draft with real alternatives, and let it grow as the human answers
|
|
39
|
+
([Writing a spec](#writing-a-spec)). A finished spec dropped after a chat-only design is not that
|
|
40
|
+
conversation.
|
|
49
41
|
|
|
50
42
|
## Writing for the human
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
message, and PR body is read by a person who has not read the code, does not share this session's
|
|
55
|
-
vocabulary, and is often on a phone. Write for that person.
|
|
44
|
+
Every spec, ask, comment, message, and PR body is read by a person who has not read the code, does
|
|
45
|
+
not share this session's vocabulary, and is often on a phone. Write for that person.
|
|
56
46
|
|
|
57
47
|
- Plain English, full sentences, one idea per sentence. Never repo shorthand or nouns you coined:
|
|
58
48
|
not "fix 8c", "READY-target", "PR B", "spec@v3", "the pair", "the packet" — say what the thing is.
|
|
59
49
|
- Expand every identifier the first time it appears: an issue key gets its title, a PR number its
|
|
60
50
|
title, a file what it is for, a session id who it is. Link a URL rather than pasting a bare id.
|
|
61
|
-
- A question lives
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
- A question lives in the spec or discussion it came from, placed as
|
|
52
|
+
[Decision blocks](#decision-blocks) says, never as a compressed standalone ask. Give the reader
|
|
53
|
+
the options, what each costs, and your recommendation with its reason; do not prescribe yourself
|
|
54
|
+
a form.
|
|
65
55
|
- Describe a change by what its reader stands to lose, not by what the system does. The
|
|
66
56
|
engineering sentence names the change; the reader's sentence names who can do what today, what
|
|
67
57
|
they will not be able to do after it, what still works, and what you cannot tell. It is a
|
|
@@ -73,46 +63,45 @@ vocabulary, and is often on a phone. Write for that person.
|
|
|
73
63
|
shapes the sentence under it.
|
|
74
64
|
- Before posting, test it: could Sami, reading only this text on his phone, know what he is being
|
|
75
65
|
told or asked? If not, rewrite it. Length is not the problem; density is.
|
|
76
|
-
- When an ask or message communicates a judgment, lead with that judgment in one sentence and put the mechanism underneath it. Do not make the reader ask a second time whether the result is a win. This shapes communication only when a judgment exists; it does not pre-decide an open question or remove its genuine options.
|
|
77
|
-
- When a Dispatch message states a root cause, include the reproducing command or test in that same message. Without it, label the diagnosis a hypothesis; a diagnosis still in progress may say so plainly. This boundary applies to causal claims, not to reporting that an investigation has started.
|
|
66
|
+
- When an ask or message communicates a judgment, lead with that judgment in one sentence and put the mechanism underneath it. Do not make the reader ask a second time whether the result is a win. This shapes communication only when a judgment exists; it does not pre-decide an open question or remove its genuine options.
|
|
67
|
+
- When a Dispatch message states a root cause, include the reproducing command or test in that same message. Without it, label the diagnosis a hypothesis; a diagnosis still in progress may say so plainly. This boundary applies to causal claims, not to reporting that an investigation has started.
|
|
78
68
|
|
|
79
69
|
## Writing a spec
|
|
80
70
|
|
|
81
|
-
A spec
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- No hedging ("might", "could consider"). No TBD, TODO, or placeholders: an open item is an ask
|
|
99
|
-
block, a technical decision your lane makes and records as a Requirement, or, for a contract
|
|
100
|
-
between two lanes or a halt condition, a question for the platform PO (see
|
|
71
|
+
A spec is the design conversation with the human, written down. It starts as the problem and the
|
|
72
|
+
evidence for it, in plain words: what goes wrong, for whom, and the counts or cases that show it.
|
|
73
|
+
It grows in place as the conversation goes. It is the issue's one primary document: extend it with
|
|
74
|
+
a new version that keeps the human's own text, never a second "spec" artifact beside it.
|
|
75
|
+
|
|
76
|
+
- **Each open question is a decision block**, placed as [Decision blocks](#decision-blocks) says.
|
|
77
|
+
Because it is an ask, it reaches the human's Inbox, and the answer lands next to its context.
|
|
78
|
+
- **A settled point records the human's own words and the date**, quoted, so no reader mistakes
|
|
79
|
+
it for your inference. A point you inferred says so, with the reasoning.
|
|
80
|
+
- **Sections follow the topic.** No heading is required and none has a fixed place; name each
|
|
81
|
+
section for what it discusses.
|
|
82
|
+
- **A changed point is rewritten, not appended to.** When an answer or a new fact changes the
|
|
83
|
+
design, rewrite the text it changes and fold the answered decision into it; the document's
|
|
84
|
+
versions keep the history.
|
|
85
|
+
- **No placeholders.** No TBD, TODO, or hedging ("might", "could consider"): an open item is a
|
|
86
|
+
decision block, a technical decision your lane makes and records in the text, or, for a
|
|
87
|
+
contract between two lanes or a halt condition, a question for the platform PO (see
|
|
101
88
|
[Before you ask](#before-you-ask) under Asking).
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
89
|
+
- **No progress.** The spec records the design and its decisions, never status, timestamps, an
|
|
90
|
+
"Update HH:MMZ" section, a pull-request list, or handoff notes. Progress is not a Dispatch
|
|
91
|
+
object at all; it lives in your transcript and your pull request (see [Messages](#messages)).
|
|
92
|
+
|
|
93
|
+
Before a new version goes out, read it as the human will: no two passages conflict, each point has
|
|
94
|
+
one reading, and every decision block passes the phone test above. A worked example is
|
|
95
|
+
`dispatch://LEGION-386/artifact/spec@v5`: the problem and its counts, what the human settled in his
|
|
96
|
+
own words, a section for each part of the design, and its one open question as a decision block
|
|
97
|
+
after the section on models.
|
|
109
98
|
|
|
110
99
|
## Decision blocks
|
|
111
100
|
|
|
112
|
-
A decision a human must make is an `:::ask` block
|
|
113
|
-
the
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
A decision a human must make is an `:::ask` block at the end of the section that discusses it,
|
|
102
|
+
carrying the options, what each costs, and your recommendation. Never gather decisions into a
|
|
103
|
+
list, at the top, at the bottom or in an "open questions" section, and never ask one as a
|
|
104
|
+
standalone `dispatch_ask` that points at the spec.
|
|
116
105
|
|
|
117
106
|
The block is what reaches the human's Inbox. A question phrased as prose in the spec reaches
|
|
118
107
|
nobody. A spec with no ask blocks is fine only when the issue genuinely needs no human decision.
|
|
@@ -126,16 +115,14 @@ only as `:::ask{…}` on a line of its own), or any `dispatch_doc_edit`: after a
|
|
|
126
115
|
ask, `dispatch_doc_read` the section and check it renders as `:::ask{#<id> …}` on its own line.
|
|
127
116
|
|
|
128
117
|
**Wrong:** a **Decisions needed** list at the top of the spec with three bullets.
|
|
129
|
-
**Right:**
|
|
130
|
-
2–4 options, a recommendation, and surrounding prose that explains the trade-off.
|
|
118
|
+
**Right:** each decision an `:::ask{#slug}` block at the end of the design section that discusses
|
|
119
|
+
it, with 2–4 options, a recommendation, and surrounding prose that explains the trade-off.
|
|
131
120
|
|
|
132
121
|
A spec that already has the pile is repaired with `move`, not rewritten: `dispatch_doc_edit` with
|
|
133
|
-
`{ op: "move", block: "<block-uuid>", after: "<the sentence that
|
|
122
|
+
`{ op: "move", block: "<block-uuid>", after: "<the last sentence of the section that discusses it>" }` relocates
|
|
134
123
|
the block and keeps its ask, its answer and its followers; the context paragraphs that were lifted
|
|
135
|
-
out of Design move the same way, and the emptied section is deleted
|
|
136
|
-
|
|
137
|
-
discussion in the spec, not just all piled up at the start with no context"). An ask block has two
|
|
138
|
-
ids that differ; [Editing a document](skill://dispatch/references/document-edits.md) says which.
|
|
124
|
+
out of Design move the same way, and the emptied section is deleted. An ask block has two ids that
|
|
125
|
+
differ; [Editing a document](skill://dispatch/references/document-edits.md) says which.
|
|
139
126
|
|
|
140
127
|
See [Typed blocks](#typed-blocks) for the syntax and [Before you ask](#before-you-ask) under
|
|
141
128
|
[Asking](#asking) to decide whether the question is a real decision at all.
|
|
@@ -177,7 +164,7 @@ absolute link. Cite the hit you build on (`dispatch://KEY` or the document refer
|
|
|
177
164
|
Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
|
|
178
165
|
The check compares title words only (shared stemmed terms), never meaning: "four tests that fail a
|
|
179
166
|
merge" pairs with "four CI gates that cannot fail a merge". So when you force past a candidate, give
|
|
180
|
-
the new issue a title that names what differs where you can, and open its spec
|
|
167
|
+
the new issue a title that names what differs where you can, and open its spec with the
|
|
181
168
|
distinction from the named issue, citing it (`dispatch://KEY`), for whoever reads the next pairing.
|
|
182
169
|
|
|
183
170
|
### Symptom versus cause
|
|
@@ -186,8 +173,7 @@ When a symptom and its cause sit on different issues, the work accrues to the ca
|
|
|
186
173
|
the symptom's issue carries a pointer to it. Before posting a measurement or finding, search
|
|
187
174
|
Dispatch for the failing identity's or component's name, and post on the issue whose title names
|
|
188
175
|
the fix, not the one naming the symptom. A symptom issue gathering messages with no human response
|
|
189
|
-
is the tell.
|
|
190
|
-
and answer sat on AGENTC-1010.)
|
|
176
|
+
is the tell.
|
|
191
177
|
|
|
192
178
|
## Claim the issue before you work it
|
|
193
179
|
|
|
@@ -217,11 +203,7 @@ field, and choosing your next issue are in [Working an issue](skill://dispatch/r
|
|
|
217
203
|
|
|
218
204
|
### Before you ask
|
|
219
205
|
|
|
220
|
-
|
|
221
|
-
completely disconnected from any discussion of design or trade-offs. This is not a very useful way
|
|
222
|
-
of having this discussion" (on a report-table shape), and "What's a fenced PutObject or phantom
|
|
223
|
-
eval_id? What's an R4 model header? What exactly is the question or uncertainty here?" (on a
|
|
224
|
-
production import). Every `dispatch_ask` passes four gates first:
|
|
206
|
+
Every `dispatch_ask` passes four gates first:
|
|
225
207
|
|
|
226
208
|
1. **Does it need his authority, taste, or risk appetite?** This is the bar for a decision
|
|
227
209
|
written as an `:::ask` block in context ([Decision blocks](#decision-blocks)). Technical
|
|
@@ -229,19 +211,18 @@ production import). Every `dispatch_ask` passes four gates first:
|
|
|
229
211
|
internals are your lane's to decide and record in the spec. Two things still go to the
|
|
230
212
|
platform PO over Envoy: a contract between two lanes, and a halt condition (a change to IAM,
|
|
231
213
|
deletion or exposure of production data, anything that reaches a customer). The PO takes those
|
|
232
|
-
to Sami as a Dispatch ask; you do not open one yourself, even as a permission ask under gate 2
|
|
233
|
-
(Sami, 2026-09-25, AGENTC-34 §12).
|
|
214
|
+
to Sami as a Dispatch ask; you do not open one yourself, even as a permission ask under gate 2.
|
|
234
215
|
2. **Is there genuine uncertainty, and have you measured what you can?** If there is none, it is
|
|
235
216
|
a plan you execute. The one legitimate ask without uncertainty is permission for an action
|
|
236
217
|
only a human can authorise — a production write, an external send, a console action — and then
|
|
237
218
|
the question is that action in one sentence, with options that name its outcomes (below).
|
|
238
219
|
Measure before you write: how many are affected, whether anything reaches the path, what the
|
|
239
220
|
current state already is. The measurement decides whether a human is needed at all, and when
|
|
240
|
-
one is, it turns a research request he cannot answer into a decision he can
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
221
|
+
one is, it turns a research request he cannot answer into a decision he can. Put the
|
|
222
|
+
measurement and the size of the affected population in the ask. When the measurement shows one
|
|
223
|
+
fix cannot repair most of that population and another fix can, drop the first, rather than
|
|
224
|
+
offering it cut down to the part it reaches. An option not to act, such as a permission ask's
|
|
225
|
+
Hold, is not a fix, and it stays.
|
|
245
226
|
Report what the measurement could **not** establish, with its own control: "I found no
|
|
246
227
|
evidence" and "there is no evidence to find" read alike and mean opposite things, and a
|
|
247
228
|
control that shares the query's blind spot proves neither. Before you say you are waiting on
|
|
@@ -303,13 +284,8 @@ passage with `anchor`. Follow up on an ask or comment with `dispatch_comment`; c
|
|
|
303
284
|
with a `dispatch://` reference (see [References](#references)). Never write "see above", "the
|
|
304
285
|
message above", or "as attached".
|
|
305
286
|
|
|
306
|
-
**Pointing at another message is a defect, not a shortcut
|
|
307
|
-
|
|
308
|
-
as a comment: "you just dump information into messages and then add a new ask that references a
|
|
309
|
-
previous message in prose with no link or no context whatsoever and uses compressed shorthand
|
|
310
|
-
jargon." The ask view does not show the issue's comments, so that ask was unanswerable; "Cloud
|
|
311
|
-
Identity licence check / 2SV override / 1-day grace" was shorthand he had never used. The rules
|
|
312
|
-
that follow from it:
|
|
287
|
+
**Pointing at another message is a defect, not a shortcut:** the ask view does not show the
|
|
288
|
+
issue's comments. The rules:
|
|
313
289
|
|
|
314
290
|
- An ask that names another message in prose — "my comment above", "the procedure I posted",
|
|
315
291
|
"see the earlier message" — is retracted by the PO as failing the gates. Put the content IN the
|
|
@@ -333,7 +309,7 @@ they must read to decide belongs in the spec in the first place — see [Artifac
|
|
|
333
309
|
|
|
334
310
|
Before saying you are waiting for human input, call `dispatch_open_asks`. With no arguments it lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply. With `dispatch_open_asks({ project })` it lists every open ask in that project — on its issues and on its documents, whoever authored them — which is how you audit what a whole project is waiting on rather than just your own asks.
|
|
335
311
|
|
|
336
|
-
**Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, send a one-line ask before the first implementation commit. A lane's schema decision or a platform-PO contract ruling does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request
|
|
312
|
+
**Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, send a one-line ask before the first implementation commit. A lane's schema decision or a platform-PO contract ruling does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request. A control or behaviour the human asked for in words is settled by those words, together with every choice inside it that his words do not make (where it sits, its defaults, its options): build it without an ask, as gate 4 of [Before you ask](#before-you-ask) says. This rule covers only product shape outside what he asked for, and its ask comes before the commit that sets that shape.
|
|
337
313
|
|
|
338
314
|
**Anything you are blocked on a human for is an open ask.** An agent waits on a human only through
|
|
339
315
|
an open ask. An approval, a credential or grant to renew, a setting only they can change, a review
|
|
@@ -372,10 +348,6 @@ other way — Sami said it live, a later comment settled it, or the question bec
|
|
|
372
348
|
design moved — resolve it yourself with `dispatch_resolve_ask` in the same turn you learn that.
|
|
373
349
|
Never leave it for the human to clear.
|
|
374
350
|
|
|
375
|
-
Sami, AGENTC-27 `rules-derivation-2026-09-17.md` row R04, verbatim: "I think this was answered
|
|
376
|
-
live. If not, please reask." The same pattern left him closing asks as `Dismissed`, `Settled`, and
|
|
377
|
-
`Resolved I think`: noise the human had to clear.
|
|
378
|
-
|
|
379
351
|
Every later write on the issue answers `You still have an open ask on …` and names it. Treat that
|
|
380
352
|
as the checklist: if it is still needed, leave it; if it was answered elsewhere, resolve it with
|
|
381
353
|
the resolving fact as the reason. Before posting a new ask, inspect your open ones. If the new ask
|
|
@@ -387,25 +359,33 @@ See [Following](#following) for why you receive what happens to asks you open an
|
|
|
387
359
|
|
|
388
360
|
## Approval of a spec
|
|
389
361
|
|
|
390
|
-
Approval is a property of a document, not a question you phrase: a human approves a specific
|
|
391
|
-
review approves a commit, and any later
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
the human's Inbox
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
362
|
+
Approval is a property of a document, not a question you phrase: a human approves a specific
|
|
363
|
+
version, the way a pull-request review approves a commit, and any later version makes that
|
|
364
|
+
approval stale. Answering a decision block writes a new version, so request approval only when
|
|
365
|
+
both hold: every decision block is settled, which means answered and folded into the text, or
|
|
366
|
+
waived as the next paragraph says; and it proposes something the human has not already settled.
|
|
367
|
+
A Legion root spec under an armed design gate always goes to approval once its blocks are settled
|
|
368
|
+
(`skill://legion-architect`).
|
|
369
|
+
|
|
370
|
+
When both hold, request it in the pass that finishes the spec: a design waiting with nothing in
|
|
371
|
+
the human's Inbox waits on nobody. A choice you can make yourself is not a decision block
|
|
372
|
+
([Before you ask](#before-you-ask), gate 1): write your call and its reason into the design and
|
|
373
|
+
name it in `summary`; a human who disagrees answers `Request changes`. When a human asks for
|
|
374
|
+
approval while a block is open, do not request it and do not hold it silently: name each open
|
|
375
|
+
block and ask them to answer it or waive it. For a waiver, close the block with `dispatch_resolve_ask`
|
|
376
|
+
(`kind: "resolved"`, their words as `reason`), then write their decision into the text in their
|
|
377
|
+
words and request. `dispatch_request_approval` refuses while any block is open.
|
|
378
|
+
|
|
379
|
+
`summary` names the proposals in this version the human has not agreed to, in one to three
|
|
380
|
+
sentences, and never an open question: the Inbox shows it after "Approve spec.md (version N)?".
|
|
381
|
+
Never write "Approve" options into an ordinary `dispatch_ask`; only humans approve. The call, its
|
|
382
|
+
result and its answer: [Approval requests](skill://dispatch/references/documents.md#approval-requests).
|
|
403
383
|
|
|
404
384
|
## The Spec
|
|
405
385
|
|
|
406
|
-
The spec holds
|
|
407
|
-
|
|
408
|
-
|
|
386
|
+
The spec holds the design and the decisions that shaped it, written as [Writing a spec](#writing-a-spec)
|
|
387
|
+
says, which also lists what it never carries. It changes when the conversation changes it, and
|
|
388
|
+
every version that records a decision is named with `dispatch_doc_edit`'s `summary`.
|
|
409
389
|
|
|
410
390
|
Read the current document before changing it:
|
|
411
391
|
|
|
@@ -442,9 +422,7 @@ Uploading one (`dispatch_artifact`, its slugs, versions and Markdown rules) is i
|
|
|
442
422
|
|
|
443
423
|
## Structure over stream
|
|
444
424
|
|
|
445
|
-
Dispatch is a structured workspace, never a message stream
|
|
446
|
-
to me that agents keep trying to use dispatch as a giant stream of messages instead of
|
|
447
|
-
high-signal, structured conversation"). The structure IS the product:
|
|
425
|
+
Dispatch is a structured workspace, never a message stream. The structure IS the product:
|
|
448
426
|
|
|
449
427
|
- **One issue per piece of work.** A new deliverable — an email to send, a document to review, a
|
|
450
428
|
decision with its own lifecycle — gets its own issue with the content as the issue's document
|
|
@@ -62,8 +62,8 @@ dispatch_resolve_ask({
|
|
|
62
62
|
Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the reason because the question remains
|
|
63
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
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
|
|
66
|
-
back reopens it. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
|
|
65
|
+
however the document moves afterwards. `dispatch_doc_edit` refuses deleting the block of an open ask; a block removed another way, by a
|
|
66
|
+
whole-document `dispatch_artifact` replace or by a person in the browser, closes its ask, and putting the block back reopens it. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
|
|
67
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
68
|
`dispatch_comment` (mutually exclusive with `reply_to`).
|
|
69
69
|
A review comment you opened has its own closer, `dispatch_resolve_comment` — see
|
|
@@ -123,6 +123,10 @@ paragraph you replaced, which keeps that paragraph's id; only when no paragraph
|
|
|
123
123
|
the old block's place is it a `delete` of the old block and then an `insert` of the new one, anchored on the
|
|
124
124
|
block before or after it. The delete is what costs the id (below); a typed block keeps its id when the insert
|
|
125
125
|
carries it, which works only in that order, because an insert carrying an id the document still holds is refused.
|
|
126
|
+
The tools refuse a `delete` or `retype` that would take out an `ask` block whose ask is still open, delete-then-insert included:
|
|
127
|
+
reword it with `replace`, relocate it with `move`, or change its question, options, urgency or `multiple` with `dispatch_edit_ask` if you asked it. A single
|
|
128
|
+
batch that moves an open block out of a container and then deletes the container is refused; make them two calls (the check reads
|
|
129
|
+
the document as it stood before the batch).
|
|
126
130
|
Then read the document back with
|
|
127
131
|
`dispatch_doc_read` and read the passage and its neighbours, not a grep for the words you added: an empty
|
|
128
132
|
`with` deletes the matched text on purpose, so a `replace` whose `with` you meant to fill
|
|
@@ -137,8 +141,8 @@ A `delete` whose `find` is a block's entire text removes the block itself — th
|
|
|
137
141
|
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
|
|
138
142
|
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
|
|
139
143
|
(paragraphs, code, tables) is refused with `INVALID_OP` naming `delete {block:"<item id>"}`, which removes the item with its content.
|
|
140
|
-
`delete` with `block` removes any block by id (paragraph, heading, list, list item, table, or typed block;
|
|
141
|
-
|
|
144
|
+
`delete` with `block` removes any block by id (paragraph, heading, list, list item, table, or typed block; an answered `ask` block keeps
|
|
145
|
+
its answer as the record, and a resolved one, which carries no answer, its resolution), and `move` with `block` relocates one, keeping its id and
|
|
142
146
|
attributes — a moved `ask` keeps its ask and answer. **A block loses its id only when it is removed**, and its anchors go with it:
|
|
143
147
|
`delete` by text or by id removes the block and any container it empties; `delete_row` / `delete_column` remove their cells' ids,
|
|
144
148
|
which is why they are refused while an open ask or unresolved comment sits on them; and a `move` that takes the last block out of a
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Documents: typed blocks, comments, suggestions, and artifacts
|
|
2
2
|
|
|
3
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,
|
|
5
|
-
`DOC_SERVICE_UNAVAILABLE` back. Changing a document's text,
|
|
6
|
-
[Editing a document](skill://dispatch/references/document-edits.md).
|
|
4
|
+
or suggest a change to a document, upload an artifact, request a document's approval, or get
|
|
5
|
+
`DOC_SCHEMA`, `INVALID_ASK_BLOCK` or `DOC_SERVICE_UNAVAILABLE` back. Changing a document's text,
|
|
6
|
+
tables included, is [Editing a document](skill://dispatch/references/document-edits.md).
|
|
7
7
|
|
|
8
8
|
## Typed blocks
|
|
9
9
|
|
|
@@ -23,7 +23,10 @@ names one block, so an insert, upload or suggestion whose markdown names an id t
|
|
|
23
23
|
outside the text it replaces is refused naming the id: `INVALID_OP` for an insert,
|
|
24
24
|
`INVALID_MARKDOWN` for any other write. To rewrite such a block whole, `delete` it and then
|
|
25
25
|
`insert` the new one carrying its id, anchored on the block before or after it, in that order and
|
|
26
|
-
in one batch: an insert carrying an id the document still holds is refused.
|
|
26
|
+
in one batch: an insert carrying an id the document still holds is refused. An `ask` block whose ask
|
|
27
|
+
is still open cannot be rewritten that way: the tools refuse the `delete`, so reword it with
|
|
28
|
+
`replace`, relocate it with `move`, or change its question, options, urgency or `multiple` with
|
|
29
|
+
`dispatch_edit_ask` if you asked it ([Editing a document](skill://dispatch/references/document-edits.md)).
|
|
27
30
|
|
|
28
31
|
Use only the type names, content rule, attributes, and enum values returned by the schema. Values are
|
|
29
32
|
quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::: {.callout}`, leaf
|
|
@@ -141,6 +144,26 @@ delimiter rows as many. Blank cells past the width are dropped, on every path. A
|
|
|
141
144
|
a version answers `INVALID_MARKDOWN`, an insert of blocks `INVALID_OP` on `markdown`, and an insert
|
|
142
145
|
of bare table rows `TABLE_WIDTH`, which names the cell counts only.
|
|
143
146
|
|
|
147
|
+
## Approval requests
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
dispatch_request_approval({ issue?, project?, artifact?, summary })
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The call opens an approval ask with the options `Approve` and `Request changes`, its question
|
|
154
|
+
"Approve spec.md (version N)?" followed by `summary`, where N is the document's latest version. It
|
|
155
|
+
is refused, with nothing sent, while that version holds a decision block open, and the refusal
|
|
156
|
+
names each block and its ask. An answer or a `dispatch_resolve_ask` closes the ask at once but
|
|
157
|
+
reaches a version only when the document settles, about two seconds later, or with your next
|
|
158
|
+
`dispatch_doc_edit`: fold the answer into the text (or, for a waiver, write the human's decision
|
|
159
|
+
in) and then request. A block written in the last few seconds counts as open before Dispatch has
|
|
160
|
+
opened its ask. A repeat at the same version returns the open request unchanged. A new version
|
|
161
|
+
retracts an open request for an older one, and its `ask.resolved` reaches you: request again for
|
|
162
|
+
the new version once its blocks are settled. The answer reaches you as `artifact.approved` or
|
|
163
|
+
`artifact.changes_requested` with the pinned `version`; `changes_requested` carries the reason,
|
|
164
|
+
which is your next piece of work. `dispatch_read` and `dispatch_doc_read` show the document's
|
|
165
|
+
approval state; `stale` means it was approved and then edited.
|
|
166
|
+
|
|
144
167
|
## A document that is reloading
|
|
145
168
|
|
|
146
169
|
These calls can answer `DOC_SERVICE_UNAVAILABLE` (HTTP 503), because each writes a document inside its
|
|
@@ -9,15 +9,14 @@ project's architecture model, or list or audit a project's backlog.
|
|
|
9
9
|
When you finish an issue, or are told to work on the next thing, take the top ready issue of the
|
|
10
10
|
whole backlog, across every project: status `todo`, highest priority first, then board rank. There
|
|
11
11
|
are no areas: a standing role, a product owner and a lane each take the top issue like everyone
|
|
12
|
-
else (
|
|
12
|
+
else (dispatch://AGENTC-34/ask/01ed2956-73cc-48d2-8ed4-7a86c6d439b1). `todo`
|
|
13
13
|
means ready: specced, unblocked, and waiting on neither a deploy nor a decision. An issue that
|
|
14
14
|
waits on one belongs in `backlog`, with what it waits on said on the issue.
|
|
15
15
|
|
|
16
16
|
Hold at most three issues in flight (`in_progress`, `testing`, `needs_review` or `retro`), of any
|
|
17
|
-
kind (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
dispatch://AGENTC-393/ask/b773d9f6): the priorities decide only what you pull next. Past three:
|
|
17
|
+
kind (dispatch://AGENTC-34/ask/1aeb8f2e-0950-4eaa-aaac-24286c9dd3ca). The limit is per agent and
|
|
18
|
+
has nothing to do with the week's priorities (dispatch://AGENTC-393/comment/a7647eb0): the
|
|
19
|
+
priorities decide only what you pull next. Past three:
|
|
21
20
|
push any unfinished work, say where in one comment on the issue, move it to `backlog` and clear
|
|
22
21
|
its route. Each issue counts on its own; a child does not ride under its parent's slot.
|
|
23
22
|
In-flight issues with no owner at all go
|
|
@@ -38,9 +37,7 @@ reordering the board yourself.
|
|
|
38
37
|
|
|
39
38
|
## Claim the issue before you work it
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
on it (Sami, 2026-09-24, verbatim: "It seems like we need a better way of tracking what's already
|
|
43
|
-
in progress"). So before you start implementing an issue, claim it:
|
|
40
|
+
Before you start implementing an issue, claim it:
|
|
44
41
|
|
|
45
42
|
```ts
|
|
46
43
|
dispatch_claim({ issue: "LEGION-234" }) // I am implementing this
|
|
@@ -76,8 +73,7 @@ you find work nobody is on.
|
|
|
76
73
|
|
|
77
74
|
**Claiming and moving the status are two separate actions, and you do both.** A claim says which
|
|
78
75
|
session is on the work; the status says where the work has got to, and humans use it to track
|
|
79
|
-
that too
|
|
80
|
-
using them to keep track of work"). So when you start: `dispatch_claim({ issue })` **and**
|
|
76
|
+
that too. So when you start: `dispatch_claim({ issue })` **and**
|
|
81
77
|
`dispatch_issue_update({ issue, status: "in_progress" })`.
|
|
82
78
|
|
|
83
79
|
## Issue status is yours to move
|
|
@@ -87,9 +83,8 @@ the daemon writes it), the session doing the work moves it, the way a person mov
|
|
|
87
83
|
`in_progress` when implementation starts, `testing` when the change is being proven on a
|
|
88
84
|
production-like surface, `needs_review` when its pull request is open and waiting on the merge
|
|
89
85
|
queue, `done` when the change has been driven in production (a merge is not `done`). Move child
|
|
90
|
-
issues you own as well as the root. An issue left at `triage` while work is underway is a defect
|
|
91
|
-
|
|
92
|
-
status is." Waiting for the deploy lane is not a status and is never announced.
|
|
86
|
+
issues you own as well as the root. An issue left at `triage` while work is underway is a defect.
|
|
87
|
+
Waiting for the deploy lane is not a status and is never announced.
|
|
93
88
|
|
|
94
89
|
```ts
|
|
95
90
|
// PATCH /api/v1/issues/{key} — status, title, labels, priority, external_links (merged by URL), route, parent
|
|
@@ -122,10 +117,9 @@ when work has started.
|
|
|
122
117
|
## Priority is yours to set
|
|
123
118
|
|
|
124
119
|
Priority is the coarse bucket a backlog is read by: `0` is P0, the highest, through `3`, P3, the
|
|
125
|
-
lowest, and `null` clears it.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
and say what you set and why; he overrides anything he disagrees with from the dashboard. A closed
|
|
120
|
+
lowest, and `null` clears it. Agents set it (`dispatch://LEGION/artifact/issue-status-conventions-md`)
|
|
121
|
+
— on creation, and on a grooming pass over issues that have none — and say what you set and why;
|
|
122
|
+
Sami overrides anything he disagrees with from the dashboard. A closed
|
|
129
123
|
issue takes only `rank`, `components`, and a reopening `status` (any status but `done`);
|
|
130
124
|
everything else, `priority` included, waits for the reopen (`409 ISSUE_CLOSED`). So reopen it
|
|
131
125
|
first, then set the priority — the two cannot go in one call. `rank` itself is not a tool field:
|
|
@@ -165,26 +159,21 @@ dispatch_issues({ project, priority: [0, 1], limit: 250 })
|
|
|
165
159
|
Every unclaimed row in `triage`, `icebox`, `backlog` or `todo` is a decision: someone takes it and
|
|
166
160
|
builds it, or it closes. A row in `in_progress`, `testing`, `needs_review` or `retro`, or one that
|
|
167
161
|
carries a claim, is work under way ([Issue status is yours to move](#issue-status-is-yours-to-move))
|
|
168
|
-
and is not re-staffed. A todo with a finished spec reads as queued work that nobody is doing
|
|
169
|
-
(LEGION-173 sat in todo for two weeks with a complete spec; AGENTC-1010's v4 plan sat in backlog
|
|
170
|
-
with nobody building it).
|
|
162
|
+
and is not re-staffed. A todo with a finished spec reads as queued work that nobody is doing.
|
|
171
163
|
|
|
172
164
|
A close that says the defect cannot happen cites the code that makes it impossible. An issue
|
|
173
165
|
closed because a rewrite forecloses it names the file and line in the rewrite that does so; a
|
|
174
166
|
close that cannot name one is not foreclosed, it is unread. The cheapest way for a rewrite to reach
|
|
175
|
-
parity is to port the code, defect included
|
|
176
|
-
the TypeScript daemon, had been ported into the Go coordinator and was live in production.
|
|
167
|
+
parity is to port the code, defect included.
|
|
177
168
|
|
|
178
169
|
The audit finds four shapes:
|
|
179
170
|
|
|
180
171
|
- **Unstaffed work.** A plan or measurement exists, and no one is building it.
|
|
181
172
|
- **Unrecorded delivery.** An issue not yet in `testing` or `done`, claimed or not, has a merged PR
|
|
182
|
-
naming it. Check the change live, then move the issue
|
|
183
|
-
agent-c #20367, merged).
|
|
173
|
+
naming it. Check the change live, then move the issue.
|
|
184
174
|
- **Unrecorded practice.** Someone does the issue's work by hand, more than once, while the issue
|
|
185
|
-
sits in backlog
|
|
186
|
-
|
|
187
|
-
wearing the wrong status.
|
|
175
|
+
sits in backlog. It leaves no plan and no PR to find; the tell is your own messages. Doing
|
|
176
|
+
something by hand more than once means an issue is wearing the wrong status.
|
|
188
177
|
- **Unreachable route.** An open issue whose route names a role nobody holds, or a session that is
|
|
189
178
|
not running, reaches nobody, whatever its priority, and the priority filter above never finds
|
|
190
179
|
it. List it on its own:
|
|
@@ -192,16 +181,14 @@ The audit finds four shapes:
|
|
|
192
181
|
dispatch_issues({ project, route_status: "no_holder", limit: 250 })
|
|
193
182
|
```
|
|
194
183
|
Each row reads `route role:sre (nobody holds it right now)` or `route session:<id> (that session
|
|
195
|
-
is not running right now)`. That is one read of the listener, and one read
|
|
196
|
-
|
|
197
|
-
is absent from the listener for minutes, and its role with it.
|
|
198
|
-
routes one read showed as unreachable pointed at a single session that was moving between boxes.
|
|
184
|
+
is not running right now)`. That is one read of the listener, and one read cannot tell a
|
|
185
|
+
restart gap from a vacancy: an agent box that restarts or resumes keeps the session id, but the
|
|
186
|
+
session is absent from the listener for minutes, and its role with it.
|
|
199
187
|
So a route is unowned only when it is `no_holder` on two reads at least ten minutes apart: list
|
|
200
188
|
again after ten minutes and act on the issues both lists name. Confirm with the second
|
|
201
189
|
`dispatch_issues` read, not `envoy_role_get`: a role lookup releases the claim of a holder whose
|
|
202
190
|
session is absent from the registry as it answers. Then staff the role, re-route the
|
|
203
|
-
issue to a live holder, or clear the route and assign it
|
|
204
|
-
503, sat routed to an unheld `role:sre` with no assignee). `route_status: "unknown"` means the
|
|
191
|
+
issue to a live holder, or clear the route and assign it. `route_status: "unknown"` means the
|
|
205
192
|
listener did not answer, so a route could not be judged; a `no_holder` filter refuses rather
|
|
206
193
|
than answer an empty list then.
|
|
207
194
|
|
|
@@ -84,12 +84,12 @@ Wave releases, child closures, and your own status are visible from the issue tr
|
|
|
84
84
|
handoffs; do not narrate them into the spec or a `dispatch_message`. A blocker only Sami can
|
|
85
85
|
clear is a `dispatch_ask`.
|
|
86
86
|
|
|
87
|
-
The issue's primary document **is** the root specification. Extend it in place
|
|
88
|
-
that keeps the human's own text and
|
|
89
|
-
|
|
87
|
+
The issue's primary document **is** the root specification. Extend it in place: a new version
|
|
88
|
+
that keeps the human's own text and grows the design (the adoption or decomposition and its
|
|
89
|
+
waves, how each outcome is proven, and the integration test), each open question a
|
|
90
|
+
[decision block](../dispatch/SKILL.md#decision-blocks). Never post a second "spec" artifact beside
|
|
90
91
|
it (`dispatch_artifact` with the primary document's name replaces the human's document; do not do
|
|
91
|
-
that).
|
|
92
|
-
must be able to follow it.
|
|
92
|
+
that).
|
|
93
93
|
The design gate runs only when the "Design gate policy" line at the end of your system prompt
|
|
94
94
|
says `gates.design: root-issues`. When it says `gates.design: off`, write the spec and continue
|
|
95
95
|
to section 2 with no approval step at all: do not request approval, do not register a gate, and
|
|
@@ -99,7 +99,11 @@ sequence **before any Legion-role spawn**, including a sub-architect:
|
|
|
99
99
|
|
|
100
100
|
```text
|
|
101
101
|
dispatch_doc_edit({ issue: "<root issue>", ... }) // extend the primary document in place
|
|
102
|
-
|
|
102
|
+
// then settle its decision blocks (below)
|
|
103
|
+
result = dispatch_request_approval({
|
|
104
|
+
issue: "<root issue>", // the primary document by default
|
|
105
|
+
summary: "<what the tree will do that the human hasn't already agreed to>",
|
|
106
|
+
})
|
|
103
107
|
legion({
|
|
104
108
|
op: "register_gate",
|
|
105
109
|
issue: "<root issue>",
|
|
@@ -108,33 +112,41 @@ legion({
|
|
|
108
112
|
})
|
|
109
113
|
```
|
|
110
114
|
|
|
115
|
+
The decision blocks come first: settle every one as
|
|
116
|
+
[Approval of a spec](../dispatch/SKILL.md#approval-of-a-spec) says before you request approval;
|
|
117
|
+
`dispatch_request_approval` refuses while one is open. Each answer reaches you, since you follow
|
|
118
|
+
every ask you open. `summary` says in one to three sentences what the tree will do that the human
|
|
119
|
+
hasn't already agreed to.
|
|
120
|
+
|
|
111
121
|
`dispatch_request_approval` opens a system question on the document with the fixed options
|
|
112
122
|
`Approve` and `Request changes`; a human answers it from the Inbox or approves from the
|
|
113
123
|
document's own header. Never open a `dispatch_ask` with an `Approve` option yourself: an
|
|
114
124
|
ordinary question is not a gate and the daemon ignores its answer. Copy `artifactId` and
|
|
115
125
|
`version` from the result of `dispatch_request_approval` — its text reads "Approval requested for
|
|
116
|
-
spec.md (document id <UUID>) at version <N>"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
spec.md (document id <UUID>) at version <N> (ask <id>)", followed by the question the human's
|
|
127
|
+
Inbox shows, and its `details.artifact` / `details.version` carry the same two values. The
|
|
128
|
+
document id is never the slug or file name you passed in (`spec`, `spec.md`): the daemon
|
|
129
|
+
recognizes the document's approval events by that id, and both the `legion` tool and the daemon
|
|
130
|
+
refuse a value that is not a UUID. Calling `dispatch_request_approval` again at the version an
|
|
131
|
+
open request names returns that request unchanged, so it is safe to repeat; once the document has
|
|
132
|
+
a newer version, that request is retracted (its `ask.resolved` reaches you) and the call opens a
|
|
133
|
+
new one at the latest version. If its text instead reads "spec.md (document id <UUID>) is already
|
|
134
|
+
approved at version <N>" — a human approved from the document header before you asked — still
|
|
135
|
+
call `register_gate` with that id and version: the daemon reads the approval from Dispatch as it
|
|
124
136
|
registers, opens the gate, and delivers `design-approved` at once. The same read covers a human
|
|
125
137
|
who answers the question between your `dispatch_request_approval` and `register_gate` calls, so
|
|
126
138
|
an approval is never lost to timing; you never approve anything yourself.
|
|
127
139
|
|
|
128
140
|
Then park. Do not release a wave or spawn a Legion role until a later delivered wake shows
|
|
129
141
|
`design-approved` on the root. On `design-changes-requested`, revise the spec (a new version of
|
|
130
|
-
the primary document), call `dispatch_request_approval` again
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
event on your issue tells you), so call
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
the tree.
|
|
142
|
+
the primary document), call `dispatch_request_approval` again with a `summary` of what the
|
|
143
|
+
revision proposes — it opens the request at the new version — and stay parked. Approval is
|
|
144
|
+
pinned to the spec version: editing the root spec after approval closes the gate again with no
|
|
145
|
+
wake (you made the edit, or the `artifact.version` event on your issue tells you), so call
|
|
146
|
+
`dispatch_request_approval` again with a `summary`, and release no new wave and spawn no new role
|
|
147
|
+
until the next `design-approved` arrives — work already in flight continues. Later waves,
|
|
148
|
+
re-scopes, and integration-failure children that leave the root spec untouched need no new
|
|
149
|
+
approval, and a child issue's spec is never gated: the root approval covers the tree.
|
|
138
150
|
|
|
139
151
|
## 2. Children in flight
|
|
140
152
|
|
|
@@ -307,7 +319,7 @@ active phase worker.
|
|
|
307
319
|
| `children-complete` | Execute steps 3–4: parent integration verification; failures become a new child wave, success advances to review and retro. |
|
|
308
320
|
| `child-reopened` | Treat the completion edge as reset. Reassess the reopened child and return the tree to children-in-flight; do not continue an already-started end-game. |
|
|
309
321
|
| `design-approved` | Payload `{type:"design-approved"}`. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
|
|
310
|
-
| `design-changes-requested` | Payload `{type:"design-changes-requested", version, reason, author?}`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec, call `dispatch_request_approval` again, and stay parked; the gate is closed. |
|
|
322
|
+
| `design-changes-requested` | Payload `{type:"design-changes-requested", version, reason, author?}`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec, call `dispatch_request_approval` again with a `summary` of what the revision proposes, and stay parked; the gate is closed. |
|
|
311
323
|
| `phase-complete` | Payload `{type:"phase-complete", issue, role, summary}`. May arrive live or via `catchup-overseer`'s `phaseCompletions`. Read the committed handoff for that phase, then spawn the next phase's owner, or `spawn_worker` on the same role again to resume it with corrections if the handoff shows unresolved gaps. A `reviewer` completion whose GitHub review is `CHANGES_REQUESTED` (the daemon returns the issue's Dispatch status to `in_progress` for this, on the reviewer's completion and again when you spawn the corrective implementer unless the daemon already knows the issue is `in_progress`) means `spawn_worker` the **implementer** again with the review findings — thread URLs and blocking items — as its task, then route back through tester and reviewer in order; never `spawn_worker` the reviewer directly off this wake and never proceed to retro on this verdict. A reviewer completion with an `APPROVED` review proceeds to retro (step 5). A `reviewer` completion after a conflict-forced rebase whose review body names an unchanged fingerprint is a confirmation, not a round: if retro already completed, `spawn_worker` the merger; otherwise resume the step you were on. An `implementer` completion that follows the merge is its production report: read the record on the pull request and the issue, then run step 7 — the issue is already at `retro`, the daemon writes no status for this completion, and you set `done` yourself. A `tester` completion whose handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof, goes back to the **implementer** with that finding — never forward to the reviewer, and never by supplying the proof from another role. A worker that reports no surface reaches the changed path gets a child issue in this tree (infrastructure, tooling, or a skill) and a resume once it lands; that report is never a reason to advance the phase. |
|
|
312
324
|
| `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. `legion state` shows the queue (`workerAdmission.queue`: role token, issue, role, kind, and the time the task was first queued — never the task text); read it before re-sending. A `spawn_worker` identical to the queued task changes nothing and is not announced again. Different text replaces the queued task silently in the same FIFO slot and retains its original queue time. A `spawn_worker` that fails with "got no response in 3 attempts" was already retried by the plugin under one request id and may still have reached the daemon: read the queue and the role's claim in `legion state` before sending it again. |
|
|
313
325
|
| `worker-started` | Payload `{type:"worker-started", issue, role}`. A previously queued role has been promoted and is now running. Treat it exactly as a normal spawn: resume tracking that role's live session. |
|
|
@@ -331,9 +343,9 @@ Sami directly with `dispatch_ask` the same way. Do not create a wait loop for an
|
|
|
331
343
|
source.
|
|
332
344
|
|
|
333
345
|
Never yield while waiting on a human. A human is waiting on you only where an open ask sits
|
|
334
|
-
in their inbox, so open it
|
|
335
|
-
|
|
336
|
-
on nobody stalls the tree until someone notices.
|
|
346
|
+
in their inbox, so open it before you stop: `dispatch_ask`, a decision block in the spec, or
|
|
347
|
+
`dispatch_request_approval` for the spec gate. Otherwise proceed: proceeding is the default, and
|
|
348
|
+
a stop that waits on nobody stalls the tree until someone notices.
|
|
337
349
|
|
|
338
350
|
## Architecture components
|
|
339
351
|
|
|
@@ -77,8 +77,15 @@ passed — never as a fresh identity.
|
|
|
77
77
|
|
|
78
78
|
Deployment instructions, when present, are the operator's standing rules for this repository —
|
|
79
79
|
required checks, deploy/smoke commands, code-owner expectations, standing roles you may consult,
|
|
80
|
-
the merge credential. They override this skill's defaults where they conflict
|
|
81
|
-
override
|
|
80
|
+
the merge credential. They override this skill's defaults where they conflict, except four rules
|
|
81
|
+
they never override: no deferrals (*PR body, review, and the merge gate*, below); bringing the base
|
|
82
|
+
into the branch only on a real conflict or a retarget
|
|
83
|
+
(`skill://legion-worker/references/conflicts-and-rewrites.md#reintegrating-the-base`); the
|
|
84
|
+
implementer's own proof on a production-like surface at the head that merges, an applied simplify
|
|
85
|
+
head included (`skill://legion-worker/references/pr-body.md#what-a-proof-is`,
|
|
86
|
+
`skill://legion-worker/references/pr-body.md#the-rules-every-phases-evidence-follows`); and the
|
|
87
|
+
implementer's production check after the merge
|
|
88
|
+
(`skill://legion-worker/references/merge-gate.md#after-the-human-merge`).
|
|
82
89
|
|
|
83
90
|
## Asking another role
|
|
84
91
|
|
|
@@ -145,8 +152,7 @@ new work.
|
|
|
145
152
|
|
|
146
153
|
**Shared operation safety:** Every Legion issue workspace is a `jj workspace` of one shared
|
|
147
154
|
clone, so they all share one operation log: `jj undo`, `jj abandon`, and
|
|
148
|
-
`jj op restore|revert|abandon|undo` rewrite it for every tree at once
|
|
149
|
-
worker's `jj undo` rewrote nine of another tree's commits). The extension refuses them in every
|
|
155
|
+
`jj op restore|revert|abandon|undo` rewrite it for every tree at once. The extension refuses them in every
|
|
150
156
|
phase-worker pane before they run — a `bash` command in any position of a pipeline or `&&`
|
|
151
157
|
chain, with or without `-R`, judged on the whole argument list; `eval` code; and a `hub`
|
|
152
158
|
process start — from your own tool calls and from any `task` subagent you spawn (it runs in
|
|
@@ -196,7 +202,7 @@ committer at all, and the one rewrite still open to you (*Rewriting pushed commi
|
|
|
196
202
|
reference) resets the committer only of commits on your own chain that descend from the commit
|
|
197
203
|
you named, after its guard cleared. Another role's commit
|
|
198
204
|
carrying you as committer, which you did not rewrite that way, is evidence that something
|
|
199
|
-
rewrote commits it should not have
|
|
205
|
+
rewrote commits it should not have. Stop and send the
|
|
200
206
|
architect that log; do not accept it as a side effect. A wrong identity on your own commit, the
|
|
201
207
|
other App or none, is a pane-environment problem to report to the architect, not something to
|
|
202
208
|
pin (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`,
|
|
@@ -220,7 +226,7 @@ subcommand's `comment`, `create`, `edit`, `close`, `reopen`, `delete`, `pin`, `u
|
|
|
220
226
|
GET (an explicit `-X`, or the POST that `-f`/`-F`/`--input` imply; pull-request conversation
|
|
221
227
|
comments live on that path too, so edit them with `gh pr comment`) — printing
|
|
222
228
|
`Legion issues live on Dispatch; use dispatch_message or dispatch_comment on <your LEGION_ISSUE>`:
|
|
223
|
-
Legion never reads or writes a GitHub issue
|
|
229
|
+
Legion never reads or writes a GitHub issue. `pr comment`, `pr review`,
|
|
224
230
|
`api …/pulls/…`, `api graphql`, and issue reads are unaffected. The credential reaches `legion`
|
|
225
231
|
through the file `$LEGION_GRANT_FILE` names, written by the extension before each of your bash
|
|
226
232
|
commands, each `github` tool call, and each `read`/`grep` of a `pr://` or `issue://` URL (and by
|
|
@@ -294,7 +300,7 @@ line), the full definition of a proof, what the tester verifies, and the simplif
|
|
|
294
300
|
thread's opener (or, on a bot's thread, from the Legion reviewer) closes one. The implementer
|
|
295
301
|
runs `legion threads resolve` before every push that answers a review, and the merger before
|
|
296
302
|
READY: `skill://legion-worker/references/review-threads.md`.
|
|
297
|
-
- **No deferrals.**
|
|
303
|
+
- **No deferrals.** A finding that changes
|
|
298
304
|
behaviour, hides an error, or breaks a gate is fixed in this pull request; naming, duplication,
|
|
299
305
|
or wording cleanup is batched into the one `Fast-follow:` line instead of iterating per push.
|
|
300
306
|
- **A red CI job** that failed on its own is re-run with
|
|
@@ -329,7 +335,9 @@ with. With `--data` omitted, `legion handoff write` reads the JSON object from s
|
|
|
329
335
|
|
|
330
336
|
`handoff_write` validates the payload against the phase's schema before writing: an
|
|
331
337
|
implement handoff without a well-formed `proof`, or a test handoff that reports no failure and
|
|
332
|
-
carries no `proof` of its own, exits 1 naming the field and writes nothing.
|
|
338
|
+
carries no `proof` of its own, exits 1 naming the field and writes nothing. Each `proof` entry, in
|
|
339
|
+
either phase, is an object of six non-empty strings: `criterion` (the acceptance line it proves),
|
|
340
|
+
`surface`, `command`, `observed`, `headSha` (the commit it ran at) and `negativeControl`.
|
|
333
341
|
|
|
334
342
|
Then verify the durable artifact exists:
|
|
335
343
|
|
|
@@ -8,8 +8,7 @@ Every path it cites is in sjawhar/legion.
|
|
|
8
8
|
## Reintegrating the base
|
|
9
9
|
|
|
10
10
|
- **Reintegrate the base only on a real conflict, except after a base retarget — and with a
|
|
11
|
-
merge, never `jj rebase`.**
|
|
12
|
-
"Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
|
|
11
|
+
merge, never `jj rebase`.**
|
|
13
12
|
The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
|
|
14
13
|
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never reintegrate the base to
|
|
15
14
|
pick up `main` or refresh CI (a single failed CI job is re-run on its own: *A red CI job* in
|
|
@@ -24,8 +23,7 @@ Every path it cites is in sjawhar/legion.
|
|
|
24
23
|
jj always rebases every descendant of any commit it rewrites — a revset naming the root of your
|
|
25
24
|
own chain and rewriting it in place also rewrites whatever another tree has stacked on that root,
|
|
26
25
|
whichever selector chose it (`-s`, `-b`, and `-r` all rewrite descendants; `-r` only re-parents
|
|
27
|
-
them to fill the hole, which is worse).
|
|
28
|
-
conflict step moved a second issue's twelve commits and its bookmark onto a conflicted copy.
|
|
26
|
+
them to fill the hole, which is worse).
|
|
29
27
|
Resolve the conflict with a forward merge instead of a rewrite — merge the branch's own
|
|
30
28
|
bookmark with the destination in one new commit, so nothing existing is rewritten and nothing
|
|
31
29
|
built on your prior commits, in this tree or another, ever moves:
|
|
@@ -91,7 +89,8 @@ and the new head; the merger never computes a fingerprint — it uses the `--sum
|
|
|
91
89
|
## Rewriting pushed commits
|
|
92
90
|
|
|
93
91
|
**Rewriting pushed commits** — a `jj squash --into` a commit already on GitHub, or any other
|
|
94
|
-
rewrite of a commit you already pushed — is the
|
|
92
|
+
rewrite of a commit you already pushed — is the hazard *Reintegrating the base* describes, in a
|
|
93
|
+
second shape: jj rebases
|
|
95
94
|
every descendant of any commit it rewrites, and in the one shared repository a descendant can be
|
|
96
95
|
another tree's branch stacked on your pushed commit, which then moves, with its bookmark, onto a
|
|
97
96
|
rewritten copy. So look for a descendant outside your own chain first, and record the pushed tip
|
|
@@ -115,9 +114,7 @@ cd -- "$LEGION_WORKSPACE" && \
|
|
|
115
114
|
`descendants(<commit>) ~ ::@` is everything built on the commit you are about to rewrite that is
|
|
116
115
|
not on your own chain. Non-empty means the rewrite would move work that is not yours: do not
|
|
117
116
|
rewrite it. Put the change in a new commit on top instead, and report the listed commits to the
|
|
118
|
-
architect.
|
|
119
|
-
`Rebased 13 descendant commits` and moved a second issue's twelve commits and its bookmark; the
|
|
120
|
-
check above listed those thirteen and refused before anything moved.
|
|
117
|
+
architect.
|
|
121
118
|
|
|
122
119
|
Then rewrite, resolve, and push with the one push procedure (*Every role pushes its own commits*
|
|
123
120
|
in `skill://legion-worker`). It lets the remote branch sit on the
|
|
@@ -82,15 +82,13 @@ completion leaves the issue in reviewing until you finish.
|
|
|
82
82
|
|
|
83
83
|
## After the human merge
|
|
84
84
|
|
|
85
|
-
- **After a human merges, the implementer verifies in production.**
|
|
86
|
-
verbatim: "the agent that developed it should be responsible for testing in production."
|
|
85
|
+
- **After a human merges, the implementer verifies in production.**
|
|
87
86
|
The architect sends the implementer back once the merge lands; the implementer watches the
|
|
88
87
|
deploy slot that carries the merge to `production-apply` (or the equivalent publish step),
|
|
89
88
|
drives the changed path in production through the user's own access path, and records the
|
|
90
89
|
observation on the PR and the issue before the architect signs off. A staging pass is not
|
|
91
|
-
this
|
|
92
|
-
|
|
93
|
-
implementer owns the fix and the next slot.
|
|
90
|
+
this, since a staging gate does not run every resource production does. If the slot fails on
|
|
91
|
+
the change, the implementer owns the fix and the next slot.
|
|
94
92
|
The record has three places: the PR body's `Production:` line, one pull-request comment
|
|
95
93
|
carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
|
|
96
94
|
read GitHub, the architect reads the issue. When the deploy that carries the merge has not
|
|
@@ -51,17 +51,10 @@ a deliberately broken input and the refusal or failure observed. The surface is
|
|
|
51
51
|
repository, a real browser, a devN stack, staging, or a local stack with real migrations, one that
|
|
52
52
|
has the resource the change touches — and each `E2E` line carries a **link** to that run,
|
|
53
53
|
screenshot, or e2e; human review does not replace user-facing verification, and a green unit suite
|
|
54
|
-
is not it. A unit or integration test is a regression lock, never proof of a criterion.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
need to fix it; if it's tooling, we need to develop it; if it's skills, we need to fix the skills
|
|
59
|
-
... it should not require deploying to production to realize your feature doesn't work."
|
|
60
|
-
Evidence for the rule: in the week of 2026-09-08 three surfaces merged green and were wrong on
|
|
61
|
-
inspection (the Astrolabe IPI stack, Dispatch on ECS, the candidate flow), and on 2026-09-12 six
|
|
62
|
-
deploy slots died on code first executed after merge, including a production-only ECS bootstrap
|
|
63
|
-
the whole staging gate never ran. The implementer's proof and the tester's proof below are both
|
|
64
|
-
this proof.
|
|
54
|
+
is not it. A unit or integration test is a regression lock, never proof of a criterion. The agent
|
|
55
|
+
that develops the change proves it this way before the merge, and whatever blocks that proof is
|
|
56
|
+
fixed, not skipped (*When no surface reaches the changed path*, below). The implementer's proof
|
|
57
|
+
and the tester's proof below are both this proof.
|
|
65
58
|
|
|
66
59
|
## The rules every phase's evidence follows
|
|
67
60
|
|
|
@@ -98,8 +91,8 @@ this proof.
|
|
|
98
91
|
diff gets none.** It is scoped to the pull request's own diff, at the head where the last review
|
|
99
92
|
round closed: nothing applied leaves that head final; applied → the applied head is the final
|
|
100
93
|
head: CI runs on it, the pair runs once on it, and the E2E proof re-runs on it for the surface
|
|
101
|
-
the simplify diff touched
|
|
102
|
-
|
|
94
|
+
the simplify diff touched, since a refactor that "preserves behaviour" is a claim until it is
|
|
95
|
+
executed. That cost is
|
|
103
96
|
why 0-applied is the expected outcome and a pass that applies is spent sparingly. At the applied
|
|
104
97
|
head the implementer re-cites the `CI` line and re-runs its own proof into `E2E (implementer)`,
|
|
105
98
|
and the tester re-runs its proof for the touched surface into `E2E (tester)`, before the
|
|
@@ -116,10 +109,8 @@ No surface reaches the changed path is a report to the architect, never a reason
|
|
|
116
109
|
Say which surface is missing and what it would have to do — a rig that can spawn the role, a
|
|
117
110
|
sandbox that holds the resource, a credential, a command that does not exist yet — and send it to
|
|
118
111
|
the architect with `envoy_publish` to its role topic. The architect creates a child issue in this
|
|
119
|
-
tree to build it (infrastructure, tooling, or a skill) and resumes you once it lands.
|
|
120
|
-
|
|
121
|
-
infrastructure, we need to fix it; if it's tooling, we need to develop it; if it's skills, we need
|
|
122
|
-
to fix the skills." A code path whose first execution would be after the merge — a deploy
|
|
112
|
+
tree to build it (infrastructure, tooling, or a skill) and resumes you once it lands. A code path
|
|
113
|
+
whose first execution would be after the merge — a deploy
|
|
123
114
|
workflow's inline step, a post-merge helper, a production-only resource — is untested until you
|
|
124
115
|
have executed it somewhere production-like; completing with a unit-test-only handoff is the
|
|
125
116
|
failure this rule exists to stop.
|