@sjawhar/pi-legion-envoy 1.69.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/envoy.js +10 -4
- package/dist/legion.js +26 -20
- package/dist/skills/dispatch/SKILL.md +22 -6
- package/dist/skills/legion-worker/SKILL.md +10 -7
- package/package.json +2 -2
package/dist/envoy.js
CHANGED
|
@@ -30229,7 +30229,7 @@ var dispatchToolSpecs = [
|
|
|
30229
30229
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
30230
30230
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
30231
30231
|
},
|
|
30232
|
-
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " +
|
|
30232
|
+
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose. ` + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
30233
30233
|
arguments: (z2) => ({
|
|
30234
30234
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
30235
30235
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -30237,8 +30237,8 @@ var dispatchToolSpecs = [
|
|
|
30237
30237
|
ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
|
|
30238
30238
|
ops: z2.array(z2.object({
|
|
30239
30239
|
op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
|
|
30240
|
-
find: z2.string().describe("Text of the target as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading. A delete of a block's entire text removes the block.").optional(),
|
|
30241
|
-
with: z2.string().describe("Replacement text for replace, parsed as inline markdown within the matched block; a
|
|
30240
|
+
find: z2.string().describe("Text of the target as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated and must be balanced; a leading '# ' matches a heading. A delete of a block's entire text removes the block.").optional(),
|
|
30241
|
+
with: z2.string().describe("Replacement text for replace, parsed as inline markdown within the matched block; a marker of a different kind from the block's own is literal text, one of the same kind is refused unless it is a heading rename (where a level named by find is what lets with change it), a backslash escape keeps prose that merely looks like a marker, and any non-empty value that renders to no text is refused - only an empty value deletes the match.").optional(),
|
|
30242
30242
|
occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
|
|
30243
30243
|
markdown: z2.string().describe("Markdown to insert.").optional(),
|
|
30244
30244
|
after: z2.string().describe(`Insert or move after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>", "block:<id>".`).optional(),
|
|
@@ -34079,7 +34079,12 @@ ${followsAsk(askOwner)}`,
|
|
|
34079
34079
|
});
|
|
34080
34080
|
const retyped = ops.filter((operation) => operation.op === "retype").length;
|
|
34081
34081
|
const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
|
|
34082
|
-
const
|
|
34082
|
+
const retypedText = retyped === 0 ? "" : `; retyped ${retyped} block${retyped === 1 ? "" : "s"}`;
|
|
34083
|
+
const nothingChanged = edited.changed === false;
|
|
34084
|
+
const head = nothingChanged ? `Applied ${edited.applied} ops${retypedText}; nothing changed (${versionText})` : `Applied ${edited.applied} ops${retypedText} (${versionText})`;
|
|
34085
|
+
const unchangedOps = edited.unchanged_ops ?? [];
|
|
34086
|
+
const unchangedText = unchangedOps.length === 0 ? "" : `; ${unchangedOps.length === 1 ? "operation" : "operations"} ${unchangedOps.join(", ")} changed nothing`;
|
|
34087
|
+
const applied = `${head}${unchangedText}`;
|
|
34083
34088
|
const adviceLines = renderAdvice(input.tool, resolvedTopic(resolved).label, edited.advice, {});
|
|
34084
34089
|
return {
|
|
34085
34090
|
text: [`${applied} ${notSubscribed(resolvedTopic(resolved))}`, ...adviceLines].join(`
|
|
@@ -34087,6 +34092,7 @@ ${followsAsk(askOwner)}`,
|
|
|
34087
34092
|
details: writeResultDetails(resolved, {
|
|
34088
34093
|
applied: edited.applied,
|
|
34089
34094
|
...edited.version === null ? {} : { version: edited.version.number },
|
|
34095
|
+
...nothingChanged ? { changed: false } : {},
|
|
34090
34096
|
...edited.advice === undefined ? {} : { advice: edited.advice }
|
|
34091
34097
|
})
|
|
34092
34098
|
};
|
package/dist/legion.js
CHANGED
|
@@ -16111,7 +16111,7 @@ var require_mod4 = __commonJS((exports) => {
|
|
|
16111
16111
|
|
|
16112
16112
|
// extensions/legion.ts
|
|
16113
16113
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
16114
|
-
import
|
|
16114
|
+
import path4 from "path";
|
|
16115
16115
|
|
|
16116
16116
|
// ../envoy-client/src/defaults.ts
|
|
16117
16117
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
@@ -16142,7 +16142,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
16142
16142
|
// package.json
|
|
16143
16143
|
var package_default = {
|
|
16144
16144
|
name: "@sjawhar/pi-legion-envoy",
|
|
16145
|
-
version: "
|
|
16145
|
+
version: "2.0.1",
|
|
16146
16146
|
type: "module",
|
|
16147
16147
|
omp: {
|
|
16148
16148
|
extensions: [
|
|
@@ -16155,7 +16155,7 @@ var package_default = {
|
|
|
16155
16155
|
},
|
|
16156
16156
|
legion: {
|
|
16157
16157
|
daemonApiVersion: 8,
|
|
16158
|
-
goDaemonApiVersion:
|
|
16158
|
+
goDaemonApiVersion: 5
|
|
16159
16159
|
},
|
|
16160
16160
|
repository: {
|
|
16161
16161
|
type: "git",
|
|
@@ -30301,7 +30301,7 @@ var dispatchToolSpecs = [
|
|
|
30301
30301
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
30302
30302
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
30303
30303
|
},
|
|
30304
|
-
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " +
|
|
30304
|
+
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose. ` + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
30305
30305
|
arguments: (z2) => ({
|
|
30306
30306
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
30307
30307
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -30309,8 +30309,8 @@ var dispatchToolSpecs = [
|
|
|
30309
30309
|
ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
|
|
30310
30310
|
ops: z2.array(z2.object({
|
|
30311
30311
|
op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
|
|
30312
|
-
find: z2.string().describe("Text of the target as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading. A delete of a block's entire text removes the block.").optional(),
|
|
30313
|
-
with: z2.string().describe("Replacement text for replace, parsed as inline markdown within the matched block; a
|
|
30312
|
+
find: z2.string().describe("Text of the target as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated and must be balanced; a leading '# ' matches a heading. A delete of a block's entire text removes the block.").optional(),
|
|
30313
|
+
with: z2.string().describe("Replacement text for replace, parsed as inline markdown within the matched block; a marker of a different kind from the block's own is literal text, one of the same kind is refused unless it is a heading rename (where a level named by find is what lets with change it), a backslash escape keeps prose that merely looks like a marker, and any non-empty value that renders to no text is refused - only an empty value deletes the match.").optional(),
|
|
30314
30314
|
occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
|
|
30315
30315
|
markdown: z2.string().describe("Markdown to insert.").optional(),
|
|
30316
30316
|
after: z2.string().describe(`Insert or move after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>", "block:<id>".`).optional(),
|
|
@@ -31308,7 +31308,7 @@ function createControllerSession(persistedTranscript, checkSubagentSession, daem
|
|
|
31308
31308
|
|
|
31309
31309
|
// src/legion/grant-file.ts
|
|
31310
31310
|
import { randomUUID } from "crypto";
|
|
31311
|
-
import { chmod, rename, rm, writeFile } from "fs/promises";
|
|
31311
|
+
import { chmod, mkdir, rename, rm, writeFile } from "fs/promises";
|
|
31312
31312
|
import path from "path";
|
|
31313
31313
|
async function writeGrantFile(file2, grantId) {
|
|
31314
31314
|
if (!path.isAbsolute(file2)) {
|
|
@@ -31316,6 +31316,7 @@ async function writeGrantFile(file2, grantId) {
|
|
|
31316
31316
|
}
|
|
31317
31317
|
const temp = `${file2}.${process.pid}.${randomUUID()}`;
|
|
31318
31318
|
try {
|
|
31319
|
+
await mkdir(path.dirname(file2), { recursive: true, mode: 448 });
|
|
31319
31320
|
await writeFile(temp, grantId, { encoding: "utf8", mode: 384 });
|
|
31320
31321
|
await chmod(temp, 384);
|
|
31321
31322
|
await rename(temp, file2);
|
|
@@ -31332,9 +31333,6 @@ async function writeMintedGrant(mint) {
|
|
|
31332
31333
|
await writeGrantFile(file2, await mint());
|
|
31333
31334
|
}
|
|
31334
31335
|
|
|
31335
|
-
// src/legion/go-bootstrap.ts
|
|
31336
|
-
import path3 from "path";
|
|
31337
|
-
|
|
31338
31336
|
// ../contracts/src/legion-go-api.ts
|
|
31339
31337
|
var nonEmptyString2 = exports_external.string().min(1);
|
|
31340
31338
|
var timestamp = exports_external.iso.datetime({ offset: true });
|
|
@@ -31662,7 +31660,7 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
31662
31660
|
|
|
31663
31661
|
// src/legion/jj-attribution.ts
|
|
31664
31662
|
import { execFile } from "child_process";
|
|
31665
|
-
import { mkdir, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
31663
|
+
import { mkdir as mkdir2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
31666
31664
|
import path2 from "path";
|
|
31667
31665
|
import { promisify } from "util";
|
|
31668
31666
|
var UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
@@ -31695,7 +31693,7 @@ async function exportJjSessionAttribution(sessionFile, stateDir, env = process.e
|
|
|
31695
31693
|
if (!base)
|
|
31696
31694
|
return;
|
|
31697
31695
|
try {
|
|
31698
|
-
await
|
|
31696
|
+
await mkdir2(stateDir, { recursive: true });
|
|
31699
31697
|
const tmp = `${overlay}.${process.pid}.tmp`;
|
|
31700
31698
|
await writeFile2(tmp, `[templates]
|
|
31701
31699
|
commit_trailers = '"Omp-Session: ${id}"'
|
|
@@ -31799,7 +31797,6 @@ async function bootstrapGoClaim(context, state) {
|
|
|
31799
31797
|
try {
|
|
31800
31798
|
const stateDir = requiredEnvironment(process.env, "LEGION_STATE_DIR");
|
|
31801
31799
|
await exportJjSessionAttribution(sessionFile, stateDir);
|
|
31802
|
-
process.env.LEGION_GRANT_FILE = path3.join(stateDir, "secrets", `${claim.claimToken}-grant`);
|
|
31803
31800
|
state.setCapability({
|
|
31804
31801
|
kind: "phase-worker",
|
|
31805
31802
|
sessionID,
|
|
@@ -32369,7 +32366,7 @@ function createLegionTool(deps) {
|
|
|
32369
32366
|
|
|
32370
32367
|
// src/subagent-session.ts
|
|
32371
32368
|
import fs from "fs";
|
|
32372
|
-
import
|
|
32369
|
+
import path3 from "path";
|
|
32373
32370
|
import * as host from "@oh-my-pi/pi-coding-agent";
|
|
32374
32371
|
var LEGION_BOOTSTRAPPED_SESSION = Symbol.for("legion.pi-envoy.bootstrapped-session");
|
|
32375
32372
|
var bootstrappedSessionStore = globalThis;
|
|
@@ -32380,7 +32377,7 @@ async function isSubagentSession(context) {
|
|
|
32380
32377
|
await context.sessionManager.ensureOnDisk();
|
|
32381
32378
|
const sessionFile = context.sessionManager.getSessionFile();
|
|
32382
32379
|
if (sessionFile !== undefined && fs.existsSync(sessionFile)) {
|
|
32383
|
-
return fs.existsSync(`${
|
|
32380
|
+
return fs.existsSync(`${path3.dirname(sessionFile)}.jsonl`);
|
|
32384
32381
|
}
|
|
32385
32382
|
const bootstrapped = bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
|
|
32386
32383
|
return bootstrapped !== undefined && bootstrapped !== sessionFile;
|
|
@@ -32444,7 +32441,16 @@ var callReadyWithRetry = async (label, call) => {
|
|
|
32444
32441
|
}
|
|
32445
32442
|
}
|
|
32446
32443
|
};
|
|
32447
|
-
|
|
32444
|
+
var GH_RESOLVED_URL = /(?:^|[\s;,"])(?:pr|issue):\/\//i;
|
|
32445
|
+
function needsGrant({ toolName, input }) {
|
|
32446
|
+
if (toolName === "bash")
|
|
32447
|
+
return typeof input.command === "string";
|
|
32448
|
+
if (toolName === "github")
|
|
32449
|
+
return true;
|
|
32450
|
+
const paths = Array.isArray(input.paths) ? input.paths : [input.path];
|
|
32451
|
+
return paths.some((entry) => typeof entry === "string" && GH_RESOLVED_URL.test(entry));
|
|
32452
|
+
}
|
|
32453
|
+
async function wrapWithGrant(mint) {
|
|
32448
32454
|
try {
|
|
32449
32455
|
await writeMintedGrant(async () => (await mint()).grantId);
|
|
32450
32456
|
return;
|
|
@@ -32458,7 +32464,7 @@ async function persistedTranscript(context) {
|
|
|
32458
32464
|
if (!sessionFile?.endsWith(".jsonl")) {
|
|
32459
32465
|
throw new Error("Legion session must have a persisted transcript");
|
|
32460
32466
|
}
|
|
32461
|
-
const agentId =
|
|
32467
|
+
const agentId = path4.basename(sessionFile, ".jsonl");
|
|
32462
32468
|
if (!agentId)
|
|
32463
32469
|
throw new Error("Legion session transcript has no agent id");
|
|
32464
32470
|
return { sessionFile, agentId };
|
|
@@ -32970,11 +32976,11 @@ function legionExtension(pi) {
|
|
|
32970
32976
|
return { block: true, reason: "the merger only verifies and reports" };
|
|
32971
32977
|
}
|
|
32972
32978
|
}
|
|
32973
|
-
if (toolCall
|
|
32979
|
+
if (!needsGrant(toolCall))
|
|
32974
32980
|
return;
|
|
32975
32981
|
if (active === undefined) {
|
|
32976
32982
|
if (controllerSession.isClaimedSession(sessionID)) {
|
|
32977
|
-
return
|
|
32983
|
+
return wrapWithGrant(() => controllerSession.mintGrant(sessionID));
|
|
32978
32984
|
}
|
|
32979
32985
|
if (process.env.LEGION_ROLE !== undefined && process.env.LEGION_CONTROLLER !== "1") {
|
|
32980
32986
|
return {
|
|
@@ -32984,7 +32990,7 @@ function legionExtension(pi) {
|
|
|
32984
32990
|
}
|
|
32985
32991
|
return;
|
|
32986
32992
|
}
|
|
32987
|
-
return
|
|
32993
|
+
return wrapWithGrant(() => process.env.LEGION_DAEMON_API === "go" ? goRoleDaemon().grant({
|
|
32988
32994
|
tree: active.tree,
|
|
32989
32995
|
issue: active.issue,
|
|
32990
32996
|
sessionId: sessionID,
|
|
@@ -507,8 +507,8 @@ omitted `artifact` reads the issue specification; a project needs `artifact`; an
|
|
|
507
507
|
```ts
|
|
508
508
|
dispatch_doc_edit({ issue?, project?, artifact, ops, precondition?, summary? })
|
|
509
509
|
```
|
|
510
|
-
It returns issue or project-document owner details plus `applied
|
|
511
|
-
exact `EditOp` shape:
|
|
510
|
+
It returns issue or project-document owner details plus `applied`, optional `version`, `changed`, and
|
|
511
|
+
`unchanged_ops`. `ops` is an array of this exact `EditOp` shape:
|
|
512
512
|
|
|
513
513
|
```ts
|
|
514
514
|
type EditOp = {
|
|
@@ -545,12 +545,28 @@ canonicalizes short ragged rows by padding missing cells, so column deletion pre
|
|
|
545
545
|
`GET /api/v1/artifacts/<artifact UUID>/blocks` reports a table's own references plus its descendant cell anchors. A row or column
|
|
546
546
|
deletion that would remove an open ask or unresolved comment anchor is `INVALID_OP` on `index`, naming the axis and anchor ids;
|
|
547
547
|
answered asks and resolved comments are history and do not block it. A `find` or quote anchor tolerates inline Markdown
|
|
548
|
-
(`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the three nearest blocks so
|
|
549
|
-
lands
|
|
550
|
-
|
|
551
|
-
|
|
548
|
+
(`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the quote and the three nearest blocks so
|
|
549
|
+
the next quote lands, and a `find` cut before a closing `**` or `` ` `` is refused as an unbalanced inline mark rather than reported
|
|
550
|
+
as a miss. A `heading:` anchor matches the whole heading text exactly — a prefix of a longer heading is a miss, naming the anchor and
|
|
551
|
+
the nearest headings. `replace` is inline: `with` is the new text of the matched span inside its block, so a marker of a *different*
|
|
552
|
+
kind from the block's own (`4. Design` written into a heading, `# Title` into a paragraph) stays literal text and never turns the
|
|
553
|
+
block into a list or heading. A `with` that opens with a marker of the *same* kind as the matched block's own would write it twice and
|
|
554
|
+
is rejected (`INVALID_OP` on `with`) — including prose that merely looks like a marker (`1999. was a year` into an ordered item),
|
|
555
|
+
which is written as text with a backslash escape (`1999\. was a year`) — omit the marker to replace the block's text, or use `insert`
|
|
556
|
+
plus `delete` to change the block's kind, level or number. The one exception is a heading rename whose `find` carried a heading
|
|
557
|
+
marker: `replace(find="## Old", with="## New")` gives `## New`. A different level in `with` applies only when `find` named the
|
|
558
|
+
heading's actual level — `find="## Old"`, `with="### New"` retitles and makes it an h3 — because `# ` is the level-blind selector,
|
|
559
|
+
so `find="# Old"` renames the text and keeps whatever level it selected. `with` that forms more than one
|
|
560
|
+
paragraph is rejected (`INVALID_OP` on `with`) — delete the block and insert new blocks instead; so is any non-empty `with` that
|
|
561
|
+
renders to no text, which a line indented four spaces or a tab does (markdown reads that as a code block), as does whitespace
|
|
562
|
+
alone. An empty `with` is the one that deletes the matched text on purpose. Use zero-based `occurrence` for a
|
|
552
563
|
repeated target; re-read a missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
|
|
553
564
|
|
|
565
|
+
A batch that leaves the document's semantic identity unchanged — including its inline anchor marks, so an edit that only orphans a
|
|
566
|
+
comment or ask anchor still mints its version — mints no version, named or not: the response carries
|
|
567
|
+
`changed: false` with `unchanged_ops` naming each operation that did nothing, and the tool result says nothing changed. A `summary`
|
|
568
|
+
does not force a version for such a batch; `POST /api/v1/artifacts/<id>/versions`, which names the current state on purpose, still does.
|
|
569
|
+
|
|
554
570
|
A `delete` whose `find` is a block's entire text removes the block itself — the bullet, paragraph, or heading, not just its words — and
|
|
555
571
|
a list emptied of every item disappears with it; a partial match keeps the block with its remaining text. Deleting the text of a bullet
|
|
556
572
|
that holds a nested list hoists that list's items into the bullet's place (as an outliner does); a bullet with any other content
|
|
@@ -189,14 +189,17 @@ comments live on that path too, so edit them with `gh pr comment`) — printing
|
|
|
189
189
|
`Legion issues live on Dispatch; use dispatch_message or dispatch_comment on <your LEGION_ISSUE>`:
|
|
190
190
|
Legion never reads or writes a GitHub issue (LEGION-78). `pr comment`, `pr review`,
|
|
191
191
|
`api …/pulls/…`, `api graphql`, and issue reads are unaffected. The credential reaches `legion`
|
|
192
|
-
through the file `$LEGION_GRANT_FILE` names, written before each of your bash
|
|
193
|
-
|
|
192
|
+
through the file `$LEGION_GRANT_FILE` names, written by the extension before each of your bash
|
|
193
|
+
commands, each `github` tool call, and each `read`/`grep` of a `pr://` or `issue://` URL (and by
|
|
194
|
+
the `legion` tool before its `handoff_complete`); never `cat`, `echo`, copy, or
|
|
194
195
|
`export` it — `legion credential`, `legion gh`, `jj git push`, and `handoff_complete` read it
|
|
195
|
-
themselves. The file is the pane's, not the
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
seconds of that call and 403s afterwards — a timing
|
|
199
|
-
credentialed commands from your own bash calls
|
|
196
|
+
themselves. The file is the pane's, not the command's, and a grant lives 60 seconds: a `task`
|
|
197
|
+
subagent, an `eval` subprocess, or a background job in your pane reads the grant your last such
|
|
198
|
+
call wrote, and a `github` tool `run_watch` keeps polling `gh` on the one written when the call
|
|
199
|
+
began, so each succeeds only within 60 seconds of that call and 403s afterwards — a timing
|
|
200
|
+
artifact, not a broken credential. Run credentialed commands from your own bash calls, and watch a
|
|
201
|
+
run that may outlast a minute with `gh run watch` in bash, which redeems once and then runs on the
|
|
202
|
+
token it got.
|
|
200
203
|
|
|
201
204
|
## GitHub PR comment attribution
|
|
202
205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion-envoy",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"omp": {
|
|
6
6
|
"extensions": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
15
|
"daemonApiVersion": 8,
|
|
16
|
-
"goDaemonApiVersion":
|
|
16
|
+
"goDaemonApiVersion": 5
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|