@sjawhar/pi-legion 8.4.2 → 8.5.1
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/legion.js +147 -25
- package/dist/skills/legion-architect/SKILL.md +47 -49
- package/dist/skills/legion-controller/SKILL.md +38 -32
- package/dist/skills/legion-retro/SKILL.md +7 -8
- package/dist/skills/legion-worker/SKILL.md +20 -15
- package/dist/skills/legion-worker/references/merge-gate.md +3 -3
- package/package.json +3 -3
package/dist/legion.js
CHANGED
|
@@ -13918,7 +13918,7 @@ var AGENT_STREAM_LIMITS = {
|
|
|
13918
13918
|
var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
|
|
13919
13919
|
var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
|
|
13920
13920
|
// ../contracts/src/dispatch-tools.ts
|
|
13921
|
-
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only
|
|
13921
|
+
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only dispatch issue with --external creates a native issue.";
|
|
13922
13922
|
var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
|
|
13923
13923
|
var ASK_QUESTION_CONTRACT = "The question carries the problem the reader recognises and why it matters now, what constrains " + "the answer, and the recommendation with its reason. It asks how to solve the problem or which " + "outcome is wanted; never enumerate choices in the question.";
|
|
13924
13924
|
var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
|
|
@@ -14019,7 +14019,7 @@ var dispatchToolSpecs = [
|
|
|
14019
14019
|
{
|
|
14020
14020
|
name: "dispatch_issue",
|
|
14021
14021
|
example: { project: "DSP", title: "Native workspace" },
|
|
14022
|
-
description: "Create a native Dispatch issue for newly tracked work. Search first with
|
|
14022
|
+
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. " + "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. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
14023
14023
|
arguments: (z2) => ({
|
|
14024
14024
|
project: z2.string().describe("Project key for the new issue."),
|
|
14025
14025
|
title: z2.string().describe("Concise issue title."),
|
|
@@ -14066,7 +14066,7 @@ var dispatchToolSpecs = [
|
|
|
14066
14066
|
{
|
|
14067
14067
|
name: "dispatch_claim",
|
|
14068
14068
|
example: { issue: "DSP-1" },
|
|
14069
|
-
description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or release
|
|
14069
|
+
description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, so also move the issue to in_progress with " + "dispatch issue-update when you start. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
|
|
14070
14070
|
arguments: (z2) => ({
|
|
14071
14071
|
issue: z2.string().describe(ISSUE_REFERENCE),
|
|
14072
14072
|
release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
|
|
@@ -14089,7 +14089,7 @@ var dispatchToolSpecs = [
|
|
|
14089
14089
|
}
|
|
14090
14090
|
]
|
|
14091
14091
|
},
|
|
14092
|
-
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 (
|
|
14092
|
+
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. " + 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. " + "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}`,
|
|
14093
14093
|
arguments: (z2) => ({
|
|
14094
14094
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14095
14095
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14163,7 +14163,7 @@ var dispatchToolSpecs = [
|
|
|
14163
14163
|
{
|
|
14164
14164
|
name: "dispatch_resolve_comment",
|
|
14165
14165
|
example: { comment: "01234567-0000-4000-8000-000000000001" },
|
|
14166
|
-
description: "Resolve a review comment thread once it has been addressed - typically your own comment " + "after the document was fixed. Any session or human may resolve any open comment on an " + "open issue or project document; reopening a resolved comment is human-only (the dashboard). " + "Not for asks: use
|
|
14166
|
+
description: "Resolve a review comment thread once it has been addressed - typically your own comment " + "after the document was fixed. Any session or human may resolve any open comment on an " + "open issue or project document; reopening a resolved comment is human-only (the dashboard). " + "Not for asks: use dispatch resolve-ask.",
|
|
14167
14167
|
arguments: (z2) => ({
|
|
14168
14168
|
comment: z2.string().describe("Comment id (uuid), or a dispatch://KEY/comment/<id> or " + "dispatch://PROJECT/artifact/<slug>/comment/<id> reference; a reference accepts an " + "8+ character id prefix that is unique on its owner.")
|
|
14169
14169
|
}),
|
|
@@ -14182,7 +14182,7 @@ var dispatchToolSpecs = [
|
|
|
14182
14182
|
{
|
|
14183
14183
|
name: "dispatch_comment",
|
|
14184
14184
|
example: { issue: "DSP-1", body: "Looks good." },
|
|
14185
|
-
description: "Add review feedback to an issue or project document quote, or reply to a question asked with
|
|
14185
|
+
description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch ask. " + "Do not use it for an exact replacement; use " + `dispatch suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters, the lines images appends included. ${OWNER_REFERENCE}`,
|
|
14186
14186
|
arguments: (z2) => ({
|
|
14187
14187
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14188
14188
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14206,7 +14206,7 @@ var dispatchToolSpecs = [
|
|
|
14206
14206
|
quote: "old wording",
|
|
14207
14207
|
replace_with: "new wording"
|
|
14208
14208
|
},
|
|
14209
|
-
description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `
|
|
14209
|
+
description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `dispatch comment instead. Optional explanation is at most 2,000 characters. ${OWNER_REFERENCE}`,
|
|
14210
14210
|
arguments: (z2) => ({
|
|
14211
14211
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14212
14212
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14222,7 +14222,7 @@ var dispatchToolSpecs = [
|
|
|
14222
14222
|
{
|
|
14223
14223
|
name: "dispatch_message",
|
|
14224
14224
|
example: { issue: "DSP-1", body: "Implementation started." },
|
|
14225
|
-
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (
|
|
14225
|
+
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) or document feedback (dispatch comment). " + "To answer a human's direct message to this session - one sent from the Agents page, which names no issue - pass that message's bare id as " + "in_reply_to and no issue; the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch read --message <id> reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters, the lines images appends included. ${ISSUE_REFERENCE}`,
|
|
14226
14226
|
arguments: (z2) => ({
|
|
14227
14227
|
issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
|
|
14228
14228
|
body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Update text, at most 2,000 characters."),
|
|
@@ -14239,7 +14239,7 @@ var dispatchToolSpecs = [
|
|
|
14239
14239
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
14240
14240
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
14241
14241
|
},
|
|
14242
|
-
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
|
|
14242
|
+
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. " + "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}`,
|
|
14243
14243
|
arguments: (z2) => ({
|
|
14244
14244
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14245
14245
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14259,7 +14259,7 @@ var dispatchToolSpecs = [
|
|
|
14259
14259
|
attributes: z2.unknown().describe("Typed block attributes for retype; a CR LF or a lone carriage return in a string value is written as a line feed.").optional()
|
|
14260
14260
|
}, { strict: true })).describe("Flat tagged edits; an operation takes only the keys below, a misspelled one is refused rather than ignored, and the server validates the fields its op requires."),
|
|
14261
14261
|
precondition: z2.object({
|
|
14262
|
-
document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by
|
|
14262
|
+
document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch doc-read.").optional(),
|
|
14263
14263
|
blocks: z2.array(z2.object({
|
|
14264
14264
|
id: z2.string({ min: 1 }).describe("Stable block id from GET /api/v1/artifacts/{id}/blocks."),
|
|
14265
14265
|
token: z2.string({ min: 1 }).describe("That block's full-state token, including inline marks.")
|
|
@@ -14272,7 +14272,7 @@ var dispatchToolSpecs = [
|
|
|
14272
14272
|
{
|
|
14273
14273
|
name: "dispatch_doc_read",
|
|
14274
14274
|
example: { issue: "DSP-1" },
|
|
14275
|
-
description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use
|
|
14275
|
+
description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
|
|
14276
14276
|
arguments: (z2) => ({
|
|
14277
14277
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14278
14278
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14285,7 +14285,7 @@ var dispatchToolSpecs = [
|
|
|
14285
14285
|
{
|
|
14286
14286
|
name: "dispatch_request_approval",
|
|
14287
14287
|
example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
|
|
14288
|
-
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. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it;
|
|
14288
|
+
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. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "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,
|
|
14289
14289
|
arguments: (z2) => ({
|
|
14290
14290
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14291
14291
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14298,7 +14298,7 @@ var dispatchToolSpecs = [
|
|
|
14298
14298
|
name: "dispatch_artifact",
|
|
14299
14299
|
example: { issue: "DSP-1", name: "design.md", content: `# Design
|
|
14300
14300
|
` },
|
|
14301
|
-
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "
|
|
14301
|
+
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch doc-edit instead. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "Markdown 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; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
|
|
14302
14302
|
arguments: (z2) => ({
|
|
14303
14303
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14304
14304
|
project: z2.string().describe("Project key for an unlinked document.").optional(),
|
|
@@ -14318,7 +14318,7 @@ var dispatchToolSpecs = [
|
|
|
14318
14318
|
{
|
|
14319
14319
|
name: "dispatch_read",
|
|
14320
14320
|
example: { issue: "DSP-1" },
|
|
14321
|
-
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use
|
|
14321
|
+
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch doc-read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch doc-read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch doc-read shows it again. " + OWNER_REFERENCE,
|
|
14322
14322
|
arguments: (z2) => ({
|
|
14323
14323
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14324
14324
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14353,7 +14353,7 @@ var dispatchToolSpecs = [
|
|
|
14353
14353
|
{
|
|
14354
14354
|
name: "dispatch_issues",
|
|
14355
14355
|
example: { project: "PROJ", limit: 250, offset: 250 },
|
|
14356
|
-
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase;
|
|
14356
|
+
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
|
|
14357
14357
|
arguments: (z2) => ({
|
|
14358
14358
|
project: z2.string().describe("Project key to list issues from."),
|
|
14359
14359
|
status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
|
|
@@ -14480,6 +14480,13 @@ function textHead(text) {
|
|
|
14480
14480
|
return flat.length > HEAD_LENGTH ? `${flat.slice(0, HEAD_LENGTH)}\u2026` : flat;
|
|
14481
14481
|
}
|
|
14482
14482
|
|
|
14483
|
+
// ../envoy-client/src/dispatch-command.ts
|
|
14484
|
+
function commandName(tool) {
|
|
14485
|
+
return tool.replace(/^dispatch_/, "").replaceAll("_", "-");
|
|
14486
|
+
}
|
|
14487
|
+
var toolsByCommand = new Map(dispatchToolSpecs.map((spec) => [commandName(spec.name), spec.name]));
|
|
14488
|
+
var surfaces = new Map;
|
|
14489
|
+
|
|
14483
14490
|
// ../envoy-client/src/dispatch-cwd.ts
|
|
14484
14491
|
import { execFile } from "child_process";
|
|
14485
14492
|
import { promisify } from "util";
|
|
@@ -14962,7 +14969,7 @@ var picturesShown = new Map;
|
|
|
14962
14969
|
|
|
14963
14970
|
// ../envoy-client/src/dispatch-execute.ts
|
|
14964
14971
|
var triageAdviceShown = new Set;
|
|
14965
|
-
var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for
|
|
14972
|
+
var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch doc-read); a dashboard URL on this Dispatch server is accepted too";
|
|
14966
14973
|
var toolSchemas = new Map;
|
|
14967
14974
|
function artifactByReference(artifacts, artifactReference, owner, canonical) {
|
|
14968
14975
|
const bySlug = artifacts.find((candidate) => candidate.slug === artifactReference);
|
|
@@ -15032,6 +15039,51 @@ function dispatchAnswered(error48, status) {
|
|
|
15032
15039
|
return error48 instanceof DispatchServiceError && error48.fromDispatch && error48.status === status;
|
|
15033
15040
|
}
|
|
15034
15041
|
|
|
15042
|
+
// ../envoy-client/src/dispatch-session-state.ts
|
|
15043
|
+
import {
|
|
15044
|
+
appendFileSync,
|
|
15045
|
+
closeSync,
|
|
15046
|
+
fstatSync,
|
|
15047
|
+
mkdirSync,
|
|
15048
|
+
openSync,
|
|
15049
|
+
readdirSync,
|
|
15050
|
+
readFileSync as readFileSync3,
|
|
15051
|
+
readSync,
|
|
15052
|
+
renameSync,
|
|
15053
|
+
rmSync,
|
|
15054
|
+
statSync,
|
|
15055
|
+
writeFileSync
|
|
15056
|
+
} from "fs";
|
|
15057
|
+
import { homedir as homedir2 } from "os";
|
|
15058
|
+
import { join as join2 } from "path";
|
|
15059
|
+
var SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
15060
|
+
var PRIVATE_DIR = 448;
|
|
15061
|
+
var PRIVATE_FILE = 384;
|
|
15062
|
+
var IDLE_SESSION_MS = 14 * 86400000;
|
|
15063
|
+
function sessionStateRoot(env) {
|
|
15064
|
+
if (env.DISPATCH_STATE_DIR)
|
|
15065
|
+
return env.DISPATCH_STATE_DIR;
|
|
15066
|
+
return join2(env.XDG_STATE_HOME || join2(homedir2(), ".local", "state"), "dispatch");
|
|
15067
|
+
}
|
|
15068
|
+
function sessionDirectory(env, sessionId) {
|
|
15069
|
+
if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
|
|
15070
|
+
throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
|
|
15071
|
+
}
|
|
15072
|
+
return join2(sessionStateRoot(env), "sessions", sessionId);
|
|
15073
|
+
}
|
|
15074
|
+
function ensureDirectory(dir) {
|
|
15075
|
+
mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR });
|
|
15076
|
+
}
|
|
15077
|
+
function writeAtomically(path2, text) {
|
|
15078
|
+
const temporary = `${path2}.${process.pid}.${Date.now()}.tmp`;
|
|
15079
|
+
writeFileSync(temporary, text, { mode: PRIVATE_FILE });
|
|
15080
|
+
renameSync(temporary, path2);
|
|
15081
|
+
}
|
|
15082
|
+
function writeSessionTitle(dir, title) {
|
|
15083
|
+
ensureDirectory(dir);
|
|
15084
|
+
writeAtomically(join2(dir, "title"), title);
|
|
15085
|
+
}
|
|
15086
|
+
|
|
15035
15087
|
// ../pi-shared/src/interface.ts
|
|
15036
15088
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
15037
15089
|
var ENVOY_PLUGIN_INTERFACE_VERSION = 1;
|
|
@@ -15110,6 +15162,70 @@ function matchInjectedUserTurn(sessionID, message) {
|
|
|
15110
15162
|
return sent.messageId;
|
|
15111
15163
|
}
|
|
15112
15164
|
|
|
15165
|
+
// ../pi-shared/src/shell-command.ts
|
|
15166
|
+
var HEREDOC_OPENING = /^(.*?)[ \t]*<<(-?)[ \t]*'([A-Za-z_][A-Za-z0-9_]*)'[ \t]*$/;
|
|
15167
|
+
var BLANK_LINE = /^[ \t]*$/;
|
|
15168
|
+
var REFUSED_UNQUOTED = "\n;&|$`<>()#\\";
|
|
15169
|
+
var REFUSED_DOUBLE_QUOTED = "$`\\";
|
|
15170
|
+
function dispatchCommandHead(command) {
|
|
15171
|
+
if (typeof command !== "string" || hasControlCharacter(command))
|
|
15172
|
+
return;
|
|
15173
|
+
const all = command.split(`
|
|
15174
|
+
`);
|
|
15175
|
+
const first = all.findIndex((line) => !BLANK_LINE.test(line));
|
|
15176
|
+
if (first === -1)
|
|
15177
|
+
return;
|
|
15178
|
+
const last = all.findLastIndex((line) => !BLANK_LINE.test(line));
|
|
15179
|
+
const lines = all.slice(first, last + 1);
|
|
15180
|
+
let head = lines[0] ?? "";
|
|
15181
|
+
if (lines.length > 1) {
|
|
15182
|
+
const opening = HEREDOC_OPENING.exec(head);
|
|
15183
|
+
if (opening === null)
|
|
15184
|
+
return;
|
|
15185
|
+
const stripTabs = opening[2] === "-";
|
|
15186
|
+
const delimiter = opening[3];
|
|
15187
|
+
const end = lines.findIndex((line, index) => index > 0 && (stripTabs ? line.replace(/^\t+/, "") : line) === delimiter);
|
|
15188
|
+
if (end !== lines.length - 1)
|
|
15189
|
+
return;
|
|
15190
|
+
head = opening[1] ?? "";
|
|
15191
|
+
}
|
|
15192
|
+
head = head.replace(/^[ \t]+|[ \t]+$/g, "");
|
|
15193
|
+
return firstWord(head) === "dispatch" && headScan(head) ? head : undefined;
|
|
15194
|
+
}
|
|
15195
|
+
function firstWord(head) {
|
|
15196
|
+
return head.split(/[ \t]/, 1)[0] ?? "";
|
|
15197
|
+
}
|
|
15198
|
+
function hasControlCharacter(text) {
|
|
15199
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
15200
|
+
const code = text.charCodeAt(index);
|
|
15201
|
+
if (code < 32 && code !== 10 && code !== 9 || code === 127)
|
|
15202
|
+
return true;
|
|
15203
|
+
}
|
|
15204
|
+
return false;
|
|
15205
|
+
}
|
|
15206
|
+
function headScan(head) {
|
|
15207
|
+
let quote;
|
|
15208
|
+
for (const char of head) {
|
|
15209
|
+
if (quote === "'") {
|
|
15210
|
+
if (char === "'")
|
|
15211
|
+
quote = undefined;
|
|
15212
|
+
continue;
|
|
15213
|
+
}
|
|
15214
|
+
if (quote === '"') {
|
|
15215
|
+
if (char === '"')
|
|
15216
|
+
quote = undefined;
|
|
15217
|
+
else if (REFUSED_DOUBLE_QUOTED.includes(char))
|
|
15218
|
+
return false;
|
|
15219
|
+
continue;
|
|
15220
|
+
}
|
|
15221
|
+
if (char === "'" || char === '"')
|
|
15222
|
+
quote = char;
|
|
15223
|
+
else if (REFUSED_UNQUOTED.includes(char))
|
|
15224
|
+
return false;
|
|
15225
|
+
}
|
|
15226
|
+
return quote === undefined;
|
|
15227
|
+
}
|
|
15228
|
+
|
|
15113
15229
|
// ../pi-shared/src/subagent-session.ts
|
|
15114
15230
|
import fs from "fs";
|
|
15115
15231
|
import path2 from "path";
|
|
@@ -15193,7 +15309,7 @@ function onEnvoyRoleRegained(listener) {
|
|
|
15193
15309
|
// package.json
|
|
15194
15310
|
var package_default = {
|
|
15195
15311
|
name: "@sjawhar/pi-legion",
|
|
15196
|
-
version: "8.
|
|
15312
|
+
version: "8.5.1",
|
|
15197
15313
|
license: "Apache-2.0",
|
|
15198
15314
|
type: "module",
|
|
15199
15315
|
omp: {
|
|
@@ -15205,7 +15321,7 @@ var package_default = {
|
|
|
15205
15321
|
]
|
|
15206
15322
|
},
|
|
15207
15323
|
legion: {
|
|
15208
|
-
daemonApiVersion:
|
|
15324
|
+
daemonApiVersion: 17
|
|
15209
15325
|
},
|
|
15210
15326
|
repository: {
|
|
15211
15327
|
type: "git",
|
|
@@ -15228,7 +15344,7 @@ var package_default = {
|
|
|
15228
15344
|
"@legion/contracts": "workspace:*",
|
|
15229
15345
|
"@legion/envoy-client": "workspace:*",
|
|
15230
15346
|
"@legion/pi-shared": "workspace:*",
|
|
15231
|
-
"@oh-my-pi/pi-utils": "^18.
|
|
15347
|
+
"@oh-my-pi/pi-utils": "^18.6.0",
|
|
15232
15348
|
"@types/bun": "latest",
|
|
15233
15349
|
typescript: "^5.3.0",
|
|
15234
15350
|
zod: "4.3.6"
|
|
@@ -16223,7 +16339,7 @@ function toolSchema(pi) {
|
|
|
16223
16339
|
...HANDOFF_OPERATIONS
|
|
16224
16340
|
]),
|
|
16225
16341
|
issue: z2.string().optional(),
|
|
16226
|
-
artifactId: z2.string().describe("register_gate's root spec document: its artifact id, slug, or filename, as
|
|
16342
|
+
artifactId: z2.string().describe("register_gate's root spec document: its artifact id, slug, or filename, as a dispatch command's --artifact takes it").optional(),
|
|
16227
16343
|
version: z2.number().optional(),
|
|
16228
16344
|
issues: z2.array(z2.string()).optional(),
|
|
16229
16345
|
to: z2.enum(LEGION_PHASE_BACKWARD_TARGETS).optional(),
|
|
@@ -16386,8 +16502,9 @@ function setLegionBootstrapExitForTests(hook) {
|
|
|
16386
16502
|
}
|
|
16387
16503
|
var GH_RESOLVED_URL = /(?:^|[\s;,"])(?:pr|issue):\/\//i;
|
|
16388
16504
|
function needsGrant({ toolName, input }) {
|
|
16389
|
-
if (toolName === "bash")
|
|
16390
|
-
return typeof input.command === "string";
|
|
16505
|
+
if (toolName === "bash") {
|
|
16506
|
+
return typeof input.command === "string" && dispatchCommandHead(input.command) === undefined;
|
|
16507
|
+
}
|
|
16391
16508
|
if (toolName === "github")
|
|
16392
16509
|
return true;
|
|
16393
16510
|
const paths = Array.isArray(input.paths) ? input.paths : [input.path];
|
|
@@ -16637,8 +16754,13 @@ function legionExtension(pi) {
|
|
|
16637
16754
|
});
|
|
16638
16755
|
const titleSession = async (context) => {
|
|
16639
16756
|
const title = legionSessionTitle(classifySession(process.env), process.env.LEGION_PROJECT);
|
|
16640
|
-
if (title
|
|
16641
|
-
|
|
16757
|
+
if (title === undefined)
|
|
16758
|
+
return;
|
|
16759
|
+
await applySessionTitle(pi, context, title);
|
|
16760
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
16761
|
+
if (sessionID === "")
|
|
16762
|
+
return;
|
|
16763
|
+
writeSessionTitle(sessionDirectory(process.env, sessionID), context.sessionManager.getSessionName?.() ?? title);
|
|
16642
16764
|
};
|
|
16643
16765
|
pi.on("session_start", async (_event, context) => {
|
|
16644
16766
|
if (await checkSubagentSession(context))
|
|
@@ -16678,7 +16800,7 @@ function legionExtension(pi) {
|
|
|
16678
16800
|
toolName: toolCall.toolName
|
|
16679
16801
|
});
|
|
16680
16802
|
paneRules ??= PANE_RULES[classifySession(process.env).kind] ?? [];
|
|
16681
|
-
const commands = toolCall.toolName === "bash" && typeof toolCall.input.command === "string" ? splitShellCommands(toolCall.input.command) : undefined;
|
|
16803
|
+
const commands = toolCall.toolName === "bash" && typeof toolCall.input.command === "string" ? splitShellCommands(dispatchCommandHead(toolCall.input.command) ?? toolCall.input.command) : undefined;
|
|
16682
16804
|
const refusal = paneRuleRefusal(toolCall, paneRules, commands);
|
|
16683
16805
|
if (refusal !== undefined)
|
|
16684
16806
|
return { block: true, reason: refusal };
|
|
@@ -23,7 +23,7 @@ separate coordinator to finish necessary work.
|
|
|
23
23
|
product, scope, design, and cross-phase decisions the same way: `envoy_publish` to your own
|
|
24
24
|
encoded token. You decide whether one needs the human and write its decision block yourself
|
|
25
25
|
(section 1 says what one does to an approved root spec); a worker never writes one. Any role may
|
|
26
|
-
use `
|
|
26
|
+
use `dispatch ask` directly for a standalone to-do only a human can complete, and replies return
|
|
27
27
|
to the asking session.
|
|
28
28
|
- The daemon starts each role as its own process with the issue's context already in its
|
|
29
29
|
environment. Never hand-format a role token: the daemon encodes one as
|
|
@@ -33,7 +33,7 @@ separate coordinator to finish necessary work.
|
|
|
33
33
|
the `roleToken` helper from `@legion/contracts` exactly the way the daemon does.
|
|
34
34
|
- There is no label vocabulary. Dispatch status replaces the board, and the design gate
|
|
35
35
|
is a human approving the root spec document at a version in Dispatch, requested with
|
|
36
|
-
`
|
|
36
|
+
`dispatch request-approval` — not a label and not an ask. Never attempt to apply a label.
|
|
37
37
|
- Deferring necessary work is failure. The sole valid deferral is a new child issue you
|
|
38
38
|
create and continue to own. Re-file a genuinely independent child through the
|
|
39
39
|
controller rather than treating it as an abandoned dependency.
|
|
@@ -59,35 +59,32 @@ exercise a criterion end to end, building that path is a child issue of this tre
|
|
|
59
59
|
- **No children:** choose a single-issue tree only when its acceptance criteria can be
|
|
60
60
|
completed and integrated as one unit. Otherwise create complete child issues with:
|
|
61
61
|
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
title: "<child outcome>",
|
|
67
|
-
spec: "<acceptance criteria, scope, and context>"
|
|
68
|
-
})
|
|
62
|
+
```bash
|
|
63
|
+
dispatch issue --project <project> --parent <root issue> --title '<child outcome>' --spec-file - <<'EOF'
|
|
64
|
+
<acceptance criteria, scope, and context>
|
|
65
|
+
EOF
|
|
69
66
|
```
|
|
70
67
|
|
|
71
|
-
|
|
68
|
+
`--project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`) — not
|
|
72
69
|
the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string. A
|
|
73
70
|
root session has `LEGION_TREE == LEGION_ISSUE`. The daemon establishes the sub-issue
|
|
74
|
-
relationship from
|
|
75
|
-
inert until released. Do not pass
|
|
71
|
+
relationship from `--parent`. Keep the returned issue keys in ordered waves; a child is
|
|
72
|
+
inert until released. Do not pass `--assignee`: the default keeps the tree's questions in one
|
|
76
73
|
Inbox — under the shared token a child inherits its parent's assignee (the human who
|
|
77
74
|
answers the tree's asks); under a personal token it goes to that token's owner, whom
|
|
78
|
-
`
|
|
75
|
+
`dispatch whoami` names. Set it only when a human told you a specific person owns that child.
|
|
79
76
|
|
|
80
77
|
Specifications written into Dispatch follow `skill://dispatch`'s [Writing a spec](skill://dispatch/SKILL.md#writing-a-spec).
|
|
81
78
|
Wave releases, child closures, and your own status are visible from the issue tree and the
|
|
82
|
-
handoffs; do not narrate them into the spec or a `
|
|
83
|
-
clear is a `
|
|
79
|
+
handoffs; do not narrate them into the spec or a `dispatch message`. A to-do only a human can
|
|
80
|
+
clear is a `dispatch ask`.
|
|
84
81
|
|
|
85
82
|
The issue's primary document **is** the root specification. Extend it in place: a new version
|
|
86
83
|
that adds only the evidence each decision needs and what the human decides, each as a
|
|
87
84
|
[decision block](skill://dispatch/SKILL.md#decision-blocks). The decomposition and its waves, how each
|
|
88
85
|
outcome is proven, and the integration test are your own calls: they go in the child issues and
|
|
89
86
|
the planner's `.legion/<issue>/plan.json`, not the root spec. Never post a second "spec" artifact beside
|
|
90
|
-
it (`
|
|
87
|
+
it (`dispatch artifact` with the primary document's name replaces the human's document; do not do
|
|
91
88
|
that).
|
|
92
89
|
The design gate's approval step runs only when the `Design gate policy` sentence of your
|
|
93
90
|
`Legion addressing` line says `gates.design: root-issues`. When it says `gates.design: off`, write
|
|
@@ -97,47 +94,46 @@ sub-architect on a child issue has no policy line and never runs the gate: the r
|
|
|
97
94
|
covers the tree. When the gate is armed, run this exact sequence **before the tree's work starts**:
|
|
98
95
|
|
|
99
96
|
```text
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
dispatch doc-edit --issue <root issue> --ops-json-file - <<'EOF' # extend the primary document in place
|
|
98
|
+
...
|
|
99
|
+
EOF
|
|
100
|
+
# then settle its decision blocks (below)
|
|
101
|
+
dispatch request-approval --issue <root issue> --summary '<what the human is approving>'
|
|
102
|
+
# the primary document by default; prints "Approval requested for spec.md (document id <UUID>) at version <N> …"
|
|
106
103
|
legion({
|
|
107
104
|
op: "register_gate",
|
|
108
105
|
issue: "<root issue>",
|
|
109
|
-
artifactId:
|
|
110
|
-
version:
|
|
106
|
+
artifactId: "<UUID>", // the document id from that text, such as 4e0aca36-77b3-43bd-96cf-d58890ae64e4
|
|
107
|
+
version: <N>, // the version number the human is asked to approve
|
|
111
108
|
})
|
|
112
109
|
```
|
|
113
110
|
|
|
114
111
|
The decision blocks come first: settle every one as
|
|
115
112
|
[Approval of a spec](skill://dispatch/SKILL.md#approval-of-a-spec) says before you request approval;
|
|
116
|
-
`
|
|
113
|
+
`dispatch request-approval` refuses while one is open. Each answer reaches you, since you follow
|
|
117
114
|
every ask you open. An approval request carries nothing new: request it only once the human has
|
|
118
115
|
agreed to every point in the spec, so a point they have not agreed to gets its own decision block
|
|
119
|
-
first, or comes out of the spec.
|
|
116
|
+
first, or comes out of the spec. `--summary` says in one to three sentences what the human is
|
|
120
117
|
approving and nothing else: no commentary and no open question.
|
|
121
118
|
|
|
122
|
-
`
|
|
119
|
+
`dispatch request-approval` opens a system question on the document with the fixed options
|
|
123
120
|
`Approve` and `Request changes`; a human answers it from the Inbox or approves from the
|
|
124
|
-
document's own header. Never open a `
|
|
121
|
+
document's own header. Never open a `dispatch ask` with an `Approve` option yourself: an
|
|
125
122
|
ordinary question is not a gate and the daemon ignores its answer. Copy `artifactId` and
|
|
126
|
-
`version` from the
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
document id is never the slug or file name you passed in (`spec`, `spec.md`): the daemon
|
|
123
|
+
`version` from the text `dispatch request-approval` prints: "Approval requested for spec.md
|
|
124
|
+
(document id <UUID>) at version <N> (ask <id>)", followed by the question the human's Inbox shows.
|
|
125
|
+
The document id is never the slug or file name you passed in (`spec`, `spec.md`): the daemon
|
|
130
126
|
recognizes the document's approval events by that id and takes no other value; the `legion` tool
|
|
131
|
-
looks a slug or file name up in Dispatch and hands the daemon the id. Calling `
|
|
132
|
-
waits on the human, with the same
|
|
133
|
-
it "already waits on the human"), so it is safe to repeat; a different
|
|
127
|
+
looks a slug or file name up in Dispatch and hands the daemon the id. Calling `dispatch request-approval` again while that request
|
|
128
|
+
waits on the human, with the same `--summary`, changes nothing and returns it (its text says
|
|
129
|
+
it "already waits on the human"), so it is safe to repeat; a different `--summary` is refused then.
|
|
134
130
|
A newer version of the spec moves the open request to that version and leaves it waiting on you,
|
|
135
131
|
with no wake when the edit was yours: once the human has agreed to every point in it, call again
|
|
136
132
|
to hand the same request back at the latest version. If its text instead reads "spec.md (document
|
|
137
133
|
id <UUID>) is already approved at version <N>" — a human approved from the document header before
|
|
138
134
|
you asked — still call `register_gate` with that id and version: the daemon reads the approval
|
|
139
135
|
from Dispatch as it registers, opens the gate, and delivers `design-approved` at once. The same
|
|
140
|
-
read covers a human who answers the question between your `
|
|
136
|
+
read covers a human who answers the question between your `dispatch request-approval` and
|
|
141
137
|
`register_gate` calls, so an approval is never lost to timing; you never approve anything
|
|
142
138
|
yourself.
|
|
143
139
|
|
|
@@ -196,7 +192,7 @@ The daemon moves each released child to `todo` and, while the root's design gate
|
|
|
196
192
|
its phases itself from its fixed workflow table, each phase worker as its own process with the
|
|
197
193
|
child's context already in its environment; it starts no sub-architect, and you start no worker.
|
|
198
194
|
Park while children are in flight. On each child closure, re-scope open work, take obsolete work
|
|
199
|
-
out of the workflow with `park_child` (saying why on the issue with `
|
|
195
|
+
out of the workflow with `park_child` (saying why on the issue with `dispatch comment`), and
|
|
200
196
|
release the next wave only when it now makes sense. There is no inter-child
|
|
201
197
|
dependency mechanism to encode.
|
|
202
198
|
|
|
@@ -238,12 +234,12 @@ the task.
|
|
|
238
234
|
Wait for the implementer to report its durable retro result. Retro output is
|
|
239
235
|
`docs/solutions/`, its last commit removing the issue's `.legion/<issue>/` from the head a human
|
|
240
236
|
merges, the PR body content the repository's instructions derive from the pull request's changed
|
|
241
|
-
paths at that head, and one `
|
|
237
|
+
paths at that head, and one `dispatch message` on the issue; it must write no handoff or
|
|
242
238
|
rewrite the reviewer-approved head. When the implementer reports that it cannot
|
|
243
239
|
compute that body content or do the work a line of it affirms, that its read of the body failed or
|
|
244
240
|
came back empty, or that GitHub refused the body, it has pushed nothing and is still in its phase.
|
|
245
241
|
Answer it with `envoy_publish` to its role topic, saying how from the deployment instructions;
|
|
246
|
-
when they do not say, open a `
|
|
242
|
+
when they do not say, open a `dispatch ask` naming the missing instruction.
|
|
247
243
|
|
|
248
244
|
## 6. Architect sign-off and merge
|
|
249
245
|
|
|
@@ -276,7 +272,7 @@ The daemon keeps this order from its fixed table; you start none of its steps:
|
|
|
276
272
|
It drives the changed path in production through the user's own access path and records
|
|
277
273
|
what it saw on the pull request and on this issue. Sign off only after the implementer's production
|
|
278
274
|
report exists. A defect it finds is a corrective child issue of this tree, not a note on a
|
|
279
|
-
closed one; if the implementer cannot perform the deploy, it opens a `
|
|
275
|
+
closed one; if the implementer cannot perform the deploy, it opens a `dispatch ask` that starts
|
|
280
276
|
with the production gap and why it matters, then names the required step, its risk, and
|
|
281
277
|
outcome-named options. The issue waits for that answer.
|
|
282
278
|
|
|
@@ -299,12 +295,12 @@ This merge happens only when GitHub reports `CONFLICTING`
|
|
|
299
295
|
wake — `phase-finished`, `catch-up`, `checks-red`, `review-stuck` — because a `CONFLICTING`
|
|
300
296
|
PR gets no CI and no wake announces it. The moment you see it, tell the worker holding the issue's
|
|
301
297
|
phase (`envoy_publish` to its role topic) to move the issue back to `implementing` with
|
|
302
|
-
`request_backward_move`; in `awaiting_merge`, where no worker holds a phase, open a `
|
|
298
|
+
`request_backward_move`; in `awaiting_merge`, where no worker holds a phase, open a `dispatch ask`
|
|
303
299
|
naming the conflict for the human who merges. Do not let the merger send a READY packet for an
|
|
304
300
|
obsolete approval.
|
|
305
301
|
|
|
306
302
|
If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
|
|
307
|
-
to resolve, open a `
|
|
303
|
+
to resolve, open a `dispatch ask` that names the thread's URL and GitHub's message for a human to
|
|
308
304
|
resolve it by hand, with options for resolved / could not; the merger does not complete while it
|
|
309
305
|
is open. That is the one review-thread step a human takes: the review App cannot resolve a thread
|
|
310
306
|
on a pull request the implementer opened, and the implementer's and merger's runs of the command
|
|
@@ -323,7 +319,9 @@ After the merge result and the implementer's production report are recorded, pos
|
|
|
323
319
|
close this issue with `sign_off`, which writes `done`:
|
|
324
320
|
|
|
325
321
|
```text
|
|
326
|
-
|
|
322
|
+
dispatch comment --issue LEGION-40 --body-file - <<'EOF'
|
|
323
|
+
<sign-off: scope, integration evidence, review, retro, merge, and the implementer's production report>
|
|
324
|
+
EOF
|
|
327
325
|
legion({ op: "sign_off", issue: "LEGION-40" })
|
|
328
326
|
```
|
|
329
327
|
|
|
@@ -346,10 +344,10 @@ active phase worker.
|
|
|
346
344
|
| `design-approved` | Its `version` is the approved one. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
|
|
347
345
|
| `design-changes-requested` | Its `version` and `reason`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec as the reason asks and request approval again as section 1 says; stay parked; the gate is closed. |
|
|
348
346
|
| `phase-finished` | The daemon has already moved the issue to its next phase by its fixed table and started that phase's role; you start nothing. Read the committed handoff for the finishing phase; if it shows unresolved gaps, tell the role now working the issue (`envoy_publish` to its role topic). A `planner` notice that names a departure from the spec's design defers to section 1's full condition: when the approved Summary, Acceptance, scope and settled decisions still hold, the plan is the record; otherwise change the root spec and request approval again as section 1 says. A `reviewer` notice whose GitHub review is `CHANGES_REQUESTED` needs nothing from you: the daemon has returned the issue to `implementing` (Dispatch `in_progress`) and started the **implementer**, whose correction goes through the tester and the reviewer again, never straight to retro. A `reviewer` notice with an `APPROVED` review means the daemon has started retro (step 5). An `implementer` notice for `production_check` is its production report: read the record on the pull request and the issue, then run step 7. A `tester` notice with `verdict: "fail"` — its handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof — has gone back to the **implementer** by the daemon's table; never supply the proof from another role. A worker that reports no surface reaches the changed path sends that report instead of completing its phase, so the daemon starts nothing more on that issue and the worker stays idle in its session, not suspended: file a child issue in this tree to build the surface (infrastructure, tooling, or a skill), and when that child's `child-closed` arrives, tell the waiting worker to continue with `envoy_publish` to its role topic. That report is never a reason to advance the phase. |
|
|
349
|
-
| `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. The notice moves nothing: the issue stays in its phase. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `
|
|
347
|
+
| `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. The notice moves nothing: the issue stays in its phase. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch ask` naming the failing checks for the human who merges, as section 6 does for a conflict there. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names either a conflict (`the head conflicts with <base>`) or the required checks that failed (`CI is red at <head>`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed) are the work going in circles: narrow it, re-plan it with the responsible worker, or ask a human with `dispatch ask`. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` (section 6) is a conflict return. The merger's move back to `reviewing` because its task names no approved head is not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
|
|
350
348
|
| `pr-merged` | Its `reason` names the pull request. The PR merged, under the repository's rules, before the issue reached `awaiting_merge`. The workflow runs on and asks no one to merge it: the daemon starts the **implementer** on the production check once the issue gets there, and you start nothing. Its `phase-finished` for `production_check` is what brings you to step 7: verify the record on the pull request and this issue first, then sign off naming it with `sign_off`. A merge is not the close. |
|
|
351
349
|
| `pr-closed-unmerged` | Decide from current scope whether the work is reopened, started over (`park_child` then `rerun_child`, for a child), or ended with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
|
|
352
|
-
| A reply on an ask you follow | Interpret the reply in the issue's design context. Answer in its thread (`
|
|
350
|
+
| A reply on an ask you follow | Interpret the reply in the issue's design context. Answer in its thread (`dispatch comment --reply-to <comment id>`; under an open ask whose next move is yours, such as the approval request you must revise or hand back, `--reply-to-ask <ask id> --turn agent`, since a default-turn reply hands that request back to the human and a corrected `--summary` is then refused), then adjust the plan or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
|
|
353
351
|
| `worker-died` | Its `role` and `phase`. That role's claim failed: its launches or prompts ran out. For a phase worker the daemon holds the issue (phase `held`) and starts nothing more on it. Reassess the work, then decide with `retry_or_escalate`: `retry` when the failure looks agent-specific or transient, `escalate` to hand the held issue to the controller when it looks environmental. |
|
|
354
352
|
| `held` | Its `role` and `phase` (the phase the issue left), or `phase` with `reason: "escalated"`. Without `reason`, that phase's worker ran out of launches or prompts: the issue is held (phase `held`), the `worker-died` that comes with it is yours to answer with `retry_or_escalate` as that row says, and the controller hears of the hold too. With `reason: "escalated"`, it records your own `escalate`: the controller has the issue now, and you start nothing for it. |
|
|
355
353
|
| `ready-refused` | Its `version` and `reason`. The merger's READY was refused, and its packet is kept with the completion so no second READY is ever needed. With `version` greater than 0, `reason` reads `READY refused: approve design version <N> before requesting READY.`: the root's spec is already registered at that version, so get it approved (section 1), and the daemon advances the merge and posts the kept packet the moment it is. With `version` 0, `reason` reads `READY refused: no design version is approved; register and approve the tree's spec before requesting READY.`: the root has no spec registered yet, so register it and get it approved (section 1) exactly as above; the daemon releases the kept packet the same way — at registration itself when that opens the gate (`gates.design: off`, or Dispatch already shows the version approved), otherwise the moment a human approves the version you registered. Either way, nothing else is needed once the gate opens. A `reason` starting `READY_PACKET_MISSING` names a READY refused before the daemon kept packets: that READY is void and the issue stays in merging, so start it over (`park_child` then `rerun_child`, for a child) or end it. |
|
|
@@ -361,14 +359,14 @@ cross-tree conflict. Report those to the controller with `envoy_publish` to the
|
|
|
361
359
|
your `Legion addressing` line names. Handle everything else in the
|
|
362
360
|
tree. A product, scope, or design decision that needs the human, yours or one a worker escalated,
|
|
363
361
|
is a decision block you write (section 1 says what one does to the root spec's gate). A standalone
|
|
364
|
-
human to-do may use `
|
|
362
|
+
human to-do may use `dispatch ask`; workers may reach the human directly with it the same way. Do not
|
|
365
363
|
create a wait loop for any wake source.
|
|
366
364
|
|
|
367
365
|
Never yield while waiting on a human. A human is waiting on you only where an open ask sits in
|
|
368
|
-
their inbox, so open it before you stop: a decision block in the spec, `
|
|
369
|
-
standalone human to-do, or `
|
|
366
|
+
their inbox, so open it before you stop: a decision block in the spec, `dispatch ask` for a
|
|
367
|
+
standalone human to-do, or `dispatch request-approval` for the spec gate. Otherwise proceed:
|
|
370
368
|
proceeding is the default, and a stop that waits on nobody stalls the tree until someone notices.
|
|
371
369
|
|
|
372
370
|
## Architecture components
|
|
373
371
|
|
|
374
|
-
Bootstrap on a root whose project has an architecture source: in the root's first PR (the implement worker pushes it), write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`, `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head, so the sync and the attach below work only once that PR has merged to the source branch: until then leave the root on `inherit` (the attach would answer `400 COMPONENTS_INPUT`, the component does not exist yet). After the merge, `
|
|
372
|
+
Bootstrap on a root whose project has an architecture source: in the root's first PR (the implement worker pushes it), write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`, `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head, so the sync and the attach below work only once that PR has merged to the source branch: until then leave the root on `inherit` (the attach would answer `400 COMPONENTS_INPUT`, the component does not exist yet). After the merge, run `dispatch architecture-sync --project <KEY>` and attach the root with `dispatch issue-update --issue <root> --components-json '{"mode":"explicit","ids":[…]}'`. Children inherit the root's attachment; give a child its own `--components-json` only when it changes a narrower set, and `{"mode":"none","reason":"…"}` when it is not architectural work. Attach before decomposing, and require every implementer to change the component file beside the code it describes in the same review.
|
|
@@ -108,9 +108,9 @@ else:
|
|
|
108
108
|
as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
|
|
109
109
|
nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
|
|
110
110
|
2. List the project's triage issues handed to Legion with
|
|
111
|
-
`
|
|
112
|
-
When its first line ends `(showing 1-250 of N)`, read the next page with
|
|
113
|
-
on until you have all N rows. The rows show no parent, so open each row with `
|
|
111
|
+
`dispatch issues --project <PROJECT> --status triage --label legion --limit 250 --offset 0`.
|
|
112
|
+
When its first line ends `(showing 1-250 of N)`, read the next page with `--offset 250`, and so
|
|
113
|
+
on until you have all N rows. The rows show no parent, so open each row with `dispatch read`
|
|
114
114
|
and follow `Links:` up through each `child_of` parent (a `child_of` under `Referenced by:` is a
|
|
115
115
|
child of this issue, not its parent). Leave a child that has an ancestor in `admission.active`
|
|
116
116
|
or `admission.waiting`: that tree's architect owns it. Triage every other row as a root,
|
|
@@ -137,8 +137,8 @@ only when you take it or file it for Legion, so the label means Legion has the i
|
|
|
137
137
|
A person's label is their decision: never take it off. A child needs no label: it runs under its
|
|
138
138
|
tree's architect once its root is admitted. `legion status <KEY> todo` admits a root, or a child
|
|
139
139
|
outside a live tree (admitted as a root of its own), only while it carries the label, so an issue
|
|
140
|
-
you hand over or file for Legion to run carries it first (`
|
|
141
|
-
`
|
|
140
|
+
you hand over or file for Legion to run carries it first (`--label` on `dispatch issue-update` or
|
|
141
|
+
`dispatch issue`). Taking the label off a waiting root drops it from the waiting line; taking it
|
|
142
142
|
off an admitted tree does not stop it.
|
|
143
143
|
|
|
144
144
|
## Trees waiting on a root claim (Go daemon)
|
|
@@ -147,7 +147,7 @@ A Go root architect whose claim on its root issue was refused starts nothing and
|
|
|
147
147
|
its slot, until the claim is free; nothing tells it when a session holder lets go without
|
|
148
148
|
replying. So every turn rechecks them ([Turn discipline](#turn-discipline)), the daemon's `tick`
|
|
149
149
|
included, which comes on its interval even with every slot taken: read each root in
|
|
150
|
-
`admission.active` whose `issues.<KEY>.phase` is still `admitted` with `
|
|
150
|
+
`admission.active` whose `issues.<KEY>.phase` is still `admitted` with `dispatch read`. When its
|
|
151
151
|
`Claimed by:` line is `nobody` or ends `· not running`, tell that tree's architect to claim again
|
|
152
152
|
with `envoy_publish` to `notifications.role.` followed by its claim token,
|
|
153
153
|
`issues.<KEY>.architect.locator.claim` in `legion state --json`. A claim that is its architect's
|
|
@@ -199,16 +199,16 @@ only labelled roots, so every root it ran since the daemon required the label ca
|
|
|
199
199
|
labelled root in `todo` is the daemon's to admit or queue, one in `triage` is yours to triage
|
|
200
200
|
(step 2 above), and one anywhere else was parked by Legion or by a person. A child you take becomes
|
|
201
201
|
a root of its own: the daemon admits a labelled `todo` child whose tree Legion does not run as a
|
|
202
|
-
new tree. The `
|
|
202
|
+
new tree. The `dispatch issues` rows show no labels and no children, so the listing below filters
|
|
203
203
|
neither: both are checked on each candidate (the table's first rows). List the `todo` issues one
|
|
204
|
-
priority at a time,
|
|
204
|
+
priority at a time, `--priority 0` first, then `1`, `2`, `3`, and `none` (no priority)
|
|
205
205
|
last, keeping the listing's order within each, which is the board's rank:
|
|
206
206
|
|
|
207
207
|
```text
|
|
208
|
-
|
|
208
|
+
dispatch issues --project <PROJECT> --status todo --priority 0 --limit 250 --offset 0
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
-
When the first line ends `(showing 1-250 of N)`, the next page is
|
|
211
|
+
When the first line ends `(showing 1-250 of N)`, the next page is `--offset 250`, then `500`. Read
|
|
212
212
|
pages only as far as you need: stop listing once the free slots are filled. `<PROJECT>` is the
|
|
213
213
|
Dispatch project key, the prefix of this deployment's issue keys (`PROJ-12` → `PROJ`), which is
|
|
214
214
|
also `daemon.project` in `legion state --json`: the project key exactly as `legion.yaml` writes it.
|
|
@@ -217,14 +217,14 @@ the row shows one, comes after the claim) is claimed, as the table below says: s
|
|
|
217
217
|
reading it.
|
|
218
218
|
|
|
219
219
|
**Walk.** Take the remaining rows in that order until the free slots are filled. Read each one with
|
|
220
|
-
`
|
|
220
|
+
`dispatch read --issue <KEY>` and skip it when any of these holds. `dispatch read` shows only
|
|
221
221
|
the issue's last 10 events, so the rows that read `Events:` are best-effort: the pull-request row
|
|
222
222
|
leans on `External links:`, and the label row is the one that never depends on history.
|
|
223
223
|
|
|
224
224
|
| Skip when | How you check it |
|
|
225
225
|
|---|---|
|
|
226
226
|
| It carries the `legion` label | `Labels:` lists `legion`, in any case. Legion has the issue or had it, as **Candidates** above says. |
|
|
227
|
-
| It has any child | `
|
|
227
|
+
| It has any child | `dispatch read --ref dispatch://<KEY>/children` lists any child, open or `done`. It is an umbrella, and a finished umbrella is still no leaf. That also skips an issue whose only child is done, which is accepted. Its open children are candidates themselves, each in its own place in the order. |
|
|
228
228
|
| An ancestor is Legion's | Follow `Links:` up through each `child_of` parent, reading each one, and skip when any ancestor carries the `legion` label or is recorded under `issues` in `legion state --json`, whatever its status: a Legion tree, running or parked, owns its children. An ancestor's claim or route does not skip the issue: a coordinator holding an umbrella files `todo` leaves for others to pick up, and the claim on the issue itself is what keeps two sessions off the same work. A `child_of` under `Referenced by:` is a child of this issue, not its parent. |
|
|
229
229
|
| Someone is designing it | `Open asks:` lists any ask, a `Spec approval: awaiting …` line shows the spec waits on a human, or `Events:` show an `artifact.version` or an `ask.opened` from the last seven days: a session or a person is shaping it even when nobody claims or routes it. |
|
|
230
230
|
| Legion ran it without the label now on it | `legion state --json` records it under `issues`, whatever its status, or `Events:` show a status write by `session legion-daemon:<PROJECT>`, the daemon's actor on every `legion status` (yours included) and on its own `in_progress` at admission. That covers a root a person took the label off, and one that ran before the daemon required the label and never had it. Name each one you skip for this in your summary. The walk never sends a root Legion already ran back into Legion: a person does that with the label and `todo`, and you do it only when a wake below says to (`worker-died`). |
|
|
@@ -232,15 +232,15 @@ leans on `External links:`, and the label row is the one that never depends on h
|
|
|
232
232
|
| Its route reaches a running session | `Route:` names a route with nothing after it, or with `(held by …)`. `(nobody holds it right now)` and `(that session is not running right now)` reach nobody; `(the Envoy listener did not answer, …)` counts as reaching someone. `Route: none` is free. |
|
|
233
233
|
| A pull request is linked or named | `External links:` lists a pull request (kind `github_pr`, or a URL ending `/pull/<n>`), or a comment or message among `Events:` names one. You cannot read GitHub, so an open, merged, or closed pull request all count. A person who wants Legion on it anyway hands it over themselves: the label, then `todo`. |
|
|
234
234
|
| Its assignee is working it | `Assignee:` names a person who holds the claim (the row above), or whose own comment or message among `Events:` says they are working on it. The assignee alone is who answers the issue's questions, not who works it. |
|
|
235
|
-
| It is outside this deployment's scope | Read the scope the deployment instructions state against the title and, when the title does not settle it, the spec (`
|
|
235
|
+
| It is outside this deployment's scope | Read the scope the deployment instructions state against the title and, when the title does not settle it, the spec (`dispatch doc-read --issue <KEY>`). When in doubt, skip it. With no scope stated, every issue of the project is in scope. |
|
|
236
236
|
|
|
237
237
|
**Take.** For each candidate that passes, in order:
|
|
238
238
|
|
|
239
|
-
1. Add the label and keep the labels it has, which `Labels:` lists (`none` is no labels).
|
|
240
|
-
|
|
239
|
+
1. Add the label and keep the labels it has, which `Labels:` lists (`none` is no labels). The
|
|
240
|
+
`--label` flags replace the whole set, so a label you leave out is removed.
|
|
241
241
|
|
|
242
242
|
```text
|
|
243
|
-
|
|
243
|
+
dispatch issue-update --issue <KEY> --label <each current label> --label legion
|
|
244
244
|
```
|
|
245
245
|
|
|
246
246
|
2. It is already in `todo`, so the label admits it: the daemon records it and gives it the free
|
|
@@ -250,7 +250,9 @@ leans on `External links:`, and the label row is the one that never depends on h
|
|
|
250
250
|
step 4 gives (which follows the design gate policy):
|
|
251
251
|
|
|
252
252
|
```text
|
|
253
|
-
|
|
253
|
+
dispatch comment --issue <KEY> --body-file - <<'EOF'
|
|
254
|
+
Legion took this issue: it was the highest-priority open issue nobody else was working on. Assigned to <login>, who will get this tree's questions and its design approval. To stop Legion, move the issue to backlog. To keep Legion off it for good, also take the legion label off; taking the label off alone does not stop a tree that has started.
|
|
255
|
+
EOF
|
|
254
256
|
```
|
|
255
257
|
|
|
256
258
|
The daemon admits each root when Dispatch's event reaches it; the next `legion state --json`
|
|
@@ -264,28 +266,32 @@ change and why, and what is running.
|
|
|
264
266
|
|
|
265
267
|
**Where.** On the issue the deployment instructions name for Legion's reports. With none named,
|
|
266
268
|
on the project's `Legion daily report` issue:
|
|
267
|
-
`
|
|
269
|
+
`dispatch search --query '"Legion daily report"' --project <PROJECT>` finds it. When it
|
|
268
270
|
does not exist, create it once and park it in `icebox`, so nobody takes it as work; it never
|
|
269
271
|
carries the `legion` label:
|
|
270
272
|
|
|
271
273
|
```text
|
|
272
|
-
|
|
274
|
+
dispatch issue --project <PROJECT> --title 'Legion daily report' --spec-file - <<'EOF'
|
|
275
|
+
## Summary
|
|
276
|
+
|
|
277
|
+
Legion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. This issue is not work, so it carries no `legion` label and stays in icebox.
|
|
278
|
+
EOF
|
|
273
279
|
legion status <report KEY> icebox
|
|
274
280
|
```
|
|
275
281
|
|
|
276
282
|
**When.** On your first turn of each UTC day, whatever it is: your start, or a wake of any kind.
|
|
277
283
|
While you are registered, the daemon's `tick` gives you a turn at least every
|
|
278
284
|
`controller_wake_interval_seconds`. After the turn's own work, read the report issue with
|
|
279
|
-
`
|
|
285
|
+
`dispatch read`, and post when its `Events:` show no `message.created` from today.
|
|
280
286
|
|
|
281
|
-
**What.** One `
|
|
287
|
+
**What.** One `dispatch message --issue <report KEY> --body-file -` of at most 2,000 characters,
|
|
282
288
|
written as `skill://dispatch`'s "Writing for the human" says: every issue by its key and title,
|
|
283
289
|
every pull request by its URL.
|
|
284
290
|
|
|
285
|
-
- **Finished.** `
|
|
286
|
-
previous report's time, or 24 hours ago>
|
|
291
|
+
- **Finished.** `dispatch issues --project <PROJECT> --status done --updated-since <the
|
|
292
|
+
previous report's time, or 24 hours ago> --limit 250`, read every page, and keep the issues
|
|
287
293
|
`legion state --json` records under `issues` whose `issue.closed` event is after the previous
|
|
288
|
-
report. For each, `
|
|
294
|
+
report. For each, `dispatch read` it: the pull request under `External links:` is the one that
|
|
289
295
|
merged.
|
|
290
296
|
- **Closed without a change.** Those with no pull request, each with the reason its closing
|
|
291
297
|
message gave (the `message.created` just before `issue.closed` among `Events:`).
|
|
@@ -326,7 +332,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
|
|
|
326
332
|
relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
|
|
327
333
|
- **Controller state is disposable.** Do not reconstruct or preserve local controller
|
|
328
334
|
bookkeeping between turns.
|
|
329
|
-
- **Write for a human.** Every `
|
|
335
|
+
- **Write for a human.** Every `dispatch comment`, `dispatch message`, and `dispatch ask` you
|
|
330
336
|
post follows `skill://dispatch`'s "Writing for the human" rules: plain sentences, every
|
|
331
337
|
identifier expanded on first use, no coined shorthand. A triage note that reads like a log
|
|
332
338
|
line is not a triage note.
|
|
@@ -339,7 +345,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
|
|
|
339
345
|
| `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `legion state --json`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
340
346
|
| `todo on <KEY>` from the Go daemon (payload `{kind: "todo"}`) | an issue not handed to Legion that changed while in `todo` and a slot stood free, sent half a minute later | Verify a free slot, then walk the whole `todo` list ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
341
347
|
| `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `legion state --json` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides it) |
|
|
342
|
-
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `
|
|
348
|
+
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `dispatch ask` only for a human to-do |
|
|
343
349
|
| Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
|
|
344
350
|
| `held on <KEY>` from the Go daemon (payload `{kind: "held", phase, role?, reason?}`) | the held issue, the phase it left, and the role whose claim failed, or `reason: "escalated"` | Verify the hold in `legion state` (the issue's phase is `held`). Without `reason`, a phase worker's launches or prompts ran out and the tree's architect decides retry or escalate: no action. With `reason: "escalated"` (on the record, `issues.<KEY>.holdReason` is `escalated`), the architect sent it to you: handle it as an architect escalation below. Parking the tree is `legion status <root> backlog`; setting the root back to `todo` later re-admits it as a new generation, which starts again from its architect |
|
|
345
351
|
| `worker-died on <KEY>` from the Go daemon with `role: "architect"` | the tree root whose architect's claim failed, and the phase the root was in | The tree's architect ran out of launches or prompts and the daemon relaunches nothing; every other notice of the tree goes to that architect, so nobody inside the tree can act. Verify in `legion state` (`issues.<KEY>.architect.state` is `failed`); if the root's phase is `done`, the tree is already parked: no action. Otherwise re-admit the tree (`legion status <root> backlog`, then `todo`: a new generation, whose architect starts again with fresh budgets) or leave it parked and say why on the issue |
|
|
@@ -347,7 +353,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
|
|
|
347
353
|
|
|
348
354
|
## New issue triage
|
|
349
355
|
|
|
350
|
-
1. Read `legion state --json`, then inspect the reported Dispatch issue with `
|
|
356
|
+
1. Read `legion state --json`, then inspect the reported Dispatch issue with `dispatch read`.
|
|
351
357
|
Verify the issue is in this project, is eligible for a root process, and whether it
|
|
352
358
|
has pre-existing children. Dispatch and daemon state, not the wake text, decide triage.
|
|
353
359
|
Note the `Assignee:` line: that human answers the tree's asks, and their Inbox opens on
|
|
@@ -371,7 +377,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
|
|
|
371
377
|
place. A root never waits for capacity in `backlog`: in `todo` the
|
|
372
378
|
daemon's admission queue holds it (`admission.waiting`), so park only a root that should not
|
|
373
379
|
run now. Do not triage a system-created child as a root issue.
|
|
374
|
-
4. When you post a triage note (a `
|
|
380
|
+
4. When you post a triage note (a `dispatch comment` on the issue saying what you decided and
|
|
375
381
|
why), name who will be asked with the assignee sentence: `Assigned to <login>, who will get
|
|
376
382
|
this tree's questions and its design approval`, or, when the `Assignee:` line says
|
|
377
383
|
`unassigned`, `Unassigned — nobody's Inbox shows this tree's questions or its design approval
|
|
@@ -396,14 +402,14 @@ or when a wake tells you to re-admit it (`worker-died`).
|
|
|
396
402
|
|
|
397
403
|
Only decide controller-actionable escalations: re-filing independent work, capacity, and
|
|
398
404
|
cross-tree conflicts. The owning architect writes an issue-design decision as a decision block and
|
|
399
|
-
uses `
|
|
405
|
+
uses `dispatch ask` only for a human to-do, not the controller.
|
|
400
406
|
|
|
401
407
|
For an independence judgment, verify the child and its parent against current daemon state
|
|
402
408
|
and the Dispatch issue. If the work belongs in an independent root:
|
|
403
409
|
|
|
404
|
-
1. File a **fresh root issue** with `
|
|
405
|
-
add
|
|
406
|
-
|
|
410
|
+
1. File a **fresh root issue** with `dispatch issue --project <KEY> --title '<title>' --spec-file -`
|
|
411
|
+
(no `--parent`; add `--label legion`, without which it is never admitted).
|
|
412
|
+
`--project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`): the
|
|
407
413
|
project key exactly as `legion.yaml` writes it, which `legion state --json` shows as
|
|
408
414
|
`daemon.project`. It is not the lowercase project token in role names such as
|
|
409
415
|
`legion-<project>-controller`.
|
|
@@ -36,7 +36,7 @@ retrospective's durable output.
|
|
|
36
36
|
and records it on the PR and the issue: the agent that developed it is responsible for testing
|
|
37
37
|
in production. The architect's sign-off waits for that record.
|
|
38
38
|
The record is the pull request's `Production:` line, one pull-request comment, and a
|
|
39
|
-
`
|
|
39
|
+
`dispatch message` on the issue, each naming what was driven, how, what was observed, and the
|
|
40
40
|
merge commit. A defect the production check finds becomes a corrective child issue of the same tree,
|
|
41
41
|
owned by the architect and implemented by the same implementer; the parent stays open until it lands.
|
|
42
42
|
|
|
@@ -157,16 +157,15 @@ Then push both commits with one `legion push`, so one CI run covers them. When t
|
|
|
157
157
|
refused after step 4 wrote the body, write `<dir>/before.md` back the same way before you report
|
|
158
158
|
the refusal to the architect, so the body matches the head GitHub has. After the push, post one
|
|
159
159
|
Dispatch message on
|
|
160
|
-
the issue —
|
|
160
|
+
the issue — `--issue` is your `LEGION_ISSUE`; Legion issues live on Dispatch, never on a GitHub
|
|
161
161
|
issue, and the `gh` shim refuses every GitHub-issue write — naming the documents, the
|
|
162
162
|
one-to-three most useful takeaways, the two proofs you read, and the production check that
|
|
163
163
|
follows the merge. The message must carry this revived implementer's structured
|
|
164
164
|
attribution footer with `phase` set to `retro`; the body is capped at 2,000 characters:
|
|
165
165
|
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
body: `## Retro Complete
|
|
166
|
+
```bash
|
|
167
|
+
dispatch message --issue <KEY> --body-file - <<'EOF'
|
|
168
|
+
## Retro Complete
|
|
170
169
|
|
|
171
170
|
**Learnings documented in:**
|
|
172
171
|
- docs/solutions/<path>.md
|
|
@@ -178,8 +177,8 @@ dispatch_message({
|
|
|
178
177
|
|
|
179
178
|
**Production check:** <what the implementer will drive after the merge, or the deploy/restart step a human will have to perform first>
|
|
180
179
|
|
|
181
|
-
<!-- legion: {"session":"<session-id>","phase":"retro"}
|
|
182
|
-
|
|
180
|
+
<!-- legion: {"session":"<session-id>","phase":"retro"} -->
|
|
181
|
+
EOF
|
|
183
182
|
```
|
|
184
183
|
|
|
185
184
|
The `docs/solutions/` commit, the removal of `.legion/<issue>/`, the PR body lines those commits
|
|
@@ -59,14 +59,18 @@ only on this phase's artifact.
|
|
|
59
59
|
|
|
60
60
|
You never start another Legion role: the daemon starts every phase worker itself, from its fixed
|
|
61
61
|
workflow table. You may still use ordinary `task` subagents for your own phase work; none of them
|
|
62
|
-
is a Legion role.
|
|
62
|
+
is a Legion role. A subagent runs in your pane as you, with every host tool but the `legion` tool
|
|
63
|
+
(it is never offered one), so a `legion` command from its bash, a completion included, is yours at
|
|
64
|
+
the daemon: give a subagent no completion step. Today such a command runs on the per-call grant
|
|
65
|
+
your own last credentialed call wrote, within its 60 seconds (*Four facts about `gh`*, below);
|
|
66
|
+
LEGION-631 (dispatch://LEGION-631) replaces that grant with the role's mounted token file.
|
|
63
67
|
Escalate a product, scope, design, cross-phase, or lifecycle decision to the owning architect with
|
|
64
68
|
`envoy_publish` to its role topic (`notifications.role.` followed by its encoded token, see
|
|
65
69
|
above), carrying the verified facts and the decision needed. A `write` to `agent://` only reaches
|
|
66
70
|
agents inside your own process, not the architect's separate one. Never write a decision block
|
|
67
71
|
into a spec yourself: the architect decides whether the human must answer it and writes the block,
|
|
68
72
|
since a new version of an approved root spec closes the tree's design gate. A standalone to-do
|
|
69
|
-
only a human can do is a `
|
|
73
|
+
only a human can do is a `dispatch ask`, and its replies return to your own session.
|
|
70
74
|
|
|
71
75
|
Because the same agent works its role until the issue closes, you may receive more than one
|
|
72
76
|
assignment across your lifetime: your session stays live after your phase ends, and when a later
|
|
@@ -228,7 +232,7 @@ subcommand's `comment`, `create`, `edit`, `close`, `reopen`, `delete`, `pin`, `u
|
|
|
228
232
|
`lock`, `unlock`, and `develop`, and any raw `gh api` call to an `/issues` path whose method is not
|
|
229
233
|
GET (an explicit `-X`, or the POST that `-f`/`-F`/`--input` imply; pull-request conversation
|
|
230
234
|
comments live on that path too, so edit them with `gh pr comment`) — printing
|
|
231
|
-
|
|
235
|
+
``Legion issues live on Dispatch; use `dispatch message` or `dispatch comment` on <your LEGION_ISSUE>``:
|
|
232
236
|
Legion never reads or writes a GitHub issue. `pr comment`, `pr review`,
|
|
233
237
|
`api …/pulls/…`, `api graphql`, and issue reads are unaffected. The credential reaches `legion`
|
|
234
238
|
through the file `$LEGION_GRANT_FILE` names, written by the extension before each of your bash
|
|
@@ -422,17 +426,18 @@ check write no `.legion/<issue>/<phase>.json`, commit no handoff, and report wit
|
|
|
422
426
|
|
|
423
427
|
## Completion: report to the architect, then stay
|
|
424
428
|
|
|
425
|
-
Report completion to the architect
|
|
426
|
-
`summary
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
invent a different completion protocol — this is the
|
|
429
|
+
Report completion to the architect with the `legion` tool's `handoff_complete` and a two-sentence
|
|
430
|
+
`summary`. This publishes your phase's completion to the architect's role and clears the daemon's
|
|
431
|
+
record of this issue's active phase. A shell `legion handoff complete`, should one run, completes
|
|
432
|
+
the phase at the daemon just as well and is not refused, but only the tool call tells the
|
|
433
|
+
extension's phase stall: after a shell completion the reminder arrives when the turn settles and
|
|
434
|
+
again after each later Envoy delivery's turn, until a tool `handoff_complete` next succeeds (in
|
|
435
|
+
practice the next phase's). A completion made on that reminder is refused and closes nothing —
|
|
436
|
+
`HANDOFF_NOT_CURRENT_PHASE` once the phase has moved, `HANDOFF_ALREADY_RECORDED` where a
|
|
437
|
+
completion moves nothing (the implementer before its pull request exists; the production check) —
|
|
438
|
+
so answer the reminder with a WAITING line saying the phase was completed from the shell. Do not
|
|
439
|
+
add pipeline labels, run a controller loop, or invent a different completion protocol — this is the
|
|
440
|
+
whole contract.
|
|
436
441
|
|
|
437
442
|
A reviewer's phase ends with its completion, not with its review; the order of a review round
|
|
438
443
|
(the handoff push; for an approval, CI settled green at that head; the review of that head; then
|
|
@@ -488,6 +493,6 @@ decision required.
|
|
|
488
493
|
|
|
489
494
|
Never yield while blocked on a decision someone else owns. Before you stop, make the block visible
|
|
490
495
|
where its owner will see it: a product, scope, design, lifecycle, or cross-phase decision goes to
|
|
491
|
-
the owning architect as above, and a standalone human to-do goes in `
|
|
496
|
+
the owning architect as above, and a standalone human to-do goes in `dispatch ask`. Otherwise
|
|
492
497
|
proceed: proceeding is the default, and a phase that stops silently holds its issue until someone
|
|
493
498
|
notices.
|
|
@@ -96,7 +96,7 @@ review posted without a completion leaves the issue in reviewing until you finis
|
|
|
96
96
|
`; ` on the one READY line, or `none` — quoted from the `## For the reviewer` block at that same
|
|
97
97
|
head (or one line saying the body carries no brief — the packet still goes out), then the
|
|
98
98
|
`--summary` output and the PR body's gate facts. The merger sends it as the `summary` of its
|
|
99
|
-
`handoff_complete` with `ready: true`; the daemon posts it as a `
|
|
99
|
+
`handoff_complete` with `ready: true`; the daemon posts it as a `dispatch message` on the issue,
|
|
100
100
|
publishes it to the project's merge queue role when one is set, and says on the issue when that
|
|
101
101
|
role has no live holder. The READY packet names both the implementer's and tester's `E2E` lines;
|
|
102
102
|
a missing one is reported to the architect instead of completing. Legion never merges.
|
|
@@ -111,10 +111,10 @@ review posted without a completion leaves the issue in reviewing until you finis
|
|
|
111
111
|
this, since a staging gate does not run every resource production does. If the slot fails on
|
|
112
112
|
the change, the implementer owns the fix and the next slot.
|
|
113
113
|
The record has three places: the PR body's `Production:` line, one pull-request comment
|
|
114
|
-
carrying the Legion footer, and a `
|
|
114
|
+
carrying the Legion footer, and a `dispatch message` on the issue — the reviewer and merger
|
|
115
115
|
read GitHub, the architect reads the issue. When the deploy that carries the merge has not
|
|
116
116
|
happened (a shared profile still holding the previous plugin release, a daemon still running
|
|
117
|
-
the previous commit, a slot nobody has run), open a `
|
|
117
|
+
the previous commit, a slot nobody has run), open a `dispatch ask` that starts with the
|
|
118
118
|
production gap and why it matters, then names the required install or restart step, its risk,
|
|
119
119
|
and outcome-named options. Keep the `Production:` line at `pending <what is missing>`, and
|
|
120
120
|
complete the check once the human answers. Never record a staging pass as the production check,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.5.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"omp": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
|
-
"daemonApiVersion":
|
|
15
|
+
"daemonApiVersion": 17
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@legion/contracts": "0.10.0",
|
|
36
36
|
"@legion/envoy-client": "0.1.0",
|
|
37
37
|
"@legion/pi-shared": "0.1.0",
|
|
38
|
-
"@oh-my-pi/pi-utils": "^18.
|
|
38
|
+
"@oh-my-pi/pi-utils": "^18.6.0",
|
|
39
39
|
"@types/bun": "latest",
|
|
40
40
|
"typescript": "^5.3.0",
|
|
41
41
|
"zod": "4.3.6"
|