@sjawhar/claude-envoy 1.13.0 → 1.14.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/.claude-plugin/plugin.json +1 -1
- package/.omp-plugin/plugin.json +1 -1
- package/dist/dispatch.js +37 -16
- package/dist/envoy-channel.js +40 -16
- package/dist/session-hook.js +33 -12
- package/legion/skills/legion-architect/SKILL.md +10 -8
- package/legion/skills/legion-architect/references/acceptance.md +5 -2
- package/legion/skills/legion-architect/references/decomposition.md +27 -18
- package/legion/skills/legion-architect/references/design-gate.md +4 -4
- package/legion/skills/legion-architect/references/wake-routing.md +5 -5
- package/legion/skills/legion-controller/SKILL.md +5 -2
- package/legion/skills/legion-controller/references/daily-report.md +7 -0
- package/legion/skills/legion-controller/references/weekly-audit.md +146 -0
- package/legion/skills/legion-worker/SKILL.md +1 -1
- package/package.json +2 -2
- package/skills/dispatch/SKILL.md +2 -2
- package/skills/dispatch-first/SKILL.md +3 -7
- package/skills/dispatch-writing-specs/SKILL.md +1 -1
package/.omp-plugin/plugin.json
CHANGED
package/dist/dispatch.js
CHANGED
|
@@ -14662,7 +14662,8 @@ var IssueEventPayloadSchema = object({
|
|
|
14662
14662
|
route: string2().nullish(),
|
|
14663
14663
|
changed: array(string2()).optional(),
|
|
14664
14664
|
ancestors: array(string2()).optional(),
|
|
14665
|
-
thread_logins: array(string2()).optional()
|
|
14665
|
+
thread_logins: array(string2()).optional(),
|
|
14666
|
+
forced: object({ past: array(string2()) }).optional()
|
|
14666
14667
|
});
|
|
14667
14668
|
var ArtifactCreatedEventPayloadSchema = object({
|
|
14668
14669
|
artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
|
|
@@ -15615,12 +15616,12 @@ var dispatchToolSpecs = [
|
|
|
15615
15616
|
{
|
|
15616
15617
|
name: "dispatch_related",
|
|
15617
15618
|
example: { project: "CORE", title: "Retry failed webhook deliveries" },
|
|
15618
|
-
description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead.
|
|
15619
|
+
description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issues leaves each named issue's own " + "items out of the results, at most 50 of them, so a draft under a parent can leave out " + "that parent and its children without changing how rare the text's words are; " + "keywords_only skips meaning search.",
|
|
15619
15620
|
arguments: (z) => ({
|
|
15620
15621
|
project: z.string().describe("Project key to search within, such as CORE."),
|
|
15621
15622
|
title: z.string({ max: ISSUE_TITLE_MAX }).describe("Optional title to check, as an issue title; at most 1,000 characters.").optional(),
|
|
15622
15623
|
text: z.string().describe("Optional text to check, such as a draft spec or a question.").optional(),
|
|
15623
|
-
|
|
15624
|
+
exclude_issues: z.array(z.string()).describe("Optional issue keys in the project whose own items are left out of the results; at most 50.").optional(),
|
|
15624
15625
|
keywords_only: z.boolean().describe("Search by keywords only, skipping meaning search.").optional()
|
|
15625
15626
|
}),
|
|
15626
15627
|
validation: {
|
|
@@ -20543,11 +20544,19 @@ function relatedCheckLines(suggestions, context) {
|
|
|
20543
20544
|
lines.push(`A past decision may already answer this: ${suggestionLabel(decision)}, answered by ${decision.answered_by}${when} \u2192 ${absoluteHref(decision.href, context.configUrl)}`);
|
|
20544
20545
|
lines.push(...askLines(decision.ask));
|
|
20545
20546
|
}
|
|
20547
|
+
const likely = suggestions.likely_duplicate;
|
|
20548
|
+
if (likely !== undefined && likely !== null) {
|
|
20549
|
+
lines.push(`Likely duplicate: ${likely.key} [${likely.status}] ${likely.title} \u2192 ${absoluteHref(likely.href, context.configUrl)}`);
|
|
20550
|
+
lines.push("Read it. If it is the same work, fold this issue into it by the duplicate rule of skill://dispatch-first.");
|
|
20551
|
+
}
|
|
20552
|
+
if (suggestions.near_copy_check !== undefined && suggestions.near_copy_check !== "") {
|
|
20553
|
+
lines.push(`No near-copy check was made: ${suggestions.near_copy_check}.`);
|
|
20554
|
+
}
|
|
20546
20555
|
for (const match of suggestions.title_matches ?? []) {
|
|
20547
|
-
lines.push(`
|
|
20556
|
+
lines.push(`Near copy you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
|
|
20548
20557
|
}
|
|
20549
20558
|
} catch (error) {
|
|
20550
|
-
lines.push(`The check's past decision or
|
|
20559
|
+
lines.push(`The check's past decision or near copies could not be shown: ${messageFor(error)}`);
|
|
20551
20560
|
}
|
|
20552
20561
|
return lines;
|
|
20553
20562
|
}
|
|
@@ -22430,7 +22439,13 @@ function eventHead(event) {
|
|
|
22430
22439
|
return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
|
|
22431
22440
|
case "artifact.human_edited":
|
|
22432
22441
|
return textHead(humanEditHead(event.payload));
|
|
22433
|
-
case "issue.created":
|
|
22442
|
+
case "issue.created": {
|
|
22443
|
+
const forced = event.payload.forced;
|
|
22444
|
+
const head = `status ${event.payload.status}`;
|
|
22445
|
+
if (forced === undefined)
|
|
22446
|
+
return head;
|
|
22447
|
+
return forced.past.length === 0 ? `${head} \xB7 forced, the near-copy check did not answer` : `${head} \xB7 forced past ${forced.past.join(", ")}`;
|
|
22448
|
+
}
|
|
22434
22449
|
case "issue.updated":
|
|
22435
22450
|
case "issue.closed":
|
|
22436
22451
|
return `status ${event.payload.status}`;
|
|
@@ -22446,10 +22461,14 @@ function eventRef(event) {
|
|
|
22446
22461
|
const kind = event.type.startsWith("message.") ? "message" : event.type.startsWith("comment.") || event.type.startsWith("suggestion.") ? "comment" : event.type.startsWith("ask.") ? "ask" : undefined;
|
|
22447
22462
|
return kind === undefined ? undefined : `dispatch://${event.issue_key}/${kind}/${id}`;
|
|
22448
22463
|
}
|
|
22464
|
+
function eventActorText(actor) {
|
|
22465
|
+
const title = actor.kind === "session" ? actor.origin?.session_title : undefined;
|
|
22466
|
+
return title === undefined || title === "" ? actorText(actor) : `${actorText(actor)} (${title})`;
|
|
22467
|
+
}
|
|
22449
22468
|
function eventLine(event) {
|
|
22450
22469
|
const head = eventHead(event);
|
|
22451
22470
|
const ref = eventRef(event);
|
|
22452
|
-
return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${
|
|
22471
|
+
return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${eventActorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
|
|
22453
22472
|
}
|
|
22454
22473
|
function childrenSummary(issue) {
|
|
22455
22474
|
return [
|
|
@@ -23266,15 +23285,17 @@ async function executeDispatchTool(input) {
|
|
|
23266
23285
|
const candidates = duplicateCandidates(error);
|
|
23267
23286
|
return {
|
|
23268
23287
|
text: [
|
|
23269
|
-
`Not created: "${title}"
|
|
23288
|
+
`Not created: "${title}" is a near copy of an open issue.`,
|
|
23270
23289
|
...candidates.map((candidate) => {
|
|
23271
23290
|
const href = new URL(candidate.href, linkBase).toString();
|
|
23272
23291
|
return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
|
|
23273
23292
|
}),
|
|
23274
|
-
"
|
|
23293
|
+
"Read each issue above. If one is the same work, fold this into it by the duplicate rule of skill://dispatch-first.",
|
|
23294
|
+
"If none is, run dispatch issue again with --force, and write in the new issue's spec why it is not each issue named here."
|
|
23275
23295
|
].join(`
|
|
23276
23296
|
`),
|
|
23277
|
-
details: { duplicates: candidates }
|
|
23297
|
+
details: { duplicates: candidates },
|
|
23298
|
+
refused: true
|
|
23278
23299
|
};
|
|
23279
23300
|
}
|
|
23280
23301
|
}
|
|
@@ -23442,12 +23463,12 @@ async function executeDispatchTool(input) {
|
|
|
23442
23463
|
const project = stringArg(args, "project");
|
|
23443
23464
|
const title = optionalString(args, "title");
|
|
23444
23465
|
const text = optionalString(args, "text");
|
|
23445
|
-
const
|
|
23466
|
+
const excludeIssues = optionalStringArray(args, "exclude_issues");
|
|
23446
23467
|
const keywordsOnly = optionalBoolean(args, "keywords_only");
|
|
23447
23468
|
const suggestions = await client.related(project, {
|
|
23448
23469
|
...title === undefined ? {} : { title },
|
|
23449
23470
|
...text === undefined ? {} : { text },
|
|
23450
|
-
...
|
|
23471
|
+
...excludeIssues === undefined ? {} : { exclude_issues: excludeIssues },
|
|
23451
23472
|
...keywordsOnly === undefined ? {} : { keywords_only: keywordsOnly }
|
|
23452
23473
|
});
|
|
23453
23474
|
return {
|
|
@@ -28293,7 +28314,7 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
28293
28314
|
if (parsed.kind === "refused") {
|
|
28294
28315
|
return parsed.tool === undefined ? refuse([`dispatch: ${parsed.problems.join(`
|
|
28295
28316
|
`)}`, "Run dispatch --help."].join(`
|
|
28296
|
-
`), USAGE) : refuse(new ToolInputError(parsed.tool, parsed.problems).message,
|
|
28317
|
+
`), USAGE) : refuse(new ToolInputError(parsed.tool, parsed.problems).message, USAGE, parsed.tool);
|
|
28297
28318
|
}
|
|
28298
28319
|
const hostName = env.DISPATCH_HOST ?? (env.CLAUDE_CODE_CHILD_SESSION === "1" ? "claude" : undefined);
|
|
28299
28320
|
const host = HOSTS.find((name) => name === hostName);
|
|
@@ -28321,7 +28342,7 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
28321
28342
|
serverUrls: settings?.enabled === true ? dispatchServerUrls(settings) : []
|
|
28322
28343
|
});
|
|
28323
28344
|
if (problems.length > 0) {
|
|
28324
|
-
return refuse(new ToolInputError(parsed.tool, problems).message,
|
|
28345
|
+
return refuse(new ToolInputError(parsed.tool, problems).message, USAGE, parsed.tool);
|
|
28325
28346
|
}
|
|
28326
28347
|
print(JSON.stringify(parsed.args, null, 2));
|
|
28327
28348
|
return OK;
|
|
@@ -28458,7 +28479,7 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
28458
28479
|
notes.push(followNoticeText(followed, await subscriptions()));
|
|
28459
28480
|
}
|
|
28460
28481
|
entry = { tool, details: result.details };
|
|
28461
|
-
code = OK;
|
|
28482
|
+
code = result.refused === true ? REFUSED : OK;
|
|
28462
28483
|
} catch (error) {
|
|
28463
28484
|
text = messageFor(error);
|
|
28464
28485
|
if (error instanceof DispatchWriteLandedError) {
|
|
@@ -28466,7 +28487,7 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
28466
28487
|
code = OK;
|
|
28467
28488
|
} else {
|
|
28468
28489
|
entry = { tool, error: text };
|
|
28469
|
-
code = REFUSED;
|
|
28490
|
+
code = error instanceof ToolInputError ? USAGE : REFUSED;
|
|
28470
28491
|
}
|
|
28471
28492
|
}
|
|
28472
28493
|
try {
|
package/dist/envoy-channel.js
CHANGED
|
@@ -36311,7 +36311,8 @@ var IssueEventPayloadSchema = object({
|
|
|
36311
36311
|
route: string2().nullish(),
|
|
36312
36312
|
changed: array(string2()).optional(),
|
|
36313
36313
|
ancestors: array(string2()).optional(),
|
|
36314
|
-
thread_logins: array(string2()).optional()
|
|
36314
|
+
thread_logins: array(string2()).optional(),
|
|
36315
|
+
forced: object({ past: array(string2()) }).optional()
|
|
36315
36316
|
});
|
|
36316
36317
|
var ArtifactCreatedEventPayloadSchema = object({
|
|
36317
36318
|
artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
|
|
@@ -37264,12 +37265,12 @@ var dispatchToolSpecs = [
|
|
|
37264
37265
|
{
|
|
37265
37266
|
name: "dispatch_related",
|
|
37266
37267
|
example: { project: "CORE", title: "Retry failed webhook deliveries" },
|
|
37267
|
-
description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead.
|
|
37268
|
+
description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issues leaves each named issue's own " + "items out of the results, at most 50 of them, so a draft under a parent can leave out " + "that parent and its children without changing how rare the text's words are; " + "keywords_only skips meaning search.",
|
|
37268
37269
|
arguments: (z) => ({
|
|
37269
37270
|
project: z.string().describe("Project key to search within, such as CORE."),
|
|
37270
37271
|
title: z.string({ max: ISSUE_TITLE_MAX }).describe("Optional title to check, as an issue title; at most 1,000 characters.").optional(),
|
|
37271
37272
|
text: z.string().describe("Optional text to check, such as a draft spec or a question.").optional(),
|
|
37272
|
-
|
|
37273
|
+
exclude_issues: z.array(z.string()).describe("Optional issue keys in the project whose own items are left out of the results; at most 50.").optional(),
|
|
37273
37274
|
keywords_only: z.boolean().describe("Search by keywords only, skipping meaning search.").optional()
|
|
37274
37275
|
}),
|
|
37275
37276
|
validation: {
|
|
@@ -47325,11 +47326,19 @@ function relatedCheckLines(suggestions, context) {
|
|
|
47325
47326
|
lines.push(`A past decision may already answer this: ${suggestionLabel(decision)}, answered by ${decision.answered_by}${when} \u2192 ${absoluteHref(decision.href, context.configUrl)}`);
|
|
47326
47327
|
lines.push(...askLines(decision.ask));
|
|
47327
47328
|
}
|
|
47329
|
+
const likely = suggestions.likely_duplicate;
|
|
47330
|
+
if (likely !== undefined && likely !== null) {
|
|
47331
|
+
lines.push(`Likely duplicate: ${likely.key} [${likely.status}] ${likely.title} \u2192 ${absoluteHref(likely.href, context.configUrl)}`);
|
|
47332
|
+
lines.push("Read it. If it is the same work, fold this issue into it by the duplicate rule of skill://dispatch-first.");
|
|
47333
|
+
}
|
|
47334
|
+
if (suggestions.near_copy_check !== undefined && suggestions.near_copy_check !== "") {
|
|
47335
|
+
lines.push(`No near-copy check was made: ${suggestions.near_copy_check}.`);
|
|
47336
|
+
}
|
|
47328
47337
|
for (const match of suggestions.title_matches ?? []) {
|
|
47329
|
-
lines.push(`
|
|
47338
|
+
lines.push(`Near copy you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
|
|
47330
47339
|
}
|
|
47331
47340
|
} catch (error) {
|
|
47332
|
-
lines.push(`The check's past decision or
|
|
47341
|
+
lines.push(`The check's past decision or near copies could not be shown: ${messageFor(error)}`);
|
|
47333
47342
|
}
|
|
47334
47343
|
return lines;
|
|
47335
47344
|
}
|
|
@@ -49180,7 +49189,13 @@ function eventHead(event) {
|
|
|
49180
49189
|
return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
|
|
49181
49190
|
case "artifact.human_edited":
|
|
49182
49191
|
return textHead(humanEditHead(event.payload));
|
|
49183
|
-
case "issue.created":
|
|
49192
|
+
case "issue.created": {
|
|
49193
|
+
const forced = event.payload.forced;
|
|
49194
|
+
const head = `status ${event.payload.status}`;
|
|
49195
|
+
if (forced === undefined)
|
|
49196
|
+
return head;
|
|
49197
|
+
return forced.past.length === 0 ? `${head} \xB7 forced, the near-copy check did not answer` : `${head} \xB7 forced past ${forced.past.join(", ")}`;
|
|
49198
|
+
}
|
|
49184
49199
|
case "issue.updated":
|
|
49185
49200
|
case "issue.closed":
|
|
49186
49201
|
return `status ${event.payload.status}`;
|
|
@@ -49196,10 +49211,14 @@ function eventRef(event) {
|
|
|
49196
49211
|
const kind = event.type.startsWith("message.") ? "message" : event.type.startsWith("comment.") || event.type.startsWith("suggestion.") ? "comment" : event.type.startsWith("ask.") ? "ask" : undefined;
|
|
49197
49212
|
return kind === undefined ? undefined : `dispatch://${event.issue_key}/${kind}/${id}`;
|
|
49198
49213
|
}
|
|
49214
|
+
function eventActorText(actor) {
|
|
49215
|
+
const title = actor.kind === "session" ? actor.origin?.session_title : undefined;
|
|
49216
|
+
return title === undefined || title === "" ? actorText(actor) : `${actorText(actor)} (${title})`;
|
|
49217
|
+
}
|
|
49199
49218
|
function eventLine(event) {
|
|
49200
49219
|
const head = eventHead(event);
|
|
49201
49220
|
const ref = eventRef(event);
|
|
49202
|
-
return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${
|
|
49221
|
+
return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${eventActorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
|
|
49203
49222
|
}
|
|
49204
49223
|
function childrenSummary(issue) {
|
|
49205
49224
|
return [
|
|
@@ -50016,15 +50035,17 @@ async function executeDispatchTool(input) {
|
|
|
50016
50035
|
const candidates = duplicateCandidates(error);
|
|
50017
50036
|
return {
|
|
50018
50037
|
text: [
|
|
50019
|
-
`Not created: "${title}"
|
|
50038
|
+
`Not created: "${title}" is a near copy of an open issue.`,
|
|
50020
50039
|
...candidates.map((candidate) => {
|
|
50021
50040
|
const href = new URL(candidate.href, linkBase).toString();
|
|
50022
50041
|
return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
|
|
50023
50042
|
}),
|
|
50024
|
-
"
|
|
50043
|
+
"Read each issue above. If one is the same work, fold this into it by the duplicate rule of skill://dispatch-first.",
|
|
50044
|
+
"If none is, run dispatch issue again with --force, and write in the new issue's spec why it is not each issue named here."
|
|
50025
50045
|
].join(`
|
|
50026
50046
|
`),
|
|
50027
|
-
details: { duplicates: candidates }
|
|
50047
|
+
details: { duplicates: candidates },
|
|
50048
|
+
refused: true
|
|
50028
50049
|
};
|
|
50029
50050
|
}
|
|
50030
50051
|
}
|
|
@@ -50192,12 +50213,12 @@ async function executeDispatchTool(input) {
|
|
|
50192
50213
|
const project = stringArg(args, "project");
|
|
50193
50214
|
const title = optionalString(args, "title");
|
|
50194
50215
|
const text = optionalString(args, "text");
|
|
50195
|
-
const
|
|
50216
|
+
const excludeIssues = optionalStringArray(args, "exclude_issues");
|
|
50196
50217
|
const keywordsOnly = optionalBoolean(args, "keywords_only");
|
|
50197
50218
|
const suggestions = await client.related(project, {
|
|
50198
50219
|
...title === undefined ? {} : { title },
|
|
50199
50220
|
...text === undefined ? {} : { text },
|
|
50200
|
-
...
|
|
50221
|
+
...excludeIssues === undefined ? {} : { exclude_issues: excludeIssues },
|
|
50201
50222
|
...keywordsOnly === undefined ? {} : { keywords_only: keywordsOnly }
|
|
50202
50223
|
});
|
|
50203
50224
|
return {
|
|
@@ -52110,7 +52131,7 @@ var legionPendingStatusWrite = object({
|
|
|
52110
52131
|
var legionStrandedClaim = object({
|
|
52111
52132
|
issue: issueKey,
|
|
52112
52133
|
role: _enum2(LEGION_ROLES),
|
|
52113
|
-
state: _enum2(["suspended", "failed"])
|
|
52134
|
+
state: _enum2(["suspended", "failed", "retired"])
|
|
52114
52135
|
});
|
|
52115
52136
|
var legionHeldOutboxRow = object({
|
|
52116
52137
|
id: number2().int().positive(),
|
|
@@ -52344,7 +52365,10 @@ var legionDeliveryView = object({
|
|
|
52344
52365
|
queuedAt: timestamp,
|
|
52345
52366
|
deliveredAt: timestamp.optional(),
|
|
52346
52367
|
confirmedAt: timestamp.optional(),
|
|
52347
|
-
interrupted: literal(true).optional()
|
|
52368
|
+
interrupted: literal(true).optional(),
|
|
52369
|
+
sendFailures: number2().int().nonnegative().optional(),
|
|
52370
|
+
lastSendFailureAt: timestamp.optional(),
|
|
52371
|
+
lastSendError: nonEmptyString.optional()
|
|
52348
52372
|
});
|
|
52349
52373
|
var LegionOperatorClaimResponse = object({
|
|
52350
52374
|
token: nonEmptyString,
|
|
@@ -55155,9 +55179,9 @@ var import_nats2 = __toESM(require_mod4(), 1);
|
|
|
55155
55179
|
|
|
55156
55180
|
// package.json
|
|
55157
55181
|
var name = "@sjawhar/claude-envoy";
|
|
55158
|
-
var version2 = "1.
|
|
55182
|
+
var version2 = "1.14.1";
|
|
55159
55183
|
var legion = {
|
|
55160
|
-
daemonApiVersion:
|
|
55184
|
+
daemonApiVersion: 56
|
|
55161
55185
|
};
|
|
55162
55186
|
|
|
55163
55187
|
// src/channel-forwarder.ts
|
package/dist/session-hook.js
CHANGED
|
@@ -13661,7 +13661,8 @@ var IssueEventPayloadSchema = object({
|
|
|
13661
13661
|
route: string2().nullish(),
|
|
13662
13662
|
changed: array(string2()).optional(),
|
|
13663
13663
|
ancestors: array(string2()).optional(),
|
|
13664
|
-
thread_logins: array(string2()).optional()
|
|
13664
|
+
thread_logins: array(string2()).optional(),
|
|
13665
|
+
forced: object({ past: array(string2()) }).optional()
|
|
13665
13666
|
});
|
|
13666
13667
|
var ArtifactCreatedEventPayloadSchema = object({
|
|
13667
13668
|
artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
|
|
@@ -14614,12 +14615,12 @@ var dispatchToolSpecs = [
|
|
|
14614
14615
|
{
|
|
14615
14616
|
name: "dispatch_related",
|
|
14616
14617
|
example: { project: "CORE", title: "Retry failed webhook deliveries" },
|
|
14617
|
-
description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead.
|
|
14618
|
+
description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issues leaves each named issue's own " + "items out of the results, at most 50 of them, so a draft under a parent can leave out " + "that parent and its children without changing how rare the text's words are; " + "keywords_only skips meaning search.",
|
|
14618
14619
|
arguments: (z) => ({
|
|
14619
14620
|
project: z.string().describe("Project key to search within, such as CORE."),
|
|
14620
14621
|
title: z.string({ max: ISSUE_TITLE_MAX }).describe("Optional title to check, as an issue title; at most 1,000 characters.").optional(),
|
|
14621
14622
|
text: z.string().describe("Optional text to check, such as a draft spec or a question.").optional(),
|
|
14622
|
-
|
|
14623
|
+
exclude_issues: z.array(z.string()).describe("Optional issue keys in the project whose own items are left out of the results; at most 50.").optional(),
|
|
14623
14624
|
keywords_only: z.boolean().describe("Search by keywords only, skipping meaning search.").optional()
|
|
14624
14625
|
}),
|
|
14625
14626
|
validation: {
|
|
@@ -19547,11 +19548,19 @@ function relatedCheckLines(suggestions, context) {
|
|
|
19547
19548
|
lines.push(`A past decision may already answer this: ${suggestionLabel(decision)}, answered by ${decision.answered_by}${when} \u2192 ${absoluteHref(decision.href, context.configUrl)}`);
|
|
19548
19549
|
lines.push(...askLines(decision.ask));
|
|
19549
19550
|
}
|
|
19551
|
+
const likely = suggestions.likely_duplicate;
|
|
19552
|
+
if (likely !== undefined && likely !== null) {
|
|
19553
|
+
lines.push(`Likely duplicate: ${likely.key} [${likely.status}] ${likely.title} \u2192 ${absoluteHref(likely.href, context.configUrl)}`);
|
|
19554
|
+
lines.push("Read it. If it is the same work, fold this issue into it by the duplicate rule of skill://dispatch-first.");
|
|
19555
|
+
}
|
|
19556
|
+
if (suggestions.near_copy_check !== undefined && suggestions.near_copy_check !== "") {
|
|
19557
|
+
lines.push(`No near-copy check was made: ${suggestions.near_copy_check}.`);
|
|
19558
|
+
}
|
|
19550
19559
|
for (const match of suggestions.title_matches ?? []) {
|
|
19551
|
-
lines.push(`
|
|
19560
|
+
lines.push(`Near copy you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
|
|
19552
19561
|
}
|
|
19553
19562
|
} catch (error) {
|
|
19554
|
-
lines.push(`The check's past decision or
|
|
19563
|
+
lines.push(`The check's past decision or near copies could not be shown: ${messageFor(error)}`);
|
|
19555
19564
|
}
|
|
19556
19565
|
return lines;
|
|
19557
19566
|
}
|
|
@@ -21434,7 +21443,13 @@ function eventHead(event) {
|
|
|
21434
21443
|
return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
|
|
21435
21444
|
case "artifact.human_edited":
|
|
21436
21445
|
return textHead(humanEditHead(event.payload));
|
|
21437
|
-
case "issue.created":
|
|
21446
|
+
case "issue.created": {
|
|
21447
|
+
const forced = event.payload.forced;
|
|
21448
|
+
const head = `status ${event.payload.status}`;
|
|
21449
|
+
if (forced === undefined)
|
|
21450
|
+
return head;
|
|
21451
|
+
return forced.past.length === 0 ? `${head} \xB7 forced, the near-copy check did not answer` : `${head} \xB7 forced past ${forced.past.join(", ")}`;
|
|
21452
|
+
}
|
|
21438
21453
|
case "issue.updated":
|
|
21439
21454
|
case "issue.closed":
|
|
21440
21455
|
return `status ${event.payload.status}`;
|
|
@@ -21450,10 +21465,14 @@ function eventRef(event) {
|
|
|
21450
21465
|
const kind = event.type.startsWith("message.") ? "message" : event.type.startsWith("comment.") || event.type.startsWith("suggestion.") ? "comment" : event.type.startsWith("ask.") ? "ask" : undefined;
|
|
21451
21466
|
return kind === undefined ? undefined : `dispatch://${event.issue_key}/${kind}/${id}`;
|
|
21452
21467
|
}
|
|
21468
|
+
function eventActorText(actor) {
|
|
21469
|
+
const title = actor.kind === "session" ? actor.origin?.session_title : undefined;
|
|
21470
|
+
return title === undefined || title === "" ? actorText(actor) : `${actorText(actor)} (${title})`;
|
|
21471
|
+
}
|
|
21453
21472
|
function eventLine(event) {
|
|
21454
21473
|
const head = eventHead(event);
|
|
21455
21474
|
const ref = eventRef(event);
|
|
21456
|
-
return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${
|
|
21475
|
+
return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${eventActorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
|
|
21457
21476
|
}
|
|
21458
21477
|
function childrenSummary(issue) {
|
|
21459
21478
|
return [
|
|
@@ -22270,15 +22289,17 @@ async function executeDispatchTool(input) {
|
|
|
22270
22289
|
const candidates = duplicateCandidates(error);
|
|
22271
22290
|
return {
|
|
22272
22291
|
text: [
|
|
22273
|
-
`Not created: "${title}"
|
|
22292
|
+
`Not created: "${title}" is a near copy of an open issue.`,
|
|
22274
22293
|
...candidates.map((candidate) => {
|
|
22275
22294
|
const href = new URL(candidate.href, linkBase).toString();
|
|
22276
22295
|
return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
|
|
22277
22296
|
}),
|
|
22278
|
-
"
|
|
22297
|
+
"Read each issue above. If one is the same work, fold this into it by the duplicate rule of skill://dispatch-first.",
|
|
22298
|
+
"If none is, run dispatch issue again with --force, and write in the new issue's spec why it is not each issue named here."
|
|
22279
22299
|
].join(`
|
|
22280
22300
|
`),
|
|
22281
|
-
details: { duplicates: candidates }
|
|
22301
|
+
details: { duplicates: candidates },
|
|
22302
|
+
refused: true
|
|
22282
22303
|
};
|
|
22283
22304
|
}
|
|
22284
22305
|
}
|
|
@@ -22446,12 +22467,12 @@ async function executeDispatchTool(input) {
|
|
|
22446
22467
|
const project = stringArg(args, "project");
|
|
22447
22468
|
const title = optionalString(args, "title");
|
|
22448
22469
|
const text = optionalString(args, "text");
|
|
22449
|
-
const
|
|
22470
|
+
const excludeIssues = optionalStringArray(args, "exclude_issues");
|
|
22450
22471
|
const keywordsOnly = optionalBoolean(args, "keywords_only");
|
|
22451
22472
|
const suggestions = await client.related(project, {
|
|
22452
22473
|
...title === undefined ? {} : { title },
|
|
22453
22474
|
...text === undefined ? {} : { text },
|
|
22454
|
-
...
|
|
22475
|
+
...excludeIssues === undefined ? {} : { exclude_issues: excludeIssues },
|
|
22455
22476
|
...keywordsOnly === undefined ? {} : { keywords_only: keywordsOnly }
|
|
22456
22477
|
});
|
|
22457
22478
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: legion-architect
|
|
3
|
-
description: "Use when you are a Legion root or sub-architect: decomposing children, gating the root spec, releasing
|
|
3
|
+
description: "Use when you are a Legion root or sub-architect: decomposing children, gating the root spec, releasing children, accepting a reviewed head, or handling a delivered wake or a worker's escalation. Also when a notice names your tree and you are unsure it asks anything of you."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Legion architect
|
|
@@ -24,26 +24,28 @@ You own one issue tree from its first decision to the merge that closes it. The
|
|
|
24
24
|
## 1. Decompose or adopt
|
|
25
25
|
|
|
26
26
|
- Read a child's handoff, create children, split across repositories or attach architecture components as `skill://legion-architect/references/decomposition.md` says.
|
|
27
|
-
- Adopt children a human created and release them
|
|
27
|
+
- Adopt children a human created and release them at once. Never replace or re-decompose them.
|
|
28
28
|
- Choose a single-issue tree only when one pull request in one repository can deliver every criterion.
|
|
29
|
-
- A child issue
|
|
29
|
+
- A child issue needs a repository or a deploy order of its own: its code is in a repository no open pull request of this tree is in, or it must be applied on its own before other work of the tree can build on it. A repeated change — the same edit per program, directory or instance — is one child, never one per instance. Never use a child to track a to-do, a fix found in review, a proof of the root's acceptance, or a part of your own work: do it in this tree. Each child costs a workspace, an admission wait and a review loop. Before any `dispatch issue` that creates a child, and before `release_children`, run the red-team check in `skill://legion-architect/references/decomposition.md`.
|
|
30
|
+
- Work in a repository this tree already has a pull request in, which waits on a sibling's merge, is no child and is not parked in `backlog`: write it into the spec of the issue whose pull request carries it — the parent's, or the sibling's when the parent's pull request is in another repository — so one pull request lands it in order.
|
|
31
|
+
- A child that duplicates another issue and is not yet released is never released: fold it by the duplicate rule of `skill://dispatch-first`. The daemon records a child at its release and re-asserts a status only on an issue it has recorded, so that close stands. A released duplicate goes out with `park_child` instead.
|
|
30
32
|
- Every child names the running system its criteria are proven on (the command, service or page the tester drives) and the repository skill that drives it. If the repository cannot exercise a criterion end to end, building that path is work of this tree.
|
|
31
|
-
- The issue's primary document is the root spec: extend it in place, because a second "spec" artifact replaces the human's document. Mark each requirement of it as a `requirement` block, and write the issue's Verification document (`verification.md`) beside it: one `check` block per thing that must be proved, each with `serves` naming the requirements it serves, its `surface` (`browser`, `cli` or `api`), its `run`, its `when` (`before_merge`, `after_merge` or `default_branch`) and its `negative_control`. The checks are the gate every later role reads: the implementer and the tester record a result against each, and the reviewer approves only once every `before_merge` check of the issue has a passing result current at the head. The
|
|
33
|
+
- The issue's primary document is the root spec: extend it in place, because a second "spec" artifact replaces the human's document. Mark each requirement of it as a `requirement` block, and write the issue's Verification document (`verification.md`) beside it: one `check` block per thing that must be proved, each with `serves` naming the requirements it serves, its `surface` (`browser`, `cli` or `api`), its `run`, its `when` (`before_merge`, `after_merge` or `default_branch`) and its `negative_control`. The checks are the gate every later role reads: the implementer and the tester record a result against each, and the reviewer approves only once every `before_merge` check of the issue has a passing result current at the head. The decomposition's order and the plan's tasks go in the children and the planner's `plan` handoff, not the root spec.
|
|
32
34
|
- Read `skill://legion-architect/references/design-gate.md` before you write the root spec and before any edit to it after approval, because any new version closes the gate. It also names the design gate steps your `Legion addressing` line declares, the document each step records, and says that a sub-architect runs none.
|
|
33
35
|
- File every child of your decomposition before `dispatch request-approval`, and before `register_gate` where your line gates no step: the gate's opening starts your root's own first phase unless Dispatch shows an open child under it, so a child filed after it opens leaves the root working beside its children.
|
|
34
|
-
- A root architect then ends its turn and releases no
|
|
36
|
+
- A root architect then ends its turn and releases no child until every gated step is open: a delivered wake shows `design-approved` on the root, and under more than one step `gatedSteps` in your record (the `legion` tool's `read_record`) shows each step `approved`. The daemon starts no phase in the tree before that. A root that waited on a person's answer gets no `design-approved` notice: a resume task saying "this resume is your design-approved wake" is that wake. A sub-architect waits for none and releases its children with `release_children`.
|
|
35
37
|
|
|
36
38
|
## 2. Children in flight
|
|
37
39
|
|
|
38
|
-
- Set a child's dependencies in one call naming every issue it waits on: `dispatch issue-update --issue <child> --blocked-by <a> --blocked-by <b>`, or `--blocked-by` on `dispatch issue` at creation. The call replaces the child's whole list, so read its existing links first (`dispatch read`). Release it with the rest: `legion({ op: "release_children", issues: ["LEGION-41", "LEGION-42"] })`. Legion holds a released child that waits on an issue not `done` and starts it itself at its last blocker's close. A child that builds on another repository's merged change is not held this way: release it as `skill://legion-architect/references/decomposition.md` "Across repositories" says.
|
|
39
|
-
- Release every child as soon as it is filed,
|
|
40
|
+
- Set a child's dependencies in one call naming every issue it waits on: `dispatch issue-update --issue <child> --blocked-by <a> --blocked-by <b>`, or `--blocked-by` on `dispatch issue` at creation. The call replaces the child's whole list, so read its existing links first (`dispatch read`). Release it with the rest: `legion({ op: "release_children", issues: ["LEGION-41", "LEGION-42"] })`. Legion holds a released child that waits on an issue not `done` and starts it itself at its last blocker's close. A child that builds on another repository's merged change is not held this way: release it as `skill://legion-architect/references/decomposition.md` "Across repositories" says. No child of this tree waits on a sibling's merge in a repository this tree already has a pull request in: that work rides in the pull request that carries it (section 1).
|
|
41
|
+
- Release every child as soon as it is filed, each with `--blocked-by` the issues it waits on, and never hold a release back for a close: a released child, held or running, keeps the root's close waiting, but once the root's own pull request has merged the root closes the moment its last released child leaves, and a child released after that finds no live tree and runs as an orphan root of its own.
|
|
40
42
|
- On each `child-closed`, read the child's handoff. Edit the spec of each open child the result changes, and take out each child it makes unneeded with `park_child` and say why with `dispatch comment`.
|
|
41
43
|
- Before you park a child for good (one made unneeded, or parked so its tree can finish), set the `--blocked-by` list of each sibling that waits on it again without it (one call with the rest of its list, or `--clear-blocked-by` when it was the only one), or park that sibling too: a parked child is stopped, not done, and its dependents stay held with no notice. A park followed by `rerun_child` keeps its dependents' links: they wait through the rerun and start when the rerun's merge closes the child.
|
|
42
44
|
- A released child holds an admission slot of its own while a worker of it has work, and waits in the admission line for one, so a tree of four working issues holds four of the cap's issues. It never becomes a root while your tree is live.
|
|
43
45
|
|
|
44
46
|
## 3. Children complete
|
|
45
47
|
|
|
46
|
-
Your issue's own first phase does not start while Dispatch shows an open child under it, one you filed and have not released included: the daemon tells you once per issue and run of its tree with a `children-first` notice naming each open child and the Dispatch status that holds it, and starts the parent at its next admission slot once the last of them has left the workflow, closed, gone to `icebox`, or been moved out from under it, to another parent or to none. So file and release
|
|
48
|
+
Your issue's own first phase does not start while Dispatch shows an open child under it, one you filed and have not released included: the daemon tells you once per issue and run of its tree with a `children-first` notice naming each open child and the Dispatch status that holds it, and starts the parent at its next admission slot once the last of them has left the workflow, closed, gone to `icebox`, or been moved out from under it, to another parent or to none. So file and release every child at once, and park what should not run. No notice marks the last child's close. A child filed after your issue started runs beside it, and the daemon refuses the parent's `sign_off` while that child is in the workflow: run the parent's last round (section 4) so its head carries the child's merge.
|
|
47
49
|
|
|
48
50
|
## 4. Integration verification
|
|
49
51
|
|
|
@@ -64,7 +64,7 @@ parent's check: a child's pull request never makes a parent check's result curre
|
|
|
64
64
|
that carries part of a parent's requirement proves it with a check on the child's own
|
|
65
65
|
Verification document whose `serves` names that requirement. Nothing in the tree builds on the parent's own merge, because that
|
|
66
66
|
merge comes last: a child that waits for it waits forever, so work another child builds on is a
|
|
67
|
-
|
|
67
|
+
child of its own, in a repository of its own or with a deploy of its own. While the approved head does not carry those merges, or the result of
|
|
68
68
|
a check in another repository is older than the close of a child it needs, leave
|
|
69
69
|
the issue in `accepting` until each of those children has closed (`child-closed`) or been parked
|
|
70
70
|
for good (`child-status`). Move a parked child's criterion to other work first; never downgrade
|
|
@@ -204,7 +204,10 @@ child that never started is handed over too, carrying no run, whatever held it a
|
|
|
204
204
|
waits in the line for a slot and for any unfinished blocker, and its architect starts as a fresh
|
|
205
205
|
root and registers its own spec. Only a child that finished, was parked or was never released
|
|
206
206
|
stays. The child stays its own tree; reopening the old root does not take it back. Park a child
|
|
207
|
-
with `park_child` before then when its work should stop with the root.
|
|
207
|
+
with `park_child` before then when its work should stop with the root. A root that is parked
|
|
208
|
+
rather than closed hands over only a child at `awaiting_merge` whose pull request is still open,
|
|
209
|
+
since the daemon re-admits no parked root and only that child's own new READY arms the merge the
|
|
210
|
+
park disarmed; park that child too when its merge should wait with the root.
|
|
208
211
|
|
|
209
212
|
If you are that child's architect:
|
|
210
213
|
|
|
@@ -13,7 +13,7 @@ and an earlier one with `--version <n>`. The child's Conversation lists every ve
|
|
|
13
13
|
|
|
14
14
|
## Adopting children
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Release every adopted child at once with
|
|
17
17
|
`legion({ op: "release_children", issues: ["LEGION-41", "LEGION-42"] })`. The daemon runs nothing on
|
|
18
18
|
a child until it is released. Once it is released, the daemon moves it to `todo` and, while the
|
|
19
19
|
root's design gate is open, starts its first phase itself at the child's own admission slot, and
|
|
@@ -44,18 +44,25 @@ EOF
|
|
|
44
44
|
that each child should not exist. Name every child that is a to-do, a fix found in review, a
|
|
45
45
|
proof of the root's acceptance, or a part of the architect's own work, and say where it folds:
|
|
46
46
|
the root's pull request or an open child's."* `wait` for its verdict, since it runs as a job of
|
|
47
|
-
its own. A child survives only when it
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
its own. A child survives it only when it needs a repository or a deploy order of its own: its
|
|
48
|
+
code is in a repository no open pull request of this tree is in, or it must be applied on its
|
|
49
|
+
own before other work of the tree can build on it. A repeated change — the same edit per
|
|
50
|
+
program, directory or instance of this tree's own work — is one child, never one per instance.
|
|
51
|
+
Work in a repository this tree already has a pull request in, which waits on a sibling's merge,
|
|
52
|
+
is no child and is not parked in `backlog`: write it into the spec of the issue whose pull
|
|
53
|
+
request carries it — the parent's, or the sibling's when the parent's pull request is in another
|
|
54
|
+
repository — so one pull request lands it in order, and a deletion of what this tree replaced
|
|
55
|
+
rides in the pull request of the last change it waits on the same way.
|
|
56
|
+
Post the verdict, with the folds you made, as one `dispatch comment` on your own issue (the
|
|
57
|
+
root, in a root session). Its first line is exactly `Red-team verdict for <root key>:`, with
|
|
58
|
+
the root's own key in place of the placeholder, so a reader can find it among the issue's
|
|
59
|
+
comments; the controller's weekly audit matches that line, and counts no child filed before
|
|
60
|
+
the release that first asked for it. The architect who wrote the list reads every child on it
|
|
61
|
+
as needed, and the comment stops a later session filing the same children again.
|
|
62
|
+
- A child that duplicates another issue and is not yet released is never released: fold it by the
|
|
63
|
+
duplicate rule of `skill://dispatch-first`. The daemon records a child at its release and
|
|
64
|
+
re-asserts a status only on an issue it has recorded, so that close stands. A released duplicate
|
|
65
|
+
goes out with `park_child` instead.
|
|
59
66
|
- Then paste each survivor's whole spec into a fresh `task(agent="plan-gap-analyst")` to check it
|
|
60
67
|
against `skill://dispatch-writing-specs`, `wait` for its result, and fix what it flags: your own
|
|
61
68
|
reread misses the terms
|
|
@@ -70,11 +77,13 @@ EOF
|
|
|
70
77
|
- File every child before you request the root spec's approval, and before `register_gate` under
|
|
71
78
|
`gates.design: off`: the gate's opening starts the root's own first phase unless Dispatch shows
|
|
72
79
|
an open child under it, so a child filed after that leaves the root working beside its children.
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
released child until its last blocker closes, and a child is inert only until it
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
- Give a child `--blocked-by` only for an issue outside this tree's own repositories, or one whose
|
|
81
|
+
deploy another child builds on, then release every child at once with `release_children`:
|
|
82
|
+
Legion holds a released child until its last blocker closes, and a child is inert only until it
|
|
83
|
+
is released. No child waits on a sibling's merge in a repository this tree already has a pull
|
|
84
|
+
request in; that work is written into the spec of the issue whose pull request carries it.
|
|
85
|
+
Each issue of the tree whose worker has work holds an admission slot of its own, so four
|
|
86
|
+
working children take four of the deployment's cap.
|
|
78
87
|
- Never release a child `--blocked-by` its own parent: the parent now merges last, so that child
|
|
79
88
|
would wait forever.
|
|
80
89
|
- Do not pass `--assignee`, because the default keeps the tree's questions in one Inbox: under the
|
|
@@ -174,7 +174,7 @@ tree is at work, the root returns its admission slot and waits on its design gat
|
|
|
174
174
|
approval or changes request delivers your next task, and so does a newer spec version once
|
|
175
175
|
Dispatch shows the request back on your turn. While the root waits on a question instead, the
|
|
176
176
|
human's approval or changes request of the spec answers that wait too, and the task says how the
|
|
177
|
-
spec stands. Release no
|
|
177
|
+
spec stands. Release no child until every gated step is open: a delivered wake shows
|
|
178
178
|
`design-approved` on the root, or a task says its design gate is open, and under more than one
|
|
179
179
|
step `gatedSteps` in your record shows each one `approved`. The daemon sends a root waiting on a
|
|
180
180
|
person no `design-approved` notice, and "this resume is your design-approved wake" is that wake.
|
|
@@ -211,13 +211,13 @@ to the version it writes. Once they have answered:
|
|
|
211
211
|
|
|
212
212
|
- When the answer is the whole change, request nothing: the document's chip reads Approved at the
|
|
213
213
|
new version, its reviews dialog shows the review in their name, and the `design-approved` the
|
|
214
|
-
daemon sent opens nothing new. Carry on, and release no
|
|
214
|
+
daemon sent opens nothing new. Carry on, and release no child because of it.
|
|
215
215
|
- When the answer changes the Summary, a requirement, a check or the tree's scope, rewrite that text and
|
|
216
216
|
repeat steps 3 to 5 on the new version, with a one-sentence `--summary` naming what changed since
|
|
217
217
|
the earlier approval, then the reviews' sentence (the card shows that approval, `Approved at
|
|
218
218
|
version N on <date> by <who>.`, and its `Show diff` control carries the detail). The rewrite
|
|
219
219
|
closes the gate until they approve;
|
|
220
|
-
work the daemon started at the answer keeps running. Release no
|
|
220
|
+
work the daemon started at the answer keeps running. Release no further child until the `design-approved`
|
|
221
221
|
after the rewrite; work already in flight continues. Every merger's `READY` in the tree is
|
|
222
222
|
refused until a human approves the latest version.
|
|
223
223
|
|
|
@@ -238,7 +238,7 @@ are unchanged,
|
|
|
238
238
|
the plan is the record and the tree carries on; otherwise change the root spec and request
|
|
239
239
|
approval again.
|
|
240
240
|
|
|
241
|
-
Later
|
|
241
|
+
Later children and re-scoping open children toward the same requirements need no spec edit and no new
|
|
242
242
|
approval. A child issue's spec is never gated, and the child's own Verification document is
|
|
243
243
|
written by its architect.
|
|
244
244
|
|
|
@@ -18,16 +18,16 @@ and 7 (a conflict, the hand-over, the close) is in
|
|
|
18
18
|
|
|
19
19
|
| Wake | Procedure |
|
|
20
20
|
| --- | --- |
|
|
21
|
-
| `carried-gate-opened` | Only a root whose catch-up said `carried` gets it. Its `version` and `reason`: a human approved the old root's spec document at `version`, which opened the gate this root carried; the daemon has released any READY it refused and started any child waiting on it. It names no step: the carry brings the old root's gate records, and the catch-up reports every gated step the carry did not bring as unregistered, so register each of those on this issue's own document for that step (`skill://legion-architect/references/design-gate.md`). It is not `design-approved`: do not proceed to section 2 or release a
|
|
21
|
+
| `carried-gate-opened` | Only a root whose catch-up said `carried` gets it. Its `version` and `reason`: a human approved the old root's spec document at `version`, which opened the gate this root carried; the daemon has released any READY it refused and started any child waiting on it. It names no step: the carry brings the old root's gate records, and the catch-up reports every gated step the carry did not bring as unregistered, so register each of those on this issue's own document for that step (`skill://legion-architect/references/design-gate.md`). It is not `design-approved`: do not proceed to section 2 or release a child because of it, and start nothing. |
|
|
22
22
|
| `child-status` | A child of your tree left the workflow or re-entered it, or joined your tree from the admission queue; the reason names the child and its new status, or reads `<KEY> joined the tree of <ROOT> from the admission queue`. `todo` (a human's move, your `release_children`, or your `rerun_child`) means it runs again under your tree from planning, and the daemon starts it; you start nothing. A child held for a blocker starts the same way once nothing holds it, and the reason, naming it and its root, says what cleared the hold: its last blocker's close, naming the issues it waited on, now done, or a person or you taking its blocked-by links off; you start nothing for it either. `backlog`, `icebox` or `triage` (a human's move, or your `park_child`) means the daemon has suspended its workers, and it advances no further until it is set back to `todo`. Joined from the admission queue means a root that waited for a slot and never ran, which a person made a child of your tree or your `release_children` adopted, now runs in your tree with no architect of its own and takes an admission slot of its own when its work starts, from planning when it is `todo` and parked otherwise; one that waits on an unfinished blocked-by issue is held for that blocker like any other child, and starts when nothing holds it; you start nothing. A reason saying an issue is beneath an ancestor whose reviewed head the daemon has already accepted is a person's `todo` the daemon recorded and holds at `admitted`: nothing starts it until that ancestor closes, and a `request_backward_move` of the ancestor is what runs it under the ancestor now. What the rest of the tree does is your decision. When a park leaves no child that an issue in `accepting` waits on, run that issue's last round (`skill://legion-architect/references/acceptance.md`). |
|
|
23
23
|
| `child-closed` | Read the child completion and remaining open children. Re-scope open work, park obsolete work for good with `park_child` (first take its link off each sibling that waits on it, or park that sibling too, as section 2 says), and file any new child the result calls for, after the red-team check in `skill://legion-architect/references/decomposition.md`, and release it at once with `release_children`, blocked by what it builds on (section 2). When a worker waits on this child for a missing surface (see `phase-finished`), tell it to continue with `envoy_publish` to its role topic. The last child's close is not the end-game (section 3). When it was the last child an issue in `accepting` waits on, run that issue's last round (`skill://legion-architect/references/acceptance.md`). |
|
|
24
|
-
| `child-release-stalled` | Its `reason` names a child your `release_children` released that your tree has not recorded, its parent, and why, one of three: the parent's release has not reached the daemon after three tries; or GitHub did not answer the App installation read of a repository on the child's parent chain, and the
|
|
24
|
+
| `child-release-stalled` | Its `reason` names a child your `release_children` released that your tree has not recorded, its parent, and why, one of three: the parent's release has not reached the daemon after three tries; or GitHub did not answer the App installation read of a repository on the child's parent chain, and the release left the child unrecorded because a parent on that chain is not `todo`, or did not write the child `todo` because its parent is not recorded; or the daemon's recovery of such a repository, once GitHub answered, still left children unrecorded (one notice per tree lists them all, each with its parent). Read the parent's record with the `legion` tool's `read_record` (`issue` the parent); release the parent first when the `reason` says to, then run `release_children` for the child again once the parent is recorded in your tree, and tell the controller if it never is. |
|
|
25
25
|
| `child-not-recorded` | Its `reason` names a child your tree did not record and why: its repository resolves to none (neither it nor an ancestor names one, and the project has no default), or to one not mapped to the project, or to one the daemon leaves out for a missing GitHub App. The daemon said why on the child too. Set the child's repository to one of the project's (`dispatch issue-update --repo owner/name`), then release it again with `release_children`; tell the controller when the repository it needs is not served. |
|
|
26
26
|
| `children-first` | An issue of your tree starts no phase of its own while Dispatch shows an open child under it, a child you filed and have not released included. The reason names the parent and each open child with the Dispatch status that holds it, and each cycle it found (a child `blocked_by` an issue above it on its own parent chain, which only a person taking that link off ends: open a `dispatch ask`). It comes once per issue and run of its tree. Release every child that should run, and close, `park_child` or re-parent the rest; the daemon starts the parent's first phase at its next admission slot once the last open child has left the workflow. You start nothing. |
|
|
27
|
-
| `design-approved` | Its `version` is the approved one, and it names no step: under more than one gated step, read `gatedSteps` in your own record (the `legion` tool's `read_record`) to see which step that version opened and whether another still holds the tree. A human approved the document of one gated step at its current version; that step is open. Proceed to section 2 once every gated step is `approved` — except on a root whose catch-up said `handed_over`: its workers already run their phases, so start nothing; the approval releases any READY the gate refused. One whose `version` is the version a human's own answer to a decision block wrote, after an earlier approval (Dispatch carried that approval to it, "After approval" in `skill://legion-architect/references/design-gate.md`), opens nothing new: carry on, and release no
|
|
27
|
+
| `design-approved` | Its `version` is the approved one, and it names no step: under more than one gated step, read `gatedSteps` in your own record (the `legion` tool's `read_record`) to see which step that version opened and whether another still holds the tree. A human approved the document of one gated step at its current version; that step is open. Proceed to section 2 once every gated step is `approved` — except on a root whose catch-up said `handed_over`: its workers already run their phases, so start nothing; the approval releases any READY the gate refused. One whose `version` is the version a human's own answer to a decision block wrote, after an earlier approval (Dispatch carried that approval to it, "After approval" in `skill://legion-architect/references/design-gate.md`), opens nothing new: carry on, and release no child because of it. A root that was waiting on a question in `awaiting_person` gets no such notice: the task that resumes it says "Your spec is approved at version N and the design gate is open: this resume is your design-approved wake", and that resume is this wake — proceed to section 2 on it. |
|
|
28
28
|
| `design-changes-requested` | Its `version` and `reason`, and no step: under more than one gated step, read `gatedSteps` in your own record (the `legion` tool's `read_record`) and take the step whose `status` is `changes_requested`. A human asked for changes to that step's document at `version`, for `reason`. Revise that document as the reason asks and request approval again as "The armed gate" in `skill://legion-architect/references/design-gate.md` says; end your turn; that step is closed. On a root that was waiting on a question in `awaiting_person`, a review the human gave after the wait began comes in the resume task, not in this notice, whether it answered the wait or came after something else did: the task names the review with its version and reason, unless a later approval superseded it. A `version` below the latest, or a second request for one version, is a request that reached the daemon late: read it as the same section's last paragraph says. Each request arrives once. |
|
|
29
29
|
| `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 finishing phase's handoff on the Dispatch issue (`dispatch handoff-read --issue <issue> --phase <word>`); 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 the full condition in "After approval" of `skill://legion-architect/references/design-gate.md`: when the approved Summary, the requirements, the checks, the scope and the settled decisions still hold, the plan is the record; otherwise change the root spec and request approval again as that section 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 issue is in `accepting` and the acceptance is yours (section 6): read the evidence, then `sign_off` (a parent only after its last round, "The last round" in `skill://legion-architect/references/acceptance.md`) or send it back with `request_backward_move`. A `tester` notice with `verdict: "fail"` — its handoff carries `implementerProof.verdict: "rejected"`, or a failure naming a check with no current passing result — has gone back to the **implementer** by the daemon's table; never record that check's result 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: build the surface in this tree: a child issue (infrastructure, tooling, or a skill) after the red-team check in `skill://legion-architect/references/decomposition.md`, or, when the red team folds it, tell the waiting worker to move the issue back to `implementing` with `request_backward_move`, naming the surface. When it is a child, tell the waiting worker to continue with `envoy_publish` to its role topic once that child's `child-closed` arrives. That report is never a reason to advance the phase. |
|
|
30
|
-
| `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: no handoff is a commit, so every such push counts except a push by the review App (a planner's, tester's, reviewer's or architect's), and the head pushed onto a red the tester's red tests earned (a planned red) does not count either. 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, and no later CI red within the attempt that ran the count out (the counted head, and every head pushed on top of it until a push lands on a head CI judged green or on a settled head whose base branch requires no check and no workflow, the tester's red tests onto the held red included) sends the work back from `testing`, `reviewing`, `accepting`, `retro` or `merging` either: the issue stays in its phase until you move it, or a worker's own decision does (a tester's `fail`, a reviewer's request for changes). A head pushed onto green CI, or onto a settled head whose base branch requires no check and no workflow, leaves that attempt, so its red still comes as a `checks-red`. 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. In `accepting`, where no worker holds a phase, the move is yours: `request_backward_move` with `issue`, naming the failing checks. 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. 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 a conflict (`the head conflicts with <base>`), the required checks that failed (`CI is red at <head>`), or the merge queue's failed merge and its failing run (`the merge queue took pull request #<n> out at READY's 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; your own send-back from `accepting` by the reason you gave. 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, or when the merge queue fails the merge) are the rounds the cap is for. Each `review_round_cap` notice is a decision you make in the turn it wakes, never one left for the next round to make: read what every counted round found, choose one of the three below, act on it in the same turn, and post it as a `dispatch comment` on the issue naming the rounds you read and the choice. **Narrow**: the change itself is sound but each round finds more around it; tell the implementer and the reviewer (`envoy_publish` to each one's role topic) which checks of the issue the next round answers; a correctness finding is still fixed in this pull request, and so is a wording nit or any clear fix in a file it touches, so the `Fast-follow:` line carries only cleanup in files this pull request does not touch. **Split**: part of the work cannot settle in this pull request
|
|
30
|
+
| `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: no handoff is a commit, so every such push counts except a push by the review App (a planner's, tester's, reviewer's or architect's), and the head pushed onto a red the tester's red tests earned (a planned red) does not count either. 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, and no later CI red within the attempt that ran the count out (the counted head, and every head pushed on top of it until a push lands on a head CI judged green or on a settled head whose base branch requires no check and no workflow, the tester's red tests onto the held red included) sends the work back from `testing`, `reviewing`, `accepting`, `retro` or `merging` either: the issue stays in its phase until you move it, or a worker's own decision does (a tester's `fail`, a reviewer's request for changes). A head pushed onto green CI, or onto a settled head whose base branch requires no check and no workflow, leaves that attempt, so its red still comes as a `checks-red`. 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. In `accepting`, where no worker holds a phase, the move is yours: `request_backward_move` with `issue`, naming the failing checks. 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. 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 a conflict (`the head conflicts with <base>`), the required checks that failed (`CI is red at <head>`), or the merge queue's failed merge and its failing run (`the merge queue took pull request #<n> out at READY's 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; your own send-back from `accepting` by the reason you gave. 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, or when the merge queue fails the merge) are the rounds the cap is for. Each `review_round_cap` notice is a decision you make in the turn it wakes, never one left for the next round to make: read what every counted round found, choose one of the three below, act on it in the same turn, and post it as a `dispatch comment` on the issue naming the rounds you read and the choice. **Narrow**: the change itself is sound but each round finds more around it; tell the implementer and the reviewer (`envoy_publish` to each one's role topic) which checks of the issue the next round answers; a correctness finding is still fixed in this pull request, and so is a wording nit or any clear fix in a file it touches, so the `Fast-follow:` line carries only cleanup in files this pull request does not touch. **Split**: part of the work cannot settle in this pull request. It is a child only when it needs a repository or a deploy order of its own (`skill://legion-architect`, section 1), and then it goes through the red-team check in `skill://legion-architect/references/decomposition.md` and is filed as a child of this issue's parent, never of this one, since this issue closes at its own merge. Work in a repository this tree already has a pull request in is no child and is not parked in `backlog`: write it into the spec of the issue whose pull request carries it, the parent's, or a sibling's when the parent's pull request is in another repository. On the root, narrow it to what settles here and open a `dispatch ask` for the rest (`skill://legion-worker/references/findings.md`): a person decides whether that becomes an issue. Tell the implementer and the reviewer that this pull request no longer carries it. **Accept the rounds**: each round found less than the one before it, and the last found no defect in the change itself (a stale line, a comment that states an old default); let the loop run and say why in the comment. A later notice is a new decision: accept again only when its round found less than the round you last accepted, and otherwise narrow or split. A finding that changes what the spec requires or what a check proves is a decision block in the root spec ("After approval" in `skill://legion-architect/references/design-gate.md`), and `dispatch ask` is only for a decision that needs a human's authority; neither replaces the choice. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` ("A conflicting head" in `skill://legion-architect/references/acceptance.md`) is a conflict return. The merger's move back to `reviewing` for a READY refusal (`READY_UNREVIEWED_CHANGE`, or `READY_NOT_APPROVED` when its task names no approved head), and the reviewer's no-fix request for changes that follows it so the tester runs the head, are not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
|
|
31
31
|
| `pr-merged` | The pull request merged and the issue did not close; its `reason` says why. Merged before the issue reached `awaiting_merge`: the workflow runs on, and the merge closes the issue the moment it gets there, your acceptance and retro first. Merged while the rest of the tree still runs: the reason names the pull request and every issue the close waits for, and the root waits in `awaiting_merge`, its workers kept and live and no admission slot held, until the last of them is done or parked ("The close" in `skill://legion-architect/references/acceptance.md`). Either way you start nothing and no further notice follows. |
|
|
32
32
|
| `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. |
|
|
33
33
|
| `pull-request-missing` | Its `reason` names the issue, the repository, the branch and the handoff commit: the issue's implementer completed at that commit and GitHub lists no open pull request on the branch, so the issue stays in `implementing`. Tell the implementer (`envoy_publish` to its role topic) to open the pull request from that branch; the daemon records it and moves the issue to `testing` once it is open. When the work should not go on, decide as the `pr-closed-unmerged` row says. It comes once per handoff commit. |
|
|
@@ -44,4 +44,4 @@ and 7 (a conflict, the hand-over, the close) is in
|
|
|
44
44
|
## A root that joins your tree, and a refused release
|
|
45
45
|
|
|
46
46
|
- A root that waited in the admission queue, never ran and never left another tree joins your tree once a person makes it your child, if it is `todo`, and otherwise at your release of it (the `child-status` row of [wake routing](skill://legion-architect/references/wake-routing.md)); once in, it is held for its blockers like any child you release. A root that left another tree, handed over by its finished tree or admitted as an orphan, stays its own tree and never joins yours, even one that was your root's child before your root was admitted again. A labelled `todo` issue under a child you have not released, unless it ran as a tree of its own or left another tree, is held under that unreleased child (not a blocked-by hold): it waits for no slot until your release of it or of that child brings it in, with any `todo` child of its own.
|
|
47
|
-
- A release that cannot take a child in is refused 409 and writes nothing, its message naming the child and why: release the rest
|
|
47
|
+
- A release that cannot take a child in is refused 409 and writes nothing, its message naming the child and why: release the rest again without it. `CHILD_NOT_ADOPTABLE` is an issue that runs or ran as a tree of its own, carries a run handed over to it, or was handed over (or admitted as an orphan) as a root of its own: leave it there, or ask the controller. `CHILD_IN_OTHER_TREE` is another tree's child. When the tree it names is a root held under an unreleased child of yours (a root that queued and never ran, so it has no architect of its own), release that root, or the unreleased child it sits under, instead: its children come with it. Otherwise re-parent the child back in Dispatch, or ask that tree's architect or the controller.
|
|
@@ -33,10 +33,13 @@ Launch, `gh`, pull requests, takeover: `skill://legion-controller/references/ses
|
|
|
33
33
|
|
|
34
34
|
1. Recheck [trees waiting on a root claim](#trees-waiting-on-a-root-claim).
|
|
35
35
|
2. Do the wake's work, or, on a start message, the start procedure below.
|
|
36
|
-
3. On your first turn of each UTC
|
|
36
|
+
3. On your first turn of each UTC week, whatever woke you: run the weekly audit
|
|
37
|
+
(`skill://legion-controller/references/weekly-audit.md`) before the day's report, so its
|
|
38
|
+
numbers and its nudges go in that report.
|
|
39
|
+
4. On your first turn of each UTC day, whatever woke you: run the weekly-plan reconciliation
|
|
37
40
|
(`skill://legion-controller/references/backlog-stewardship.md`), then post the
|
|
38
41
|
[daily report](#daily-report).
|
|
39
|
-
|
|
42
|
+
5. Name the [free slots](#free-slots) in your summary, so a person sees room to hand Legion work.
|
|
40
43
|
|
|
41
44
|
## What happened before you started
|
|
42
45
|
|
|
@@ -73,6 +73,13 @@ every issue by its `dispatch://` link and what it does for the reader, every pul
|
|
|
73
73
|
name, its `detail`, and a deployment row's `configLine`. Say who acts on it, as
|
|
74
74
|
`skill://legion-controller/references/wakes.md` says for `tick`. With none open, say nothing of
|
|
75
75
|
them.
|
|
76
|
+
- **The week** (on the report of your first turn of each UTC week only, from
|
|
77
|
+
`skill://legion-controller/references/weekly-audit.md`): the window's two times; the forced
|
|
78
|
+
creates by filer with the no-answer creates apart; the tree sizes in the five buckets with the
|
|
79
|
+
parked descendants; each nudge you sent, by its tree; and the duplicate audit's pairs, those
|
|
80
|
+
you folded and those you skipped with the event that settles each. Name beside each number the
|
|
81
|
+
command that recounts it. When you lead a stream that keeps a scoreboard, the same two measures
|
|
82
|
+
go on it under `forced_creates_by_filer` and `tree_sizes`.
|
|
76
83
|
|
|
77
84
|
A day with nothing finished says so in one sentence. When the lists do not fit, give each list's
|
|
78
85
|
count and name its issues of `--priority 0` and `--priority 1`, in board rank order.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# The weekly audit
|
|
2
|
+
|
|
3
|
+
Read this on your first turn of each UTC week, whatever woke you, and run all four passes below
|
|
4
|
+
before you post the day's report. They are the week's own numbers, beside the daily ones. The
|
|
5
|
+
fourth pass, the duplicate audit, serves R8 of dispatch://INFRA-1160 and is not optional.
|
|
6
|
+
|
|
7
|
+
## The week
|
|
8
|
+
|
|
9
|
+
The week's boundary is your first turn on or after Monday 00:00 UTC. The window is the seven days
|
|
10
|
+
before that boundary: `date -u +%Y-%m-%dT%H:%M:%SZ` prints the time now, and the window runs from
|
|
11
|
+
seven days before it to it. Name the window's two times in the report, so a reader can recount.
|
|
12
|
+
|
|
13
|
+
You already ran this week when the report issue carries a message of yours from on or after the
|
|
14
|
+
boundary whose text names the window. Read the report issue before you start
|
|
15
|
+
(`skill://legion-controller/references/daily-report.md`).
|
|
16
|
+
|
|
17
|
+
## 1. The week's forced creates
|
|
18
|
+
|
|
19
|
+
A create that went past Dispatch's near-copy refusal with `--force` carries `forced` on its
|
|
20
|
+
`issue.created` event, with `past` naming the issues the refusal named. A forced create the
|
|
21
|
+
refusal would not have stopped carries nothing, so this is a count of overrides, not of uses of
|
|
22
|
+
`--force`. A `past` that is the empty list is a create whose check did not answer: report those
|
|
23
|
+
apart, since nobody forced past anything there. Each form reads as itself on the `issue.created`
|
|
24
|
+
line: an override ends `forced past <keys>`, and a create whose check did not answer ends
|
|
25
|
+
`forced, the near-copy check did not answer`.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
dispatch issues --project <PROJECT> --all
|
|
29
|
+
dispatch read --issue <KEY> --all-events # read the issue.created line
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--all-events` is not optional: a bare `dispatch read` prints the last ten events alone, and on
|
|
33
|
+
any issue with more than ten the `issue.created` line is not printed at all.
|
|
34
|
+
|
|
35
|
+
Keep the issues created inside the window, read each one's `issue.created` line, and count by
|
|
36
|
+
filer. The line names its writer as `session <id> (<the session's title>)`, and the filer is that
|
|
37
|
+
title cut at its last middle dot (`·`), so `Legion implementer · INFRA-1160` counts as
|
|
38
|
+
`Legion implementer`, with every person counted together as one filer. A session that carries no
|
|
39
|
+
title prints its id alone and is counted by that id. Report, per filer, how many creates forced
|
|
40
|
+
past a refusal and which issues each named, then the no-answer creates as their own line.
|
|
41
|
+
|
|
42
|
+
The recount is the same two commands: name them in the report beside the number.
|
|
43
|
+
|
|
44
|
+
## 2. The week's tree sizes
|
|
45
|
+
|
|
46
|
+
A Legion tree is an open issue with the `legion` label and no ancestor carrying it, with its
|
|
47
|
+
descendants. A tree closed inside the window counts at its close.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
dispatch issues --project <PROJECT> --label legion --all
|
|
51
|
+
dispatch read --issue <KEY> # Referenced by: child_of names its children
|
|
52
|
+
dispatch issues --project <PROJECT> --parent <KEY> # the same children, one level at a time
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`Links: child_of` on that read gives the issue's own parent, never its children, so walk down
|
|
56
|
+
instead: read the root, take each `child_of` row under `Referenced by:` as a child, and read each
|
|
57
|
+
child the same way until a read names none. `dispatch issues --parent` lists one level in one
|
|
58
|
+
request and is the shorter walk on a wide tree.
|
|
59
|
+
|
|
60
|
+
Count each tree's descendants at any depth, and report the trees in five buckets: 0, 1-2, 3-5,
|
|
61
|
+
6-10, and 11 or more. Count the parked descendants of each bucket too: a descendant in `backlog`
|
|
62
|
+
or `icebox`. Name the recount commands beside the numbers, since a reader checks them against
|
|
63
|
+
Dispatch's own parent links.
|
|
64
|
+
|
|
65
|
+
The report and, when you lead a stream that keeps a scoreboard, the stream's scoreboard artifact
|
|
66
|
+
both carry the two measures, under the keys `forced_creates_by_filer` and `tree_sizes`.
|
|
67
|
+
|
|
68
|
+
## 3. The nudges
|
|
69
|
+
|
|
70
|
+
Ask the architect of each tree the counts name to fold what can ride in an open pull request.
|
|
71
|
+
Both rules below count only a tree with at least one open descendant, a descendant whose status
|
|
72
|
+
is not `done`. A tree whose descendants are all closed has nothing left to fold, and a nudge
|
|
73
|
+
sent to one asks its architect to fold work that is already finished.
|
|
74
|
+
|
|
75
|
+
- a tree with at least one open descendant, and more than 8 descendants in all or more than 2
|
|
76
|
+
levels below its root;
|
|
77
|
+
- a tree with at least one open descendant that filed a child no red-team verdict names. The
|
|
78
|
+
verdict is a comment on the root whose first line reads `Red-team verdict for <root key>:`
|
|
79
|
+
(`skill://legion-architect/references/decomposition.md`). Read the root's comments with
|
|
80
|
+
`dispatch read --issue <ROOT> --all-events` and the replies under each verdict comment; a child
|
|
81
|
+
named in none of them was filed without the check.
|
|
82
|
+
|
|
83
|
+
Count no child created before that first line existed. The architects write it from the Legion
|
|
84
|
+
release that carries this file, so every child filed before then has no verdict comment to
|
|
85
|
+
find and is not evidence of a skipped check: read the release's own publication time from its
|
|
86
|
+
tag and leave out every child created at or before it. Without that, the first run of this
|
|
87
|
+
pass nudges nearly every tree that has children. Until a release carries this file, there is
|
|
88
|
+
no such tag to read: when `git tag --contains <the commit that added this file>` names no
|
|
89
|
+
release, the cutoff is the time of your run, so every child that exists counts as filed
|
|
90
|
+
before the rule and no tree is nudged for a missing verdict yet.
|
|
91
|
+
|
|
92
|
+
Each nudge is one `dispatch comment` on the tree's root, naming what you counted and asking the
|
|
93
|
+
architect to fold what can ride in an open pull request of the tree. End the comment with
|
|
94
|
+
`descendants: <n>, levels: <m>`, the counts you sent it at. When the daemon's state names an
|
|
95
|
+
architect for the tree (`read_state`'s `issues.<KEY>`), send the same text to that architect's
|
|
96
|
+
role topic with `envoy_publish`.
|
|
97
|
+
|
|
98
|
+
Send no new comment to a tree whose architect replied in the thread of your newest nudge comment
|
|
99
|
+
there, until the tree's descendants pass the number that comment ends with. A tree whose children
|
|
100
|
+
were all filed before the verdict's first line existed has no such comment and is nudged once;
|
|
101
|
+
the architect's reply then settles it until the tree grows.
|
|
102
|
+
|
|
103
|
+
List every nudge you sent in the day's report.
|
|
104
|
+
|
|
105
|
+
## 4. The duplicate audit
|
|
106
|
+
|
|
107
|
+
For each open issue of the project, read the related items of its title and spec through the
|
|
108
|
+
no-write related route, leaving the issue, its ancestors and its descendants out, and judge
|
|
109
|
+
whether each pair the two issues' own records do not already settle is the same work.
|
|
110
|
+
|
|
111
|
+
The project holds hundreds of open issues and one related read takes about a second, so a serial
|
|
112
|
+
pass is ten minutes of HTTP before any judging. Shard the open list and dispatch one
|
|
113
|
+
`task(agent="scout")` per shard of at most 50 issues, then `wait` for them: each returns the pairs
|
|
114
|
+
it found with its evidence, and you judge and fold them yourself.
|
|
115
|
+
|
|
116
|
+
Give each scout the shard's keys and these commands:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
dispatch read --issue <KEY> # title, spec artifact, Links:, Referenced by:
|
|
120
|
+
dispatch doc-read --issue <KEY> # the spec text the related read uses
|
|
121
|
+
dispatch related --project <PROJECT> --title '<its title>' --text-file - \
|
|
122
|
+
--exclude-issue <KEY> --exclude-issue <each ancestor> --exclude-issue <each descendant>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`--exclude-issue` is repeatable and takes up to 50 issues; the issue itself, its ancestors and its
|
|
126
|
+
descendants all go in one call. The read names both: a `child_of` row under `Links:` is the
|
|
127
|
+
issue's own parent, read again on that parent for the next ancestor up, and each `child_of` row
|
|
128
|
+
under `Referenced by:` is a child, read again on each child for the next level down. Ask each
|
|
129
|
+
scout for, per issue, every related item that is an issue with the reason it looks like the same
|
|
130
|
+
work, and nothing else: no comment, no close, no message.
|
|
131
|
+
|
|
132
|
+
A pair is settled, and is skipped rather than proposed, when the two issues already say so: one
|
|
133
|
+
was closed as a duplicate of the other, one was reopened after such a close, or one was made a
|
|
134
|
+
child of the other. Read the first two from `dispatch read --issue <KEY> --all-events`'s
|
|
135
|
+
`Events:`: the close's reason is the `message.created` line just before `issue.closed`, and a
|
|
136
|
+
reopen is an `issue.updated` line that moves the status off `done`. No `Events:` line reports a
|
|
137
|
+
parent change, so read the third from the same read's `Links:` and `Referenced by:`.
|
|
138
|
+
|
|
139
|
+
Fold each pair you judge the same work by the duplicate rule of `skill://dispatch-first`: keep the
|
|
140
|
+
issue with the spec, copy to it what only the other has, and close the other
|
|
141
|
+
(`--status done --reason 'Duplicate of dispatch://KEY.'`). An issue carrying the `legion` label,
|
|
142
|
+
or another holder's claim, is not yours to close: comment the kept issue's name on it instead. A
|
|
143
|
+
pair inside a Legion tree goes to that tree's architect with `envoy_publish`, which decides it.
|
|
144
|
+
|
|
145
|
+
The report lists every pair you judged the same work, with what you did about it, and every pair
|
|
146
|
+
you skipped with what settles it: the event, or the parent link.
|
|
@@ -19,7 +19,7 @@ You work one phase of one issue in a jj workspace every role of the issue shares
|
|
|
19
19
|
|
|
20
20
|
## Stop rules
|
|
21
21
|
|
|
22
|
-
- **A refused completion is information, not a retry loop.** Quote the answer verbatim to the architect, so "my work is lost" and "my work belongs to an ended run" read differently. `HANDOFF_STALE_GENERATION` (the issue is on a newer run) or `HANDOFF_NO_RUN` (your claim holds no task): push nothing further, end the turn without completing, and tell the architect; if the task then arrives and its work and handoff are already done, call `handoff_complete` again in that task's turn, and never redo the work or write a second handoff. `HANDOFF_STALE_ASSIGNMENT` (the issue left your phase and came back while your turn ran, so your work belongs to the assignment before the one the phase hands you now): end the turn without completing and tell the architect, and report that work and that verdict no further. The phase was entered again, so the task it hands you now is work of its own — a testing assignment after a red is for the head the fix pushed, which your verdict on the head before it does not answer for: do that work at the head the task names, write a handoff of its own for it, and complete in its turn. `HANDOFF_NOT_CURRENT_PHASE`: the issue left your phase; report, never complete again. `HANDOFF_NO_PULL_REQUEST` (the implementer's completion found no open pull request on `legion/<KEY>`, and none recorded merged): open it and complete again at the same commit; when the answer names a closed pull request, the architect decides the phase: tell it, and open or reopen nothing until it answers. `HANDOFF_ALREADY_RECORDED`: the first answer stands; if you did not see it, tell the architect. `HANDOFF_SESSION_MISMATCH` names a record another session wrote: read it, record it again from your session with `dispatch handoff-write`, and complete. A reminder is not a task: after an accepted completion, a second call is refused.
|
|
22
|
+
- **A refused completion is information, not a retry loop.** Quote the answer verbatim to the architect, so "my work is lost" and "my work belongs to an ended run" read differently. `HANDOFF_STALE_GENERATION` (the issue is on a newer run) or `HANDOFF_NO_RUN` (the daemon can attribute your completion to no run: your claim holds no task of this issue's current run and phase — no task at all, or one of another run, of another phase, or of no phase — and the answer says which of those it holds): push nothing further, end the turn without completing, and tell the architect; if the task then arrives and its work and handoff are already done, call `handoff_complete` again in that task's turn, and never redo the work or write a second handoff. `HANDOFF_STALE_ASSIGNMENT` (the issue left your phase and came back while your turn ran, so your work belongs to the assignment before the one the phase hands you now): end the turn without completing and tell the architect, and report that work and that verdict no further. The phase was entered again, so the task it hands you now is work of its own — a testing assignment after a red is for the head the fix pushed, which your verdict on the head before it does not answer for: do that work at the head the task names, write a handoff of its own for it, and complete in its turn. `HANDOFF_NOT_CURRENT_PHASE`: the issue left your phase; report, never complete again. `HANDOFF_NO_PULL_REQUEST` (the implementer's completion found no open pull request on `legion/<KEY>`, and none recorded merged): open it and complete again at the same commit; when the answer names a closed pull request, the architect decides the phase: tell it, and open or reopen nothing until it answers. `HANDOFF_ALREADY_RECORDED`: the first answer stands; if you did not see it, tell the architect. `HANDOFF_SESSION_MISMATCH` names a record another session wrote: read it, record it again from your session with `dispatch handoff-write`, and complete. A reminder is not a task: after an accepted completion, a second call is refused.
|
|
23
23
|
- **A turn that ends with your phase open is reminded**, at the end of every turn for as long as the phase stays open. Answer by completing, or with a WAITING line naming what you wait on, which buys a quiet wait; a turn that ends while background work of your own is still running buys the same quiet with no line at all, since that work wakes you when it finishes. A run of reminders that change nothing tells your architect. Under `runtime: kubernetes` that quiet wait also lets the daemon release your issue's pod, while background work of yours keeps the pod, because your session reports that work to the daemon.
|
|
24
24
|
- **A released pod is not a stop.** Under `runtime: kubernetes` the daemon releases your issue's pod once no role of it has a turn in progress or a turn due for ten minutes. Your session, your phase, your place in it and the task you hold are all kept, and your unpushed commits and working-copy change are restored into the next pod, so a release costs a pod start and nothing else. A wait that raises no event of its own wakes you at the backstop alone, 60 minutes after the release, so name the wait in your WAITING line and expect that hour. A release keeps nothing else of the disk — `$TMPDIR`, every ignored file, each role's home, a test database and the CodeGraph index all go with the pod — so put work you must keep on the branch, in a tracked file or in Dispatch. The backstop runs only while your phase is open.
|
|
25
25
|
- **The workspace is read-only for you from the moment your phase ends**, whether you ended it by completing or a CI red took it back while your turn ran: no commit, no `jj new`, no edit to a tracked file, in the turn still running when the message reaches you and in every turn after it, until a new task names your phase. Nothing stops that turn for you, so stopping is yours to do: drop the work in hand and leave whatever you have uncommitted. The active phase owns the workspace, and your session is kept, until your issue leaves the workflow, to answer questions and to take your next assignment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/claude-envoy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Claude Code channel plugin for Legion's Envoy bus",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"AGENTS.md"
|
|
20
20
|
],
|
|
21
21
|
"legion": {
|
|
22
|
-
"daemonApiVersion":
|
|
22
|
+
"daemonApiVersion": 56
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ Dispatch is a record for the people who decide, never a log of your work.
|
|
|
9
9
|
|
|
10
10
|
- Use the `dispatch` command; the HTTP API only for what no command does. `dispatch <command> --help` gives its rules and an example.
|
|
11
11
|
- Put long text on stdin: `--<field>-file -` with a quoted here-document (`<<'EOF'`); give object and list fields as JSON.
|
|
12
|
-
- Exit 1 is a refusal, 2 a usage error. A refusal lists
|
|
12
|
+
- Exit 1 is a refusal, 2 a usage error. A refusal lists every problem: fix all, call once.
|
|
13
13
|
- A Dispatch command hard to use, or storing or losing something wrongly, is a defect to report, not work around: open an issue in the owning project's code and tell that project's controller; a Legion role tells its architect (`skill://legion-worker/references/findings.md`).
|
|
14
14
|
|
|
15
15
|
## Asks and ask blocks
|
|
@@ -35,7 +35,7 @@ The question and the recommendation, as `skill://dispatch-writing-specs` writes
|
|
|
35
35
|
|
|
36
36
|
## Choosing what to work on
|
|
37
37
|
|
|
38
|
-
-
|
|
38
|
+
- Finishing an issue, take the top `todo` one by priority then board rank, per project.
|
|
39
39
|
- `todo` means specced and waiting on nothing; one that waits goes in `backlog`, saying on what.
|
|
40
40
|
- Hold at most three issues in flight; move the rest to `backlog`, saying where each is.
|
|
41
41
|
- Never reorder the board: one agent keeps each project's order (`issue-update --before`/`--after`). If the top looks wrong, tell `notifications.role.legion-<project>-controller`.
|
|
@@ -5,19 +5,17 @@ description: "Use when a session has the dispatch command, before you plan, file
|
|
|
5
5
|
|
|
6
6
|
# Dispatch first
|
|
7
7
|
|
|
8
|
-
Dispatch holds the open issues, their specs and people's answers.
|
|
9
|
-
|
|
10
8
|
## Search before you act
|
|
11
9
|
|
|
12
10
|
- Before you plan, file an issue, ask, post a finding or start work, run `dispatch search`.
|
|
13
|
-
- Each query word must match. Use two or three
|
|
11
|
+
- Each query word must match. Use two or three: the thing and what is wrong. Never paste a draft.
|
|
14
12
|
|
|
15
13
|
```bash
|
|
16
14
|
dispatch search --query 'broadcast send order'
|
|
17
15
|
dispatch search --query 'reviewer threads OR review comments'
|
|
18
16
|
```
|
|
19
17
|
|
|
20
|
-
- No hits: remove a word before
|
|
18
|
+
- No hits: remove a word before adding one. Try two or three wordings before deciding nothing exists.
|
|
21
19
|
- Open each hit that can be yours with `dispatch read --issue KEY`, then its parent (`child_of` under `Links:`, or a document's `Issue:`), with its status and recent events.
|
|
22
20
|
|
|
23
21
|
## What to do with what you find
|
|
@@ -26,7 +24,7 @@ dispatch search --query 'reviewer threads OR review comments'
|
|
|
26
24
|
|
|
27
25
|
- **Work already tracked:** put the finding on that issue. File a new issue only when no hit covers the work, and cite the nearest one you ruled out (`dispatch://KEY`).
|
|
28
26
|
- **Question already decided:** the answered ask is the record. Cite it (`dispatch://KEY/ask/<id>`). Do not ask again.
|
|
29
|
-
- **Duplicate found:**
|
|
27
|
+
- **Duplicate found, or a likely duplicate a create names:** read it; fold only when it is the same work. Keep the issue with the spec, copy to it what only the duplicate has, and close the duplicate (`--status done --reason 'Duplicate of dispatch://KEY.'`). When it has the `legion` label or another holder's claim, comment the kept issue's name on it instead.
|
|
30
28
|
|
|
31
29
|
## Research before you ask
|
|
32
30
|
|
|
@@ -36,8 +34,6 @@ dispatch search --query 'reviewer threads OR review comments'
|
|
|
36
34
|
|
|
37
35
|
## Reach a person only through Dispatch
|
|
38
36
|
|
|
39
|
-
Reach a person through an ask, an ask block or an approval request.
|
|
40
|
-
|
|
41
37
|
- The moment a person blocks your work, open an ask: an ask block for a design question its document records, else `dispatch ask`. A blocker that is not an open ask does not exist for them. A Legion phase worker sends decisions to its architect instead. Before you say you wait on a person, run `dispatch open-asks`.
|
|
42
38
|
- A result a person asked for is the same: a document or artifact notifies nobody. End that document with an ask block asking their next step, else `dispatch ask`.
|
|
43
39
|
- Record every request you accept in Dispatch before you start it: an issue, or a line in your issue's `open-items.md` artifact. Before you end a turn, read that record. Each item is done, in progress, or an open ask. Never drop one.
|
|
@@ -28,7 +28,7 @@ Part | Content
|
|
|
28
28
|
`Design` | How it meets them; a value and its reason for each limit, size and time
|
|
29
29
|
`Decisions` | One line: `The decisions are in dispatch://KEY/artifact/decisions-md.`
|
|
30
30
|
|
|
31
|
-
`serves` and `needs` name a block of this issue or an ancestor. Another agent can build and check the work from these alone. A small change gets a few sentences per part, no matrix; split a large design into
|
|
31
|
+
`serves` and `needs` name a block of this issue or an ancestor. Another agent can build and check the work from these alone. A small change gets a few sentences per part, no matrix; split a large design into children.
|
|
32
32
|
|
|
33
33
|
## Rules for the text
|
|
34
34
|
|