@sjawhar/pi-legion-envoy 5.16.8 → 5.16.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/envoy.js CHANGED
@@ -30028,6 +30028,16 @@ var messageValidation = {
30028
30028
  },
30029
30029
  message: "issue is required unless in_reply_to names a message delivered to this session, which is the one message with no issue."
30030
30030
  };
30031
+ var readOwner = documentOwnerValidation(true);
30032
+ var readValidation = {
30033
+ check: (value) => {
30034
+ const input = value;
30035
+ if (typeof input.message !== "string")
30036
+ return readOwner.check(value);
30037
+ return input.issue === undefined && input.project === undefined && input.artifact === undefined && input.ref === undefined;
30038
+ },
30039
+ message: `${readOwner.message} message stands alone: it names the conversation, so name no issue, project, artifact, or ref with it.`
30040
+ };
30031
30041
  var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
30032
30042
  function componentsArgument(z2) {
30033
30043
  return z2.object({
@@ -30255,7 +30265,7 @@ var dispatchToolSpecs = [
30255
30265
  {
30256
30266
  name: "dispatch_message",
30257
30267
  example: { issue: "DSP-1", body: "Implementation started." },
30258
- description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation, and a second call with the same in_reply_to posts nothing because " + "Dispatch keeps the one reply per message. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30268
+ description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30259
30269
  arguments: (z2) => ({
30260
30270
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
30261
30271
  body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
@@ -30349,14 +30359,15 @@ var dispatchToolSpecs = [
30349
30359
  {
30350
30360
  name: "dispatch_read",
30351
30361
  example: { issue: "DSP-1" },
30352
- description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain. Do not use it for document " + "contents; use dispatch_doc_read instead. Supply ref, issue, or project plus artifact. " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
30362
+ description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
30353
30363
  arguments: (z2) => ({
30354
30364
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30355
30365
  project: z2.string().describe("Project key owning the document.").optional(),
30356
30366
  artifact: z2.string().describe("Project document artifact id, slug, or filename.").optional(),
30357
- ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional()
30367
+ ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
30368
+ message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional()
30358
30369
  }),
30359
- validation: documentOwnerValidation(true)
30370
+ validation: readValidation
30360
30371
  },
30361
30372
  {
30362
30373
  name: "dispatch_search",
@@ -32443,8 +32454,8 @@ class DispatchClient {
32443
32454
  async message(issue2, input) {
32444
32455
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "messages"], input);
32445
32456
  }
32446
- async messageReply(id, input) {
32447
- return this.#json("POST", ["api", "v1", "messages", id, "reply"], input);
32457
+ async messageReply(id, input, options = {}) {
32458
+ return this.#json("POST", ["api", "v1", "messages", id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
32448
32459
  }
32449
32460
  async getMessage(issue2, id) {
32450
32461
  return this.#json("GET", [
@@ -32456,6 +32467,9 @@ class DispatchClient {
32456
32467
  id
32457
32468
  ]);
32458
32469
  }
32470
+ async getMessageThread(id, session) {
32471
+ return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
32472
+ }
32459
32473
  async artifact(issue2, input) {
32460
32474
  const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
32461
32475
  if ("content" in input)
@@ -33107,6 +33121,13 @@ function argumentProblems(tool, args) {
33107
33121
  }
33108
33122
  break;
33109
33123
  }
33124
+ case "dispatch_read": {
33125
+ const message = optionalString(args, "message");
33126
+ if (message !== undefined && messageIdOf(message) === undefined) {
33127
+ problems.push("message must be a full message id (uuid) or a dispatch://KEY/message/<id> reference");
33128
+ }
33129
+ break;
33130
+ }
33110
33131
  }
33111
33132
  return problems;
33112
33133
  }
@@ -33244,6 +33265,9 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
33244
33265
  if (tool === "dispatch_message" && typeof replyTarget === "string" && !replyTarget.startsWith("dispatch://")) {
33245
33266
  return { args, ref, owner: null };
33246
33267
  }
33268
+ if (tool === "dispatch_read" && typeof args.message === "string") {
33269
+ return { args, ref, owner: null };
33270
+ }
33247
33271
  const legionIssue = env.LEGION_ISSUE;
33248
33272
  if (!legionIssue) {
33249
33273
  problems.push(ownerRequiredProblem);
@@ -34166,16 +34190,30 @@ ${followsAsk(askOwner)}`,
34166
34190
  const inReplyTo = messageInReplyTo(args);
34167
34191
  const body = stringArg(args, "body");
34168
34192
  if (owner === null && inReplyTo !== undefined) {
34169
- const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor });
34193
+ const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor }, { followUp: true });
34194
+ if (reply.duplicate === true && reply.body === body) {
34195
+ return {
34196
+ text: `Dispatch already has this exact text in the conversation (message ${reply.id}); ` + "nothing new was posted. Send different text if you have more to say.",
34197
+ details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
34198
+ };
34199
+ }
34170
34200
  if (reply.body !== body) {
34171
34201
  return {
34172
34202
  text: `Message ${inReplyTo} was already answered by message ${reply.id}; Dispatch kept ` + "that reply and posted nothing. Wait for their next message rather than answering " + "this one again.",
34173
34203
  details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
34174
34204
  };
34175
34205
  }
34206
+ const readBack = `dispatch_read({message: "${inReplyTo}"}) reads the conversation back.`;
34207
+ const parent = reply.in_reply_to ?? undefined;
34208
+ const follows = parent === inReplyTo ? undefined : parent;
34176
34209
  return {
34177
- text: `Replied to message ${inReplyTo} with message ${reply.id}`,
34178
- details: { message: reply.id, in_reply_to: inReplyTo, posted: true }
34210
+ text: follows === undefined ? `Replied to message ${inReplyTo} with message ${reply.id}. ${readBack}` : `Replied to message ${inReplyTo} with message ${reply.id}, a follow-up threaded ` + `under your reply ${follows}. ${readBack}`,
34211
+ details: {
34212
+ message: reply.id,
34213
+ in_reply_to: inReplyTo,
34214
+ posted: true,
34215
+ ...follows === undefined ? {} : { follows }
34216
+ }
34179
34217
  };
34180
34218
  }
34181
34219
  const issueKey = issue2();
@@ -34347,6 +34385,21 @@ ${trailer.join(`
34347
34385
  };
34348
34386
  }
34349
34387
  case "dispatch_read": {
34388
+ const message = optionalString(args, "message");
34389
+ if (message !== undefined) {
34390
+ const sessionId = input.sessionId?.trim();
34391
+ if (!sessionId)
34392
+ throw new Error("host session id is required for dispatch_read({message})");
34393
+ const thread = await client.getMessageThread(messageIdOf(message), sessionId);
34394
+ const issueKey2 = thread.message.issue_key;
34395
+ return {
34396
+ text: messageSummary(thread, issueKey2 === null ? [] : await graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))),
34397
+ details: {
34398
+ message: thread.message.id,
34399
+ ...issueKey2 === null ? {} : { issue: issueKey2 }
34400
+ }
34401
+ };
34402
+ }
34350
34403
  if (ownerArguments.ref?.kind === "ask") {
34351
34404
  const ref = ownerArguments.ref;
34352
34405
  const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveDocument(ref.owner, ref.artifact)).artifact.id, "all"));
package/dist/legion.js CHANGED
@@ -30197,6 +30197,16 @@ var messageValidation = {
30197
30197
  },
30198
30198
  message: "issue is required unless in_reply_to names a message delivered to this session, which is the one message with no issue."
30199
30199
  };
30200
+ var readOwner = documentOwnerValidation(true);
30201
+ var readValidation = {
30202
+ check: (value) => {
30203
+ const input = value;
30204
+ if (typeof input.message !== "string")
30205
+ return readOwner.check(value);
30206
+ return input.issue === undefined && input.project === undefined && input.artifact === undefined && input.ref === undefined;
30207
+ },
30208
+ message: `${readOwner.message} message stands alone: it names the conversation, so name no issue, project, artifact, or ref with it.`
30209
+ };
30200
30210
  var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
30201
30211
  function componentsArgument(z2) {
30202
30212
  return z2.object({
@@ -30424,7 +30434,7 @@ var dispatchToolSpecs = [
30424
30434
  {
30425
30435
  name: "dispatch_message",
30426
30436
  example: { issue: "DSP-1", body: "Implementation started." },
30427
- description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation, and a second call with the same in_reply_to posts nothing because " + "Dispatch keeps the one reply per message. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30437
+ description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30428
30438
  arguments: (z2) => ({
30429
30439
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
30430
30440
  body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
@@ -30518,14 +30528,15 @@ var dispatchToolSpecs = [
30518
30528
  {
30519
30529
  name: "dispatch_read",
30520
30530
  example: { issue: "DSP-1" },
30521
- description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain. Do not use it for document " + "contents; use dispatch_doc_read instead. Supply ref, issue, or project plus artifact. " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
30531
+ description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
30522
30532
  arguments: (z2) => ({
30523
30533
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30524
30534
  project: z2.string().describe("Project key owning the document.").optional(),
30525
30535
  artifact: z2.string().describe("Project document artifact id, slug, or filename.").optional(),
30526
- ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional()
30536
+ ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
30537
+ message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional()
30527
30538
  }),
30528
- validation: documentOwnerValidation(true)
30539
+ validation: readValidation
30529
30540
  },
30530
30541
  {
30531
30542
  name: "dispatch_search",
@@ -31359,8 +31370,8 @@ class DispatchClient {
31359
31370
  async message(issue2, input) {
31360
31371
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "messages"], input);
31361
31372
  }
31362
- async messageReply(id, input) {
31363
- return this.#json("POST", ["api", "v1", "messages", id, "reply"], input);
31373
+ async messageReply(id, input, options = {}) {
31374
+ return this.#json("POST", ["api", "v1", "messages", id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
31364
31375
  }
31365
31376
  async getMessage(issue2, id) {
31366
31377
  return this.#json("GET", [
@@ -31372,6 +31383,9 @@ class DispatchClient {
31372
31383
  id
31373
31384
  ]);
31374
31385
  }
31386
+ async getMessageThread(id, session) {
31387
+ return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
31388
+ }
31375
31389
  async artifact(issue2, input) {
31376
31390
  const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
31377
31391
  if ("content" in input)
@@ -32046,6 +32060,13 @@ function argumentProblems(tool, args) {
32046
32060
  }
32047
32061
  break;
32048
32062
  }
32063
+ case "dispatch_read": {
32064
+ const message = optionalString(args, "message");
32065
+ if (message !== undefined && messageIdOf(message) === undefined) {
32066
+ problems.push("message must be a full message id (uuid) or a dispatch://KEY/message/<id> reference");
32067
+ }
32068
+ break;
32069
+ }
32049
32070
  }
32050
32071
  return problems;
32051
32072
  }
@@ -32183,6 +32204,9 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
32183
32204
  if (tool === "dispatch_message" && typeof replyTarget === "string" && !replyTarget.startsWith("dispatch://")) {
32184
32205
  return { args, ref, owner: null };
32185
32206
  }
32207
+ if (tool === "dispatch_read" && typeof args.message === "string") {
32208
+ return { args, ref, owner: null };
32209
+ }
32186
32210
  const legionIssue = env.LEGION_ISSUE;
32187
32211
  if (!legionIssue) {
32188
32212
  problems.push(ownerRequiredProblem);
@@ -33105,16 +33129,30 @@ ${followsAsk(askOwner)}`,
33105
33129
  const inReplyTo = messageInReplyTo(args);
33106
33130
  const body = stringArg(args, "body");
33107
33131
  if (owner === null && inReplyTo !== undefined) {
33108
- const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor });
33132
+ const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor }, { followUp: true });
33133
+ if (reply.duplicate === true && reply.body === body) {
33134
+ return {
33135
+ text: `Dispatch already has this exact text in the conversation (message ${reply.id}); ` + "nothing new was posted. Send different text if you have more to say.",
33136
+ details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
33137
+ };
33138
+ }
33109
33139
  if (reply.body !== body) {
33110
33140
  return {
33111
33141
  text: `Message ${inReplyTo} was already answered by message ${reply.id}; Dispatch kept ` + "that reply and posted nothing. Wait for their next message rather than answering " + "this one again.",
33112
33142
  details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
33113
33143
  };
33114
33144
  }
33145
+ const readBack = `dispatch_read({message: "${inReplyTo}"}) reads the conversation back.`;
33146
+ const parent = reply.in_reply_to ?? undefined;
33147
+ const follows = parent === inReplyTo ? undefined : parent;
33115
33148
  return {
33116
- text: `Replied to message ${inReplyTo} with message ${reply.id}`,
33117
- details: { message: reply.id, in_reply_to: inReplyTo, posted: true }
33149
+ text: follows === undefined ? `Replied to message ${inReplyTo} with message ${reply.id}. ${readBack}` : `Replied to message ${inReplyTo} with message ${reply.id}, a follow-up threaded ` + `under your reply ${follows}. ${readBack}`,
33150
+ details: {
33151
+ message: reply.id,
33152
+ in_reply_to: inReplyTo,
33153
+ posted: true,
33154
+ ...follows === undefined ? {} : { follows }
33155
+ }
33118
33156
  };
33119
33157
  }
33120
33158
  const issueKey = issue2();
@@ -33286,6 +33324,21 @@ ${trailer.join(`
33286
33324
  };
33287
33325
  }
33288
33326
  case "dispatch_read": {
33327
+ const message = optionalString(args, "message");
33328
+ if (message !== undefined) {
33329
+ const sessionId = input.sessionId?.trim();
33330
+ if (!sessionId)
33331
+ throw new Error("host session id is required for dispatch_read({message})");
33332
+ const thread = await client.getMessageThread(messageIdOf(message), sessionId);
33333
+ const issueKey2 = thread.message.issue_key;
33334
+ return {
33335
+ text: messageSummary(thread, issueKey2 === null ? [] : await graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))),
33336
+ details: {
33337
+ message: thread.message.id,
33338
+ ...issueKey2 === null ? {} : { issue: issueKey2 }
33339
+ }
33340
+ };
33341
+ }
33289
33342
  if (ownerArguments.ref?.kind === "ask") {
33290
33343
  const ref = ownerArguments.ref;
33291
33344
  const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveDocument(ref.owner, ref.artifact)).artifact.id, "all"));
@@ -33420,7 +33473,7 @@ import { logger } from "@oh-my-pi/pi-utils";
33420
33473
  // package.json
33421
33474
  var package_default = {
33422
33475
  name: "@sjawhar/pi-legion-envoy",
33423
- version: "5.16.8",
33476
+ version: "5.16.9",
33424
33477
  type: "module",
33425
33478
  omp: {
33426
33479
  extensions: [
@@ -894,15 +894,23 @@ dispatch_message({
894
894
 
895
895
  Leave `issue` out — there is no issue to post into, and naming one would file your answer on
896
896
  unrelated work. Dispatch threads the reply under their message in the same conversation, and the
897
- human sees it on your agent card. Every other message still names its issue, so keep the `issue`
897
+ human sees it in your conversation on the Agents page, where it shows as an unread reply until
898
+ they read it. Every other message still names its issue, so keep the `issue`
898
899
  the frame gave you whenever it gave you one; a `dispatch://KEY/message/<id>` reference names the
899
900
  issue its message lives on, so that form is a reply on that issue, not a direct message.
900
901
 
901
- One reply per message they send. A second call with the same `in_reply_to` posts nothing:
902
- Dispatch answers it with the reply already stored, and the tool result says the message was
903
- already answered rather than reporting a send. That happens most often when your host answered
904
- the **BTW** automatically before you got here — read the result before writing again, and wait
905
- for their next message instead.
902
+ Have more to say after you answered? Call it again with the same `in_reply_to` and the new text:
903
+ Dispatch threads that follow-up under your first reply, and the tool result names the reply it
904
+ follows. The same text again posts nothing, so a retry is safe. Your host may already have
905
+ answered a **BTW** automatically before you got here; a second call is then your follow-up to
906
+ that answer, so read the result before writing again.
907
+
908
+ Read the whole conversation back — their message and every reply, yours included — with the
909
+ message id alone; it has no issue:
910
+
911
+ ```ts
912
+ dispatch_read({ message: "<the direct message's id, or any reply's>" })
913
+ ```
906
914
 
907
915
  ## Following
908
916
 
@@ -188,11 +188,17 @@ reviewer, or an architect (in Legion's own deployment, `legion-implementer[bot]`
188
188
  check
189
189
  `jj -R "$LEGION_WORKSPACE" log -r 'main@origin..@' -T 'author.email() ++ " | " ++ committer.email() ++ " " ++ description.first_line() ++ "\n"'`
190
190
  shows your role's App in both columns **on every commit you made** — not on the whole list:
191
- earlier phases' commits are legitimately authored by their own role's App, and a conflict-forced
192
- rebase legitimately sets the committer of every rebased commit, other roles' included, to the
193
- rebaser. A wrong identity on your own commit, the other App or none, is a pane-environment
194
- problem to report to the architect, not something to pin
195
- (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`, Hazard 1).
191
+ earlier phases' commits are legitimately authored by their own role's App. Their *committer* is
192
+ a different matter, and no longer noise to accept. Resolving a conflict rewrites nothing now —
193
+ it is the forward merge above — so it changes no committer at all, and the one rewrite still
194
+ open to you (*Rewriting pushed commits*, below) resets the committer only of commits on your own
195
+ chain that descend from the commit you named, after its guard cleared. Another role's commit
196
+ carrying you as committer, which you did not rewrite that way, is evidence that something
197
+ rewrote commits it should not have — the observable symptom of LEGION-118. Stop and send the
198
+ architect that log; do not accept it as a side effect. A wrong identity on your own commit, the
199
+ other App or none, is a pane-environment problem to report to the architect, not something to
200
+ pin (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`,
201
+ Hazard 1).
196
202
  Your session receives the credential capability it needs; invoke GitHub through the
197
203
  credential helper:
198
204
 
@@ -390,14 +396,17 @@ this proof.
390
396
  changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
391
397
  Findings about naming, duplication, or wording are batched into the single `Fast-follow`
392
398
  line instead of iterating per push.
393
- - **Rebase only on a real conflict, except after a base retarget.** Sami, 2026-09-11, verbatim:
399
+ - **Reintegrate the base only on a real conflict, except after a base retarget — and with a
400
+ merge, never `jj rebase`.** Sami, 2026-09-11, verbatim:
394
401
  "Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
395
- The implementer rebases the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
396
- because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never rebase to
402
+ The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
403
+ because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never reintegrate the base to
397
404
  pick up `main` or refresh CI. A single failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
398
405
  pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
399
- confirmation, not a new round (see *The unchanged-diff check* below). Before rebasing, record
400
- the fingerprint at the current tip; after pushing the rebased branch, record it at the new
406
+ confirmation, not a new round (see *The unchanged-diff check* below); that name is the event's,
407
+ kept by the rules below and the learnings that cite it, and the operation it names is always
408
+ the merge here. Before merging, record
409
+ the fingerprint at the current tip; after pushing the merged branch, record it at the new
401
410
  tip; post one PR comment (Legion footer):
402
411
  `rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
403
412
  Every issue workspace is a `jj workspace` of the same shared repository and operation log, and
@@ -646,12 +655,19 @@ remote branch sideways onto your commit and drops theirs (jj 0.45.1:
646
655
  push is refused by jj itself (`unexpectedly moved on the remote`).
647
656
 
648
657
  **Rewriting pushed commits** — a `jj squash --into` a commit already on GitHub, or any other
649
- rewrite of a commit you already pushed — leaves the pushed tip outside `::@-`, so record
650
- that tip first, after a fetch and while your chain still descends from it:
658
+ rewrite of a commit you already pushed — is the LEGION-118 hazard in a second shape: jj rebases
659
+ every descendant of any commit it rewrites, and in the one shared repository a descendant can be
660
+ another tree's branch stacked on your pushed commit, which then moves, with its bookmark, onto a
661
+ rewritten copy. So look for a descendant outside your own chain first, and record the pushed tip
662
+ — which the rewrite leaves outside `::@-` — after a fetch and while your chain still descends
663
+ from it:
651
664
 
652
665
  ```bash
653
666
  cd -- "$LEGION_WORKSPACE" && \
654
667
  jj -R "$LEGION_WORKSPACE" git fetch && \
668
+ foreign=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
669
+ -r 'descendants(<the commit you are about to rewrite>) ~ ::@') && \
670
+ { [ -z "$foreign" ] || { echo "not mine, and descends from the commit to rewrite: $foreign" >&2; false; }; } && \
655
671
  behind=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
656
672
  -r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin") ~ ::@-') && \
657
673
  { [ -z "$behind" ] || { echo "legion/<KEY>@origin is at $behind, which @- does not descend from" >&2; false; }; } && \
@@ -660,6 +676,13 @@ cd -- "$LEGION_WORKSPACE" && \
660
676
  >"${TMPDIR:-/tmp}/legion-<KEY>-$LEGION_ROLE-rewritten-tip"
661
677
  ```
662
678
 
679
+ `descendants(<commit>) ~ ::@` is everything built on the commit you are about to rewrite that is
680
+ not on your own chain. Non-empty means the rewrite would move work that is not yours: do not
681
+ rewrite it. Put the change in a new commit on top instead, and report the listed commits to the
682
+ architect. On a two-workspace rig of this shape a `jj squash --into` a pushed commit reported
683
+ `Rebased 13 descendant commits` and moved a second issue's twelve commits and its bookmark; the
684
+ check above listed those thirteen and refused before anything moved.
685
+
663
686
  Then rewrite, resolve, and push with the procedure above. It lets the remote branch sit on the
664
687
  tip you recorded, which the rewrite replaced, and on nothing else: when another role pushed after
665
688
  you recorded it, the push is refused. The push deletes the file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.16.8",
3
+ "version": "5.16.9",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [