@sjawhar/pi-legion-envoy 5.24.4 → 5.24.6
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/envoy.js +7 -1
- package/dist/legion.js +8 -2
- package/dist/skills/dispatch/SKILL.md +27 -49
- package/dist/skills/dispatch/references/document-edits.md +6 -0
- package/dist/skills/dispatch/references/documents.md +6 -1
- package/dist/skills/dispatch/references/issues.md +20 -33
- package/dist/skills/legion-worker/SKILL.md +16 -8
- package/dist/skills/legion-worker/references/conflicts-and-rewrites.md +5 -8
- package/dist/skills/legion-worker/references/merge-gate.md +3 -5
- package/dist/skills/legion-worker/references/pr-body.md +8 -17
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -32805,8 +32805,14 @@ function renderAdvice(tool, key, advice, opts) {
|
|
|
32805
32805
|
const openAsks = advice.your_open_asks;
|
|
32806
32806
|
const writesSinceHuman = advice.session_writes_since_human;
|
|
32807
32807
|
const lines = [];
|
|
32808
|
+
const unparsed = advice.unparsed_openers;
|
|
32809
|
+
if (unparsed !== undefined && unparsed.count > 0 && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
|
|
32810
|
+
const quoted = unparsed.examples.map((example) => JSON.stringify(example)).join(", ");
|
|
32811
|
+
const subject2 = unparsed.count === 1 ? "1 typed-block opening in this document is text, not a block" : `${unparsed.count} typed-block openings in this document are text, not blocks`;
|
|
32812
|
+
lines.push(`${subject2}: ${quoted}. An opening like \`:::ask{\u2026}\` makes a block only as a line of its own, so as text it asks nobody. Mentioning the syntax on purpose? Put it in code. See the \`dispatch\` skill, "Decision blocks".`);
|
|
32813
|
+
}
|
|
32808
32814
|
if (advice.decision_blocks === 0 && opts.isPrimarySpec === true && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
|
|
32809
|
-
lines.push('
|
|
32815
|
+
lines.push('This spec holds no ask blocks, so nothing here reaches a human\'s inbox. Want human feedback? See the `dispatch` skill, "Decision blocks".');
|
|
32810
32816
|
}
|
|
32811
32817
|
if (hasIssueAdvice && writesSinceHuman !== undefined && writesSinceHuman >= 3 && (tool === "dispatch_message" || tool === "dispatch_ask" || tool === "dispatch_comment" && opts.isAskReply !== true)) {
|
|
32812
32818
|
const middle = writesSinceHuman >= 6 ? "Stop posting here until a human replies." : "Progress ledger or scratchpad? If so, stop.";
|
package/dist/legion.js
CHANGED
|
@@ -31744,8 +31744,14 @@ function renderAdvice(tool, key, advice, opts) {
|
|
|
31744
31744
|
const openAsks = advice.your_open_asks;
|
|
31745
31745
|
const writesSinceHuman = advice.session_writes_since_human;
|
|
31746
31746
|
const lines = [];
|
|
31747
|
+
const unparsed = advice.unparsed_openers;
|
|
31748
|
+
if (unparsed !== undefined && unparsed.count > 0 && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
|
|
31749
|
+
const quoted = unparsed.examples.map((example) => JSON.stringify(example)).join(", ");
|
|
31750
|
+
const subject2 = unparsed.count === 1 ? "1 typed-block opening in this document is text, not a block" : `${unparsed.count} typed-block openings in this document are text, not blocks`;
|
|
31751
|
+
lines.push(`${subject2}: ${quoted}. An opening like \`:::ask{\u2026}\` makes a block only as a line of its own, so as text it asks nobody. Mentioning the syntax on purpose? Put it in code. See the \`dispatch\` skill, "Decision blocks".`);
|
|
31752
|
+
}
|
|
31747
31753
|
if (advice.decision_blocks === 0 && opts.isPrimarySpec === true && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
|
|
31748
|
-
lines.push('
|
|
31754
|
+
lines.push('This spec holds no ask blocks, so nothing here reaches a human\'s inbox. Want human feedback? See the `dispatch` skill, "Decision blocks".');
|
|
31749
31755
|
}
|
|
31750
31756
|
if (hasIssueAdvice && writesSinceHuman !== undefined && writesSinceHuman >= 3 && (tool === "dispatch_message" || tool === "dispatch_ask" || tool === "dispatch_comment" && opts.isAskReply !== true)) {
|
|
31751
31757
|
const middle = writesSinceHuman >= 6 ? "Stop posting here until a human replies." : "Progress ledger or scratchpad? If so, stop.";
|
|
@@ -33531,7 +33537,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
33531
33537
|
// package.json
|
|
33532
33538
|
var package_default = {
|
|
33533
33539
|
name: "@sjawhar/pi-legion-envoy",
|
|
33534
|
-
version: "5.24.
|
|
33540
|
+
version: "5.24.6",
|
|
33535
33541
|
type: "module",
|
|
33536
33542
|
omp: {
|
|
33537
33543
|
extensions: [
|
|
@@ -49,10 +49,8 @@ discussion it came from — never as a compressed standalone ask.
|
|
|
49
49
|
|
|
50
50
|
## Writing for the human
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
message, and PR body is read by a person who has not read the code, does not share this session's
|
|
55
|
-
vocabulary, and is often on a phone. Write for that person.
|
|
52
|
+
Every spec, ask, comment, message, and PR body is read by a person who has not read the code, does
|
|
53
|
+
not share this session's vocabulary, and is often on a phone. Write for that person.
|
|
56
54
|
|
|
57
55
|
- Plain English, full sentences, one idea per sentence. Never repo shorthand or nouns you coined:
|
|
58
56
|
not "fix 8c", "READY-target", "PR B", "spec@v3", "the pair", "the packet" — say what the thing is.
|
|
@@ -73,8 +71,8 @@ vocabulary, and is often on a phone. Write for that person.
|
|
|
73
71
|
shapes the sentence under it.
|
|
74
72
|
- Before posting, test it: could Sami, reading only this text on his phone, know what he is being
|
|
75
73
|
told or asked? If not, rewrite it. Length is not the problem; density is.
|
|
76
|
-
- When an ask or message communicates a judgment, lead with that judgment in one sentence and put the mechanism underneath it. Do not make the reader ask a second time whether the result is a win. This shapes communication only when a judgment exists; it does not pre-decide an open question or remove its genuine options.
|
|
77
|
-
- When a Dispatch message states a root cause, include the reproducing command or test in that same message. Without it, label the diagnosis a hypothesis; a diagnosis still in progress may say so plainly. This boundary applies to causal claims, not to reporting that an investigation has started.
|
|
74
|
+
- When an ask or message communicates a judgment, lead with that judgment in one sentence and put the mechanism underneath it. Do not make the reader ask a second time whether the result is a win. This shapes communication only when a judgment exists; it does not pre-decide an open question or remove its genuine options.
|
|
75
|
+
- When a Dispatch message states a root cause, include the reproducing command or test in that same message. Without it, label the diagnosis a hypothesis; a diagnosis still in progress may say so plainly. This boundary applies to causal claims, not to reporting that an investigation has started.
|
|
78
76
|
|
|
79
77
|
## Writing a spec
|
|
80
78
|
|
|
@@ -110,15 +108,18 @@ A spec has two readers: the human who decides reads the **Summary** and **New si
|
|
|
110
108
|
## Decision blocks
|
|
111
109
|
|
|
112
110
|
A decision a human must make is an `:::ask` block where the decision arises in the spec: inside
|
|
113
|
-
the section whose content it is about, never gathered into a list at the top or bottom.
|
|
114
|
-
LEGION-204 comment, 2026-09-20 14:47Z, verbatim: "Adding a bunch of decision blocks at the top is
|
|
115
|
-
terrible!! Decisions should be in context in the spec".
|
|
111
|
+
the section whose content it is about, never gathered into a list at the top or bottom.
|
|
116
112
|
|
|
117
113
|
The block is what reaches the human's Inbox. A question phrased as prose in the spec reaches
|
|
118
114
|
nobody. A spec with no ask blocks is fine only when the issue genuinely needs no human decision.
|
|
119
|
-
When `dispatch_issue` or `dispatch_artifact` answers `
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
When `dispatch_issue` or `dispatch_artifact` answers `This spec holds no ask blocks …`, read it as a
|
|
116
|
+
question: either no decision is needed and you say nothing, or you forgot to make it a block. When
|
|
117
|
+
it answers `… typed-block openings in this document are text, not blocks`, the quoted openings are
|
|
118
|
+
blocks stored as prose (inside a line, or a paste with something before every line): fix the markdown
|
|
119
|
+
and upload again; a mention on purpose belongs in code. Neither answer sees a spec wrapped whole in a
|
|
120
|
+
code fence (take the fence off), a malformed opening inside a line (`::ask{`, `:::ask {`: an ask opens
|
|
121
|
+
only as `:::ask{…}` on a line of its own), or any `dispatch_doc_edit`: after an edit that writes an
|
|
122
|
+
ask, `dispatch_doc_read` the section and check it renders as `:::ask{#<id> …}` on its own line.
|
|
122
123
|
|
|
123
124
|
**Wrong:** a **Decisions needed** list at the top of the spec with three bullets.
|
|
124
125
|
**Right:** put each decision in the design section it belongs to as an `:::ask{#slug}` block, with
|
|
@@ -127,14 +128,8 @@ make the decision a block and must fix the spec.
|
|
|
127
128
|
A spec that already has the pile is repaired with `move`, not rewritten: `dispatch_doc_edit` with
|
|
128
129
|
`{ op: "move", block: "<block-uuid>", after: "<the sentence that states the options>" }` relocates
|
|
129
130
|
the block and keeps its ask, its answer and its followers; the context paragraphs that were lifted
|
|
130
|
-
out of Design move the same way, and the emptied section is deleted
|
|
131
|
-
|
|
132
|
-
discussion in the spec, not just all piled up at the start with no context"). An ask block has two
|
|
133
|
-
ids: the block id, shown as `:::ask{#<uuid> …}` in the rendered document and taken bare by
|
|
134
|
-
`move`/`delete` in `block` (the `block:<uuid>` form is only for `before`/`after` anchors), and the
|
|
135
|
-
ask id, which `dispatch_open_asks`, the dashboard's `?ask=` link, `dispatch_read` and
|
|
136
|
-
`dispatch_comment({ reply_to_ask })` use. They differ; `dispatch://KEY/ask/<block-id>` answers
|
|
137
|
-
`not found`.
|
|
131
|
+
out of Design move the same way, and the emptied section is deleted. An ask block has two ids that
|
|
132
|
+
differ; [Editing a document](skill://dispatch/references/document-edits.md) says which.
|
|
138
133
|
|
|
139
134
|
See [Typed blocks](#typed-blocks) for the syntax and [Before you ask](#before-you-ask) under
|
|
140
135
|
[Asking](#asking) to decide whether the question is a real decision at all.
|
|
@@ -185,8 +180,7 @@ When a symptom and its cause sit on different issues, the work accrues to the ca
|
|
|
185
180
|
the symptom's issue carries a pointer to it. Before posting a measurement or finding, search
|
|
186
181
|
Dispatch for the failing identity's or component's name, and post on the issue whose title names
|
|
187
182
|
the fix, not the one naming the symptom. A symptom issue gathering messages with no human response
|
|
188
|
-
is the tell.
|
|
189
|
-
and answer sat on AGENTC-1010.)
|
|
183
|
+
is the tell.
|
|
190
184
|
|
|
191
185
|
## Claim the issue before you work it
|
|
192
186
|
|
|
@@ -216,11 +210,7 @@ field, and choosing your next issue are in [Working an issue](skill://dispatch/r
|
|
|
216
210
|
|
|
217
211
|
### Before you ask
|
|
218
212
|
|
|
219
|
-
|
|
220
|
-
completely disconnected from any discussion of design or trade-offs. This is not a very useful way
|
|
221
|
-
of having this discussion" (on a report-table shape), and "What's a fenced PutObject or phantom
|
|
222
|
-
eval_id? What's an R4 model header? What exactly is the question or uncertainty here?" (on a
|
|
223
|
-
production import). Every `dispatch_ask` passes four gates first:
|
|
213
|
+
Every `dispatch_ask` passes four gates first:
|
|
224
214
|
|
|
225
215
|
1. **Does it need his authority, taste, or risk appetite?** This is the bar for a decision
|
|
226
216
|
written as an `:::ask` block in context ([Decision blocks](#decision-blocks)). Technical
|
|
@@ -228,19 +218,18 @@ production import). Every `dispatch_ask` passes four gates first:
|
|
|
228
218
|
internals are your lane's to decide and record in the spec. Two things still go to the
|
|
229
219
|
platform PO over Envoy: a contract between two lanes, and a halt condition (a change to IAM,
|
|
230
220
|
deletion or exposure of production data, anything that reaches a customer). The PO takes those
|
|
231
|
-
to Sami as a Dispatch ask; you do not open one yourself, even as a permission ask under gate 2
|
|
232
|
-
(Sami, 2026-09-25, AGENTC-34 §12).
|
|
221
|
+
to Sami as a Dispatch ask; you do not open one yourself, even as a permission ask under gate 2.
|
|
233
222
|
2. **Is there genuine uncertainty, and have you measured what you can?** If there is none, it is
|
|
234
223
|
a plan you execute. The one legitimate ask without uncertainty is permission for an action
|
|
235
224
|
only a human can authorise — a production write, an external send, a console action — and then
|
|
236
225
|
the question is that action in one sentence, with options that name its outcomes (below).
|
|
237
226
|
Measure before you write: how many are affected, whether anything reaches the path, what the
|
|
238
227
|
current state already is. The measurement decides whether a human is needed at all, and when
|
|
239
|
-
one is, it turns a research request he cannot answer into a decision he can
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
228
|
+
one is, it turns a research request he cannot answer into a decision he can. Put the
|
|
229
|
+
measurement and the size of the affected population in the ask. When the measurement shows one
|
|
230
|
+
fix cannot repair most of that population and another fix can, drop the first, rather than
|
|
231
|
+
offering it cut down to the part it reaches. An option not to act, such as a permission ask's
|
|
232
|
+
Hold, is not a fix, and it stays.
|
|
244
233
|
Report what the measurement could **not** establish, with its own control: "I found no
|
|
245
234
|
evidence" and "there is no evidence to find" read alike and mean opposite things, and a
|
|
246
235
|
control that shares the query's blind spot proves neither. Before you say you are waiting on
|
|
@@ -302,13 +291,8 @@ passage with `anchor`. Follow up on an ask or comment with `dispatch_comment`; c
|
|
|
302
291
|
with a `dispatch://` reference (see [References](#references)). Never write "see above", "the
|
|
303
292
|
message above", or "as attached".
|
|
304
293
|
|
|
305
|
-
**Pointing at another message is a defect, not a shortcut
|
|
306
|
-
|
|
307
|
-
as a comment: "you just dump information into messages and then add a new ask that references a
|
|
308
|
-
previous message in prose with no link or no context whatsoever and uses compressed shorthand
|
|
309
|
-
jargon." The ask view does not show the issue's comments, so that ask was unanswerable; "Cloud
|
|
310
|
-
Identity licence check / 2SV override / 1-day grace" was shorthand he had never used. The rules
|
|
311
|
-
that follow from it:
|
|
294
|
+
**Pointing at another message is a defect, not a shortcut:** the ask view does not show the
|
|
295
|
+
issue's comments. The rules:
|
|
312
296
|
|
|
313
297
|
- An ask that names another message in prose — "my comment above", "the procedure I posted",
|
|
314
298
|
"see the earlier message" — is retracted by the PO as failing the gates. Put the content IN the
|
|
@@ -332,7 +316,7 @@ they must read to decide belongs in the spec in the first place — see [Artifac
|
|
|
332
316
|
|
|
333
317
|
Before saying you are waiting for human input, call `dispatch_open_asks`. With no arguments it lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply. With `dispatch_open_asks({ project })` it lists every open ask in that project — on its issues and on its documents, whoever authored them — which is how you audit what a whole project is waiting on rather than just your own asks.
|
|
334
318
|
|
|
335
|
-
**Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, send a one-line ask before the first implementation commit. A lane's schema decision or a platform-PO contract ruling does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request
|
|
319
|
+
**Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, send a one-line ask before the first implementation commit. A lane's schema decision or a platform-PO contract ruling does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request. A control or behaviour the human asked for in words is settled by those words, together with every choice inside it that his words do not make (where it sits, its defaults, its options): build it without an ask, as gate 4 of [Before you ask](#before-you-ask) says. This rule covers only product shape outside what he asked for, and its ask comes before the commit that sets that shape.
|
|
336
320
|
|
|
337
321
|
**Anything you are blocked on a human for is an open ask.** An agent waits on a human only through
|
|
338
322
|
an open ask. An approval, a credential or grant to renew, a setting only they can change, a review
|
|
@@ -371,10 +355,6 @@ other way — Sami said it live, a later comment settled it, or the question bec
|
|
|
371
355
|
design moved — resolve it yourself with `dispatch_resolve_ask` in the same turn you learn that.
|
|
372
356
|
Never leave it for the human to clear.
|
|
373
357
|
|
|
374
|
-
Sami, AGENTC-27 `rules-derivation-2026-09-17.md` row R04, verbatim: "I think this was answered
|
|
375
|
-
live. If not, please reask." The same pattern left him closing asks as `Dismissed`, `Settled`, and
|
|
376
|
-
`Resolved I think`: noise the human had to clear.
|
|
377
|
-
|
|
378
358
|
Every later write on the issue answers `You still have an open ask on …` and names it. Treat that
|
|
379
359
|
as the checklist: if it is still needed, leave it; if it was answered elsewhere, resolve it with
|
|
380
360
|
the resolving fact as the reason. Before posting a new ask, inspect your open ones. If the new ask
|
|
@@ -441,9 +421,7 @@ Uploading one (`dispatch_artifact`, its slugs, versions and Markdown rules) is i
|
|
|
441
421
|
|
|
442
422
|
## Structure over stream
|
|
443
423
|
|
|
444
|
-
Dispatch is a structured workspace, never a message stream
|
|
445
|
-
to me that agents keep trying to use dispatch as a giant stream of messages instead of
|
|
446
|
-
high-signal, structured conversation"). The structure IS the product:
|
|
424
|
+
Dispatch is a structured workspace, never a message stream. The structure IS the product:
|
|
447
425
|
|
|
448
426
|
- **One issue per piece of work.** A new deliverable — an email to send, a document to review, a
|
|
449
427
|
decision with its own lifecycle — gets its own issue with the content as the issue's document
|
|
@@ -182,3 +182,9 @@ deletion cannot undo a removal.
|
|
|
182
182
|
`retype` turns the paragraph or typed block with `block` into the named typed `type` in place. It keeps the
|
|
183
183
|
block id, keeps a typed block's body, and uses `attributes` for client-owned typed attributes. Use it when
|
|
184
184
|
an existing paragraph is the question that should become a decision.
|
|
185
|
+
|
|
186
|
+
An ask block has two ids: the block id, shown as `:::ask{#<id> …}` in the rendered document and
|
|
187
|
+
taken bare by `move`/`delete` in `block` (the `block:<id>` form is only for `before`/`after`
|
|
188
|
+
anchors), and the ask id, which `dispatch_open_asks`, the dashboard's `?ask=` link, `dispatch_read`
|
|
189
|
+
and `dispatch_comment({ reply_to_ask })` use. They differ; `dispatch://KEY/ask/<block-id>` answers
|
|
190
|
+
`not found`.
|
|
@@ -11,7 +11,12 @@ The server declares typed document blocks at `GET /api/v1/schema/blocks`. Write
|
|
|
11
11
|
container-directive form `:::name{#block-id key="value"}` on its own line, ordinary block children,
|
|
12
12
|
and a closing line of as many colons at the same nesting. A typed block directly inside another needs the outer
|
|
13
13
|
one's fence a colon longer (`::::callout{…}` around a `:::callout{…}`), and so does one whose code holds a `:::` line;
|
|
14
|
-
Dispatch writes its fences that way. An unclosed typed block at document level is rejected.
|
|
14
|
+
Dispatch writes its fences that way. An unclosed typed block at document level is rejected. An
|
|
15
|
+
opening line that continues a paragraph instead of standing on its own (indented four or more
|
|
16
|
+
columns under the paragraph's text) is refused, naming the line's number and text - `INVALID_OP` on an edit's `markdown` or `with`, `INVALID_MARKDOWN` on any other write - since it
|
|
17
|
+
would be stored as the paragraph's text; start the block on its own line. An opening written inside
|
|
18
|
+
a line is stored as text, and `dispatch_issue` and `dispatch_artifact` quote it back as a
|
|
19
|
+
typed-block opening that is text, not a block. For
|
|
15
20
|
a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing typed block, retain
|
|
16
21
|
its id and every rendered attribute. Never copy an existing block's id into new markdown: an id
|
|
17
22
|
names one block, so an insert, upload or suggestion whose markdown names an id the document holds
|
|
@@ -9,15 +9,14 @@ project's architecture model, or list or audit a project's backlog.
|
|
|
9
9
|
When you finish an issue, or are told to work on the next thing, take the top ready issue of the
|
|
10
10
|
whole backlog, across every project: status `todo`, highest priority first, then board rank. There
|
|
11
11
|
are no areas: a standing role, a product owner and a lane each take the top issue like everyone
|
|
12
|
-
else (
|
|
12
|
+
else (dispatch://AGENTC-34/ask/01ed2956-73cc-48d2-8ed4-7a86c6d439b1). `todo`
|
|
13
13
|
means ready: specced, unblocked, and waiting on neither a deploy nor a decision. An issue that
|
|
14
14
|
waits on one belongs in `backlog`, with what it waits on said on the issue.
|
|
15
15
|
|
|
16
16
|
Hold at most three issues in flight (`in_progress`, `testing`, `needs_review` or `retro`), of any
|
|
17
|
-
kind (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
dispatch://AGENTC-393/ask/b773d9f6): the priorities decide only what you pull next. Past three:
|
|
17
|
+
kind (dispatch://AGENTC-34/ask/1aeb8f2e-0950-4eaa-aaac-24286c9dd3ca). The limit is per agent and
|
|
18
|
+
has nothing to do with the week's priorities (dispatch://AGENTC-393/comment/a7647eb0): the
|
|
19
|
+
priorities decide only what you pull next. Past three:
|
|
21
20
|
push any unfinished work, say where in one comment on the issue, move it to `backlog` and clear
|
|
22
21
|
its route. Each issue counts on its own; a child does not ride under its parent's slot.
|
|
23
22
|
In-flight issues with no owner at all go
|
|
@@ -38,9 +37,7 @@ reordering the board yourself.
|
|
|
38
37
|
|
|
39
38
|
## Claim the issue before you work it
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
on it (Sami, 2026-09-24, verbatim: "It seems like we need a better way of tracking what's already
|
|
43
|
-
in progress"). So before you start implementing an issue, claim it:
|
|
40
|
+
Before you start implementing an issue, claim it:
|
|
44
41
|
|
|
45
42
|
```ts
|
|
46
43
|
dispatch_claim({ issue: "LEGION-234" }) // I am implementing this
|
|
@@ -76,8 +73,7 @@ you find work nobody is on.
|
|
|
76
73
|
|
|
77
74
|
**Claiming and moving the status are two separate actions, and you do both.** A claim says which
|
|
78
75
|
session is on the work; the status says where the work has got to, and humans use it to track
|
|
79
|
-
that too
|
|
80
|
-
using them to keep track of work"). So when you start: `dispatch_claim({ issue })` **and**
|
|
76
|
+
that too. So when you start: `dispatch_claim({ issue })` **and**
|
|
81
77
|
`dispatch_issue_update({ issue, status: "in_progress" })`.
|
|
82
78
|
|
|
83
79
|
## Issue status is yours to move
|
|
@@ -87,9 +83,8 @@ the daemon writes it), the session doing the work moves it, the way a person mov
|
|
|
87
83
|
`in_progress` when implementation starts, `testing` when the change is being proven on a
|
|
88
84
|
production-like surface, `needs_review` when its pull request is open and waiting on the merge
|
|
89
85
|
queue, `done` when the change has been driven in production (a merge is not `done`). Move child
|
|
90
|
-
issues you own as well as the root. An issue left at `triage` while work is underway is a defect
|
|
91
|
-
|
|
92
|
-
status is." Waiting for the deploy lane is not a status and is never announced.
|
|
86
|
+
issues you own as well as the root. An issue left at `triage` while work is underway is a defect.
|
|
87
|
+
Waiting for the deploy lane is not a status and is never announced.
|
|
93
88
|
|
|
94
89
|
```ts
|
|
95
90
|
// PATCH /api/v1/issues/{key} — status, title, labels, priority, external_links (merged by URL), route, parent
|
|
@@ -122,10 +117,9 @@ when work has started.
|
|
|
122
117
|
## Priority is yours to set
|
|
123
118
|
|
|
124
119
|
Priority is the coarse bucket a backlog is read by: `0` is P0, the highest, through `3`, P3, the
|
|
125
|
-
lowest, and `null` clears it.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
and say what you set and why; he overrides anything he disagrees with from the dashboard. A closed
|
|
120
|
+
lowest, and `null` clears it. Agents set it (`dispatch://LEGION/artifact/issue-status-conventions-md`)
|
|
121
|
+
— on creation, and on a grooming pass over issues that have none — and say what you set and why;
|
|
122
|
+
Sami overrides anything he disagrees with from the dashboard. A closed
|
|
129
123
|
issue takes only `rank`, `components`, and a reopening `status` (any status but `done`);
|
|
130
124
|
everything else, `priority` included, waits for the reopen (`409 ISSUE_CLOSED`). So reopen it
|
|
131
125
|
first, then set the priority — the two cannot go in one call. `rank` itself is not a tool field:
|
|
@@ -165,26 +159,21 @@ dispatch_issues({ project, priority: [0, 1], limit: 250 })
|
|
|
165
159
|
Every unclaimed row in `triage`, `icebox`, `backlog` or `todo` is a decision: someone takes it and
|
|
166
160
|
builds it, or it closes. A row in `in_progress`, `testing`, `needs_review` or `retro`, or one that
|
|
167
161
|
carries a claim, is work under way ([Issue status is yours to move](#issue-status-is-yours-to-move))
|
|
168
|
-
and is not re-staffed. A todo with a finished spec reads as queued work that nobody is doing
|
|
169
|
-
(LEGION-173 sat in todo for two weeks with a complete spec; AGENTC-1010's v4 plan sat in backlog
|
|
170
|
-
with nobody building it).
|
|
162
|
+
and is not re-staffed. A todo with a finished spec reads as queued work that nobody is doing.
|
|
171
163
|
|
|
172
164
|
A close that says the defect cannot happen cites the code that makes it impossible. An issue
|
|
173
165
|
closed because a rewrite forecloses it names the file and line in the rewrite that does so; a
|
|
174
166
|
close that cannot name one is not foreclosed, it is unread. The cheapest way for a rewrite to reach
|
|
175
|
-
parity is to port the code, defect included
|
|
176
|
-
the TypeScript daemon, had been ported into the Go coordinator and was live in production.
|
|
167
|
+
parity is to port the code, defect included.
|
|
177
168
|
|
|
178
169
|
The audit finds four shapes:
|
|
179
170
|
|
|
180
171
|
- **Unstaffed work.** A plan or measurement exists, and no one is building it.
|
|
181
172
|
- **Unrecorded delivery.** An issue not yet in `testing` or `done`, claimed or not, has a merged PR
|
|
182
|
-
naming it. Check the change live, then move the issue
|
|
183
|
-
agent-c #20367, merged).
|
|
173
|
+
naming it. Check the change live, then move the issue.
|
|
184
174
|
- **Unrecorded practice.** Someone does the issue's work by hand, more than once, while the issue
|
|
185
|
-
sits in backlog
|
|
186
|
-
|
|
187
|
-
wearing the wrong status.
|
|
175
|
+
sits in backlog. It leaves no plan and no PR to find; the tell is your own messages. Doing
|
|
176
|
+
something by hand more than once means an issue is wearing the wrong status.
|
|
188
177
|
- **Unreachable route.** An open issue whose route names a role nobody holds, or a session that is
|
|
189
178
|
not running, reaches nobody, whatever its priority, and the priority filter above never finds
|
|
190
179
|
it. List it on its own:
|
|
@@ -192,16 +181,14 @@ The audit finds four shapes:
|
|
|
192
181
|
dispatch_issues({ project, route_status: "no_holder", limit: 250 })
|
|
193
182
|
```
|
|
194
183
|
Each row reads `route role:sre (nobody holds it right now)` or `route session:<id> (that session
|
|
195
|
-
is not running right now)`. That is one read of the listener, and one read
|
|
196
|
-
|
|
197
|
-
is absent from the listener for minutes, and its role with it.
|
|
198
|
-
routes one read showed as unreachable pointed at a single session that was moving between boxes.
|
|
184
|
+
is not running right now)`. That is one read of the listener, and one read cannot tell a
|
|
185
|
+
restart gap from a vacancy: an agent box that restarts or resumes keeps the session id, but the
|
|
186
|
+
session is absent from the listener for minutes, and its role with it.
|
|
199
187
|
So a route is unowned only when it is `no_holder` on two reads at least ten minutes apart: list
|
|
200
188
|
again after ten minutes and act on the issues both lists name. Confirm with the second
|
|
201
189
|
`dispatch_issues` read, not `envoy_role_get`: a role lookup releases the claim of a holder whose
|
|
202
190
|
session is absent from the registry as it answers. Then staff the role, re-route the
|
|
203
|
-
issue to a live holder, or clear the route and assign it
|
|
204
|
-
503, sat routed to an unheld `role:sre` with no assignee). `route_status: "unknown"` means the
|
|
191
|
+
issue to a live holder, or clear the route and assign it. `route_status: "unknown"` means the
|
|
205
192
|
listener did not answer, so a route could not be judged; a `no_holder` filter refuses rather
|
|
206
193
|
than answer an empty list then.
|
|
207
194
|
|
|
@@ -77,8 +77,15 @@ passed — never as a fresh identity.
|
|
|
77
77
|
|
|
78
78
|
Deployment instructions, when present, are the operator's standing rules for this repository —
|
|
79
79
|
required checks, deploy/smoke commands, code-owner expectations, standing roles you may consult,
|
|
80
|
-
the merge credential. They override this skill's defaults where they conflict
|
|
81
|
-
override
|
|
80
|
+
the merge credential. They override this skill's defaults where they conflict, except four rules
|
|
81
|
+
they never override: no deferrals (*PR body, review, and the merge gate*, below); bringing the base
|
|
82
|
+
into the branch only on a real conflict or a retarget
|
|
83
|
+
(`skill://legion-worker/references/conflicts-and-rewrites.md#reintegrating-the-base`); the
|
|
84
|
+
implementer's own proof on a production-like surface at the head that merges, an applied simplify
|
|
85
|
+
head included (`skill://legion-worker/references/pr-body.md#what-a-proof-is`,
|
|
86
|
+
`skill://legion-worker/references/pr-body.md#the-rules-every-phases-evidence-follows`); and the
|
|
87
|
+
implementer's production check after the merge
|
|
88
|
+
(`skill://legion-worker/references/merge-gate.md#after-the-human-merge`).
|
|
82
89
|
|
|
83
90
|
## Asking another role
|
|
84
91
|
|
|
@@ -145,8 +152,7 @@ new work.
|
|
|
145
152
|
|
|
146
153
|
**Shared operation safety:** Every Legion issue workspace is a `jj workspace` of one shared
|
|
147
154
|
clone, so they all share one operation log: `jj undo`, `jj abandon`, and
|
|
148
|
-
`jj op restore|revert|abandon|undo` rewrite it for every tree at once
|
|
149
|
-
worker's `jj undo` rewrote nine of another tree's commits). The extension refuses them in every
|
|
155
|
+
`jj op restore|revert|abandon|undo` rewrite it for every tree at once. The extension refuses them in every
|
|
150
156
|
phase-worker pane before they run — a `bash` command in any position of a pipeline or `&&`
|
|
151
157
|
chain, with or without `-R`, judged on the whole argument list; `eval` code; and a `hub`
|
|
152
158
|
process start — from your own tool calls and from any `task` subagent you spawn (it runs in
|
|
@@ -196,7 +202,7 @@ committer at all, and the one rewrite still open to you (*Rewriting pushed commi
|
|
|
196
202
|
reference) resets the committer only of commits on your own chain that descend from the commit
|
|
197
203
|
you named, after its guard cleared. Another role's commit
|
|
198
204
|
carrying you as committer, which you did not rewrite that way, is evidence that something
|
|
199
|
-
rewrote commits it should not have
|
|
205
|
+
rewrote commits it should not have. Stop and send the
|
|
200
206
|
architect that log; do not accept it as a side effect. A wrong identity on your own commit, the
|
|
201
207
|
other App or none, is a pane-environment problem to report to the architect, not something to
|
|
202
208
|
pin (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`,
|
|
@@ -220,7 +226,7 @@ subcommand's `comment`, `create`, `edit`, `close`, `reopen`, `delete`, `pin`, `u
|
|
|
220
226
|
GET (an explicit `-X`, or the POST that `-f`/`-F`/`--input` imply; pull-request conversation
|
|
221
227
|
comments live on that path too, so edit them with `gh pr comment`) — printing
|
|
222
228
|
`Legion issues live on Dispatch; use dispatch_message or dispatch_comment on <your LEGION_ISSUE>`:
|
|
223
|
-
Legion never reads or writes a GitHub issue
|
|
229
|
+
Legion never reads or writes a GitHub issue. `pr comment`, `pr review`,
|
|
224
230
|
`api …/pulls/…`, `api graphql`, and issue reads are unaffected. The credential reaches `legion`
|
|
225
231
|
through the file `$LEGION_GRANT_FILE` names, written by the extension before each of your bash
|
|
226
232
|
commands, each `github` tool call, and each `read`/`grep` of a `pr://` or `issue://` URL (and by
|
|
@@ -294,7 +300,7 @@ line), the full definition of a proof, what the tester verifies, and the simplif
|
|
|
294
300
|
thread's opener (or, on a bot's thread, from the Legion reviewer) closes one. The implementer
|
|
295
301
|
runs `legion threads resolve` before every push that answers a review, and the merger before
|
|
296
302
|
READY: `skill://legion-worker/references/review-threads.md`.
|
|
297
|
-
- **No deferrals.**
|
|
303
|
+
- **No deferrals.** A finding that changes
|
|
298
304
|
behaviour, hides an error, or breaks a gate is fixed in this pull request; naming, duplication,
|
|
299
305
|
or wording cleanup is batched into the one `Fast-follow:` line instead of iterating per push.
|
|
300
306
|
- **A red CI job** that failed on its own is re-run with
|
|
@@ -329,7 +335,9 @@ with. With `--data` omitted, `legion handoff write` reads the JSON object from s
|
|
|
329
335
|
|
|
330
336
|
`handoff_write` validates the payload against the phase's schema before writing: an
|
|
331
337
|
implement handoff without a well-formed `proof`, or a test handoff that reports no failure and
|
|
332
|
-
carries no `proof` of its own, exits 1 naming the field and writes nothing.
|
|
338
|
+
carries no `proof` of its own, exits 1 naming the field and writes nothing. Each `proof` entry, in
|
|
339
|
+
either phase, is an object of six non-empty strings: `criterion` (the acceptance line it proves),
|
|
340
|
+
`surface`, `command`, `observed`, `headSha` (the commit it ran at) and `negativeControl`.
|
|
333
341
|
|
|
334
342
|
Then verify the durable artifact exists:
|
|
335
343
|
|
|
@@ -8,8 +8,7 @@ Every path it cites is in sjawhar/legion.
|
|
|
8
8
|
## Reintegrating the base
|
|
9
9
|
|
|
10
10
|
- **Reintegrate the base only on a real conflict, except after a base retarget — and with a
|
|
11
|
-
merge, never `jj rebase`.**
|
|
12
|
-
"Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
|
|
11
|
+
merge, never `jj rebase`.**
|
|
13
12
|
The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
|
|
14
13
|
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never reintegrate the base to
|
|
15
14
|
pick up `main` or refresh CI (a single failed CI job is re-run on its own: *A red CI job* in
|
|
@@ -24,8 +23,7 @@ Every path it cites is in sjawhar/legion.
|
|
|
24
23
|
jj always rebases every descendant of any commit it rewrites — a revset naming the root of your
|
|
25
24
|
own chain and rewriting it in place also rewrites whatever another tree has stacked on that root,
|
|
26
25
|
whichever selector chose it (`-s`, `-b`, and `-r` all rewrite descendants; `-r` only re-parents
|
|
27
|
-
them to fill the hole, which is worse).
|
|
28
|
-
conflict step moved a second issue's twelve commits and its bookmark onto a conflicted copy.
|
|
26
|
+
them to fill the hole, which is worse).
|
|
29
27
|
Resolve the conflict with a forward merge instead of a rewrite — merge the branch's own
|
|
30
28
|
bookmark with the destination in one new commit, so nothing existing is rewritten and nothing
|
|
31
29
|
built on your prior commits, in this tree or another, ever moves:
|
|
@@ -91,7 +89,8 @@ and the new head; the merger never computes a fingerprint — it uses the `--sum
|
|
|
91
89
|
## Rewriting pushed commits
|
|
92
90
|
|
|
93
91
|
**Rewriting pushed commits** — a `jj squash --into` a commit already on GitHub, or any other
|
|
94
|
-
rewrite of a commit you already pushed — is the
|
|
92
|
+
rewrite of a commit you already pushed — is the hazard *Reintegrating the base* describes, in a
|
|
93
|
+
second shape: jj rebases
|
|
95
94
|
every descendant of any commit it rewrites, and in the one shared repository a descendant can be
|
|
96
95
|
another tree's branch stacked on your pushed commit, which then moves, with its bookmark, onto a
|
|
97
96
|
rewritten copy. So look for a descendant outside your own chain first, and record the pushed tip
|
|
@@ -115,9 +114,7 @@ cd -- "$LEGION_WORKSPACE" && \
|
|
|
115
114
|
`descendants(<commit>) ~ ::@` is everything built on the commit you are about to rewrite that is
|
|
116
115
|
not on your own chain. Non-empty means the rewrite would move work that is not yours: do not
|
|
117
116
|
rewrite it. Put the change in a new commit on top instead, and report the listed commits to the
|
|
118
|
-
architect.
|
|
119
|
-
`Rebased 13 descendant commits` and moved a second issue's twelve commits and its bookmark; the
|
|
120
|
-
check above listed those thirteen and refused before anything moved.
|
|
117
|
+
architect.
|
|
121
118
|
|
|
122
119
|
Then rewrite, resolve, and push with the one push procedure (*Every role pushes its own commits*
|
|
123
120
|
in `skill://legion-worker`). It lets the remote branch sit on the
|
|
@@ -82,15 +82,13 @@ completion leaves the issue in reviewing until you finish.
|
|
|
82
82
|
|
|
83
83
|
## After the human merge
|
|
84
84
|
|
|
85
|
-
- **After a human merges, the implementer verifies in production.**
|
|
86
|
-
verbatim: "the agent that developed it should be responsible for testing in production."
|
|
85
|
+
- **After a human merges, the implementer verifies in production.**
|
|
87
86
|
The architect sends the implementer back once the merge lands; the implementer watches the
|
|
88
87
|
deploy slot that carries the merge to `production-apply` (or the equivalent publish step),
|
|
89
88
|
drives the changed path in production through the user's own access path, and records the
|
|
90
89
|
observation on the PR and the issue before the architect signs off. A staging pass is not
|
|
91
|
-
this
|
|
92
|
-
|
|
93
|
-
implementer owns the fix and the next slot.
|
|
90
|
+
this, since a staging gate does not run every resource production does. If the slot fails on
|
|
91
|
+
the change, the implementer owns the fix and the next slot.
|
|
94
92
|
The record has three places: the PR body's `Production:` line, one pull-request comment
|
|
95
93
|
carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
|
|
96
94
|
read GitHub, the architect reads the issue. When the deploy that carries the merge has not
|
|
@@ -51,17 +51,10 @@ a deliberately broken input and the refusal or failure observed. The surface is
|
|
|
51
51
|
repository, a real browser, a devN stack, staging, or a local stack with real migrations, one that
|
|
52
52
|
has the resource the change touches — and each `E2E` line carries a **link** to that run,
|
|
53
53
|
screenshot, or e2e; human review does not replace user-facing verification, and a green unit suite
|
|
54
|
-
is not it. A unit or integration test is a regression lock, never proof of a criterion.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
need to fix it; if it's tooling, we need to develop it; if it's skills, we need to fix the skills
|
|
59
|
-
... it should not require deploying to production to realize your feature doesn't work."
|
|
60
|
-
Evidence for the rule: in the week of 2026-09-08 three surfaces merged green and were wrong on
|
|
61
|
-
inspection (the Astrolabe IPI stack, Dispatch on ECS, the candidate flow), and on 2026-09-12 six
|
|
62
|
-
deploy slots died on code first executed after merge, including a production-only ECS bootstrap
|
|
63
|
-
the whole staging gate never ran. The implementer's proof and the tester's proof below are both
|
|
64
|
-
this proof.
|
|
54
|
+
is not it. A unit or integration test is a regression lock, never proof of a criterion. The agent
|
|
55
|
+
that develops the change proves it this way before the merge, and whatever blocks that proof is
|
|
56
|
+
fixed, not skipped (*When no surface reaches the changed path*, below). The implementer's proof
|
|
57
|
+
and the tester's proof below are both this proof.
|
|
65
58
|
|
|
66
59
|
## The rules every phase's evidence follows
|
|
67
60
|
|
|
@@ -98,8 +91,8 @@ this proof.
|
|
|
98
91
|
diff gets none.** It is scoped to the pull request's own diff, at the head where the last review
|
|
99
92
|
round closed: nothing applied leaves that head final; applied → the applied head is the final
|
|
100
93
|
head: CI runs on it, the pair runs once on it, and the E2E proof re-runs on it for the surface
|
|
101
|
-
the simplify diff touched
|
|
102
|
-
|
|
94
|
+
the simplify diff touched, since a refactor that "preserves behaviour" is a claim until it is
|
|
95
|
+
executed. That cost is
|
|
103
96
|
why 0-applied is the expected outcome and a pass that applies is spent sparingly. At the applied
|
|
104
97
|
head the implementer re-cites the `CI` line and re-runs its own proof into `E2E (implementer)`,
|
|
105
98
|
and the tester re-runs its proof for the touched surface into `E2E (tester)`, before the
|
|
@@ -116,10 +109,8 @@ No surface reaches the changed path is a report to the architect, never a reason
|
|
|
116
109
|
Say which surface is missing and what it would have to do — a rig that can spawn the role, a
|
|
117
110
|
sandbox that holds the resource, a credential, a command that does not exist yet — and send it to
|
|
118
111
|
the architect with `envoy_publish` to its role topic. The architect creates a child issue in this
|
|
119
|
-
tree to build it (infrastructure, tooling, or a skill) and resumes you once it lands.
|
|
120
|
-
|
|
121
|
-
infrastructure, we need to fix it; if it's tooling, we need to develop it; if it's skills, we need
|
|
122
|
-
to fix the skills." A code path whose first execution would be after the merge — a deploy
|
|
112
|
+
tree to build it (infrastructure, tooling, or a skill) and resumes you once it lands. A code path
|
|
113
|
+
whose first execution would be after the merge — a deploy
|
|
123
114
|
workflow's inline step, a post-merge helper, a production-only resource — is untested until you
|
|
124
115
|
have executed it somewhere production-like; completing with a unit-test-only handoff is the
|
|
125
116
|
failure this rule exists to stop.
|