@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.
@@ -2,5 +2,5 @@
2
2
  "name": "claude-envoy",
3
3
  "description": "Deliver Envoy events as Claude Code channel notifications and expose Envoy MCP tools.",
4
4
  "mcpServers": "./.mcp.json",
5
- "version": "1.13.0"
5
+ "version": "1.14.1"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-envoy",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "Deliver Envoy events as Claude Code channel notifications and expose Envoy MCP tools.",
5
5
  "mcpServers": {}
6
6
  }
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. exclude_issue leaves that issue's own items out, " + "as a check on that issue's own write does; keywords_only skips meaning search.",
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
- exclude_issue: z.string().describe("Optional issue key in the project whose own items are left out.").optional(),
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(`Title overlaps you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
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 title overlaps could not be shown: ${messageFor(error)}`);
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 ${actorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
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}" looks like a duplicate.`,
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
- "Reference the existing issue, or run dispatch issue again with --force after reading it."
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 excludeIssue = optionalString(args, "exclude_issue");
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
- ...excludeIssue === undefined ? {} : { exclude_issue: excludeIssue },
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, REFUSED, parsed.tool);
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, REFUSED, parsed.tool);
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 {
@@ -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. exclude_issue leaves that issue's own items out, " + "as a check on that issue's own write does; keywords_only skips meaning search.",
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
- exclude_issue: z.string().describe("Optional issue key in the project whose own items are left out.").optional(),
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(`Title overlaps you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
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 title overlaps could not be shown: ${messageFor(error)}`);
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 ${actorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
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}" looks like a duplicate.`,
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
- "Reference the existing issue, or run dispatch issue again with --force after reading it."
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 excludeIssue = optionalString(args, "exclude_issue");
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
- ...excludeIssue === undefined ? {} : { exclude_issue: excludeIssue },
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.13.0";
55182
+ var version2 = "1.14.1";
55159
55183
  var legion = {
55160
- daemonApiVersion: 55
55184
+ daemonApiVersion: 56
55161
55185
  };
55162
55186
 
55163
55187
  // src/channel-forwarder.ts
@@ -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. exclude_issue leaves that issue's own items out, " + "as a check on that issue's own write does; keywords_only skips meaning search.",
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
- exclude_issue: z.string().describe("Optional issue key in the project whose own items are left out.").optional(),
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(`Title overlaps you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
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 title overlaps could not be shown: ${messageFor(error)}`);
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 ${actorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
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}" looks like a duplicate.`,
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
- "Reference the existing issue, or run dispatch issue again with --force after reading it."
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 excludeIssue = optionalString(args, "exclude_issue");
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
- ...excludeIssue === undefined ? {} : { exclude_issue: excludeIssue },
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 waves, 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."
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 in the first wave. Never replace or re-decompose 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 is a separately deliverable change: its own pull request in its own repository, or its own deploy. 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`.
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 waves and the plan's tasks go in the children and the planner's `plan` handoff, not the root spec.
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 wave 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`.
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, a later wave's included, each with `--blocked-by` the children it builds 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
+ - 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 the whole wave 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.
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
- first-wave child of its own. While the approved head does not carry those merges, or the result of
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
- Put every adopted child into the first wave and release it with
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 cannot ride in the
48
- root's pull request or an open child's: its code is in a repository no open pull request of this
49
- tree is in, or it is its own deploy: it must be applied on its own before other work of the tree
50
- can build on it.
51
- A repeated change — the same edit per program, directory or wave of this tree's own work —
52
- survives as one child, never one per instance, and a deletion of what this tree replaced rides
53
- in the pull request of the last change it waits on, never a child of its own: a wave is an order
54
- of merges, not a deliverable.
55
- Post the verdict, with the folds you made, as one `dispatch comment` on your own issue (the root,
56
- in a root session): the architect
57
- who wrote the list reads every child on it as needed, and the comment stops a later session
58
- filing the same children again.
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
- - Order the returned keys into waves with `--blocked-by` links (a later wave's child blocked by the
74
- children it builds on), then release every child at once with `release_children`: Legion holds a
75
- released child until its last blocker closes, and a child is inert only until it is released.
76
- Each issue of the tree whose worker has work holds an admission slot of its own, so a wave of
77
- four working children takes four of the deployment's cap.
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 wave until every gated step is open: a delivered wake shows
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 wave because of it.
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 new wave until the `design-approved`
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 waves and re-scoping open children toward the same requirements need no spec edit and no new
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 wave because of it, and start nothing. |
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 wave 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. |
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 wave 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. |
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 and is a separately deliverable change (`skill://legion-architect`, section 1). On a child, run the red-team check in `skill://legion-architect/references/decomposition.md`, then file that part as a child of this issue's parent, never of this one (this issue closes at its own merge, and the split part usually builds on this pull request's unmerged code): `dispatch issue --project <project> --parent <parent> --title '<outcome>' --blocked-by <this issue> --spec-file -`. Release it now with `release_children`: Legion holds it on that `--blocked-by` link until this issue closes, and starts it then. 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. |
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 of the wave 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.
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 day, whatever woke you: run the weekly-plan reconciliation
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
- 4. Name the [free slots](#free-slots) in your summary, so a person sees room to hand Legion work.
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.13.0",
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": 55
22
+ "daemonApiVersion": 56
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -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 each problem: fix them all, then call once.
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
- - When you finish an issue, take the top ready one: `todo`, by priority then board rank, per project.
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 words: the thing and what is wrong with it. Never paste a draft.
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 you add one. Try two or three wordings before you decide nothing exists.
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:** 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.
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 child issues.
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