@sjawhar/pi-legion-envoy 1.24.2 → 1.24.4

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
@@ -29972,7 +29972,7 @@ var dispatchToolSpecs = [
29972
29972
  },
29973
29973
  {
29974
29974
  name: "dispatch_doc_edit",
29975
- description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29975
+ description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "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 insert anchors, find text exactly as rendered: omit Markdown markers such as backticks or asterisks. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29976
29976
  arguments: (z) => ({
29977
29977
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29978
29978
  project: z.string().describe("Project key owning the document.").optional(),
package/dist/legion.js CHANGED
@@ -29972,7 +29972,7 @@ var dispatchToolSpecs = [
29972
29972
  },
29973
29973
  {
29974
29974
  name: "dispatch_doc_edit",
29975
- description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29975
+ description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "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 insert anchors, find text exactly as rendered: omit Markdown markers such as backticks or asterisks. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29976
29976
  arguments: (z) => ({
29977
29977
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29978
29978
  project: z.string().describe("Project key owning the document.").optional(),
@@ -30852,7 +30852,7 @@ function createLegionTool(deps) {
30852
30852
  return {
30853
30853
  name: "legion",
30854
30854
  label: "legion",
30855
- description: "Perform a Legion lifecycle write through the Legion daemon. " + "register_gate records the root spec document a human must approve: `artifactId` is the " + "document id (a UUID) and `version` the version number, both copied from the `artifact` and " + "`version` fields of dispatch_request_approval's result \u2014 never the slug or file name you " + "passed to that tool. " + `spawn_worker's response "status" means: "spawned" \u2014 a fresh pane just opened and is ` + 'running now; "resumed" \u2014 an existing worker was prompted directly over its live socket, ' + "or (if its boot has not confirmed yet) its task was recorded to deliver once that boot " + 'completes; "queued" \u2014 the running-worker cap is full, the task was recorded and this ' + 'role will start on its own once a slot frees. Never re-spawn a role after "resumed" or ' + '"queued" \u2014 wait for the worker-started notification instead.',
30855
+ description: "Perform a Legion lifecycle write through the Legion daemon. " + "register_gate records the root spec document a human must approve: `artifactId` is the " + "document id (a UUID) and `version` the version number, both copied from the `artifact` and " + "`version` fields of dispatch_request_approval's result \u2014 never the slug or file name you " + "passed to that tool. " + `spawn_worker's response "status" means: "spawned" \u2014 a fresh pane just opened and is ` + 'running now; "resumed" \u2014 an existing worker was prompted directly over its live socket ' + "and its turn started, or (if its boot has not confirmed yet) its task was recorded to " + 'deliver once that boot completes; "queued" \u2014 the task was recorded and this role will ' + "start on its own: either the running-worker cap is full, or the live worker acknowledged " + "the task without starting a turn and the daemon is retrying it. Never re-spawn a role " + 'after "resumed" or "queued" \u2014 wait for the worker-started notification instead.',
30856
30856
  defaultInactive: true,
30857
30857
  parameters: legionToolSchema(pi),
30858
30858
  execute: async (_id, parameters, _signal, _onUpdate, context) => {
@@ -30961,25 +30961,6 @@ function createLegionTool(deps) {
30961
30961
  };
30962
30962
  }
30963
30963
 
30964
- // src/legion/workspace-helpers.ts
30965
- async function setJjIdentity(cwd, gitName, gitEmail) {
30966
- for (const [key, value] of [
30967
- ["user.name", gitName],
30968
- ["user.email", gitEmail]
30969
- ]) {
30970
- const child = Bun.spawn(["jj", "config", "set", "--repo", key, JSON.stringify(value)], {
30971
- cwd,
30972
- stdout: "ignore",
30973
- stderr: "pipe"
30974
- });
30975
- const exitCode = await child.exited;
30976
- if (exitCode === 0)
30977
- continue;
30978
- const stderr = await new Response(child.stderr).text();
30979
- throw new Error(`jj config set ${key} failed: ${stderr.trim()}`);
30980
- }
30981
- }
30982
-
30983
30964
  // extensions/envoy.ts
30984
30965
  import { existsSync } from "fs";
30985
30966
  import { dirname, resolve } from "path";
@@ -35047,7 +35028,6 @@ function legionExtension(pi) {
35047
35028
  if (bootstrap)
35048
35029
  return bootstrap;
35049
35030
  const bootToken = requiredSecret(process.env, "LEGION_BOOT_TOKEN");
35050
- const workspace = requiredEnvironment(process.env, "LEGION_WORKSPACE");
35051
35031
  bootstrap = (async () => {
35052
35032
  const { sessionFile, agentId } = await persistedTranscript(context);
35053
35033
  const started = await (async () => {
@@ -35071,7 +35051,6 @@ function legionExtension(pi) {
35071
35051
  })();
35072
35052
  try {
35073
35053
  await exportJjSessionAttribution(sessionFile, requiredEnvironment(process.env, "LEGION_STATE_DIR"));
35074
- await setJjIdentity(workspace, started.gitName, started.gitEmail);
35075
35054
  capability = {
35076
35055
  kind: "phase-worker",
35077
35056
  sessionID,
@@ -34,8 +34,11 @@ durable human question; replies come back to the worker's own session.
34
34
  ## Durable artifacts
35
35
 
36
36
  Phase handoffs are committed in lifecycle order: architect, plan, implement, test, and review.
37
+ Only the implementer pushes them: it and the merger act as the code-writing GitHub App, while
38
+ the planner, tester, reviewer, and architects act as the review App (`appRoleForLegionRole`,
39
+ `packages/daemon/src/daemon/github-apps.ts`), which holds no `contents` permission — their
40
+ handoff commits stay on the shared workspace's issue branch and ride the implementer's next push.
37
41
  A clean review ends with the `.legion/` deletion pushed by the implementer at the reviewer's
38
- direction (the review App holds no `contents` permission and cannot push), which the reviewer
39
- then approves; retro records its learning in
42
+ direction, which the reviewer then approves; retro records its learning in
40
43
  `docs/solutions/` and writes no handoff. GitHub comments and reviews carry the required Legion
41
44
  footer so the daemon can attribute artifacts to their worker session.
@@ -243,9 +243,10 @@ type EditOp = {
243
243
  };
244
244
  ```
245
245
 
246
- Target `replace` and `delete` by the document's plain text: inline-code and link text match without Markdown syntax, and a table-cell
247
- anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay within one textblock; split changes
248
- that span separate blocks into separate operations.
246
+ Target `replace`, `delete`, and quote insert anchors by a block's text as rendered: write inline
247
+ code without backticks, bold without asterisks, and link text without link syntax. A table-cell
248
+ anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay
249
+ within one textblock; split changes that span separate blocks into separate operations.
249
250
 
250
251
  `replace` requires `find` and `with`; `delete` requires `find`; `insert` requires `markdown` and exactly one of `after` or `before`. An
251
252
  insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Ordinary inserts create a sibling block before or after the quote or
@@ -286,6 +287,11 @@ Should we ship the migration?
286
287
  :::
287
288
  ```
288
289
 
290
+ When a human answers a decision written as an ask block, the answer lives on that ask. Use
291
+ `dispatch_resolve_ask` when the decision is resolved without a human response, or preserve the
292
+ human's answer; never rewrite the question into its answer or blank its options. An edit that leaves
293
+ an ask block without a question or with a blank option is rejected with `INVALID_ASK_BLOCK`.
294
+
289
295
  ## Comments and suggestions
290
296
 
291
297
  Add feedback with:
@@ -290,7 +290,7 @@ corresponding lifecycle procedure.
290
290
  | `design-approved` | Payload `{type:"design-approved"}`. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
291
291
  | `design-changes-requested` | Payload `{type:"design-changes-requested", version, reason, author?}`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec, call `dispatch_request_approval` again, and stay parked; the gate is closed. |
292
292
  | `phase-complete` | Payload `{type:"phase-complete", issue, role, summary}`. May arrive live or via `catchup-overseer`'s `phaseCompletions`. Read the committed handoff for that phase, then spawn the next phase's owner, or `spawn_worker` on the same role again to resume it with corrections if the handoff shows unresolved gaps. A `reviewer` completion whose GitHub review is `CHANGES_REQUESTED` (the daemon returns the issue's Dispatch status to `in_progress` for this, on the reviewer's completion and again when you spawn the corrective implementer unless the daemon already knows the issue is `in_progress`) means `spawn_worker` the **implementer** again with the review findings — thread URLs and blocking items — as its task, then route back through tester and reviewer in order; never `spawn_worker` the reviewer directly off this wake and never proceed to retro on this verdict. A reviewer completion with an `APPROVED` review proceeds to retro (step 5). A `reviewer` completion after a conflict-forced rebase whose review body names an unchanged fingerprint is a confirmation, not a round: if retro already completed, `spawn_worker` the merger; otherwise resume the step you were on. |
293
- | `worker-queued` | Payload `{type:"worker-queued", issue, role}`. The deployment's worker cap is full; this role's spawn is queued. Do not respawn or retry — wait for `worker-started`. |
293
+ | `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. |
294
294
  | `worker-started` | Payload `{type:"worker-started", issue, role}`. A previously queued role has been promoted and is now running. Treat it exactly as a normal spawn: resume tracking that role's live session. |
295
295
  | `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
296
296
  | `pr-review` | Payload `{type:"pr-review", state, author, body}`. Delivered to whichever role is currently active for the issue, falling back to you when no worker phase is active. Follows the same verdict rule as a reviewer's `phase-complete`: `state: "changes_requested"` sends the implementer back in with the review findings, then tester, then reviewer — never the reviewer again and never retro; that `spawn_worker` returns the issue to `in_progress` on its own (the daemon writes it for a corrective implementer whenever the PR's latest recorded review is changes requested, a human's after approval included), so you set nothing by hand; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. `state: "approved"` on a rebased head whose body names an unchanged fingerprint is that confirmation: proceed to retro if it has not run, otherwise to the merger — never to a second retro or test round. |
@@ -89,7 +89,7 @@ Concurrent issues have disjoint workspaces; only the currently active phase muta
89
89
  one. After you complete and go idle, treat `$LEGION_WORKSPACE` as read-only: you are kept
90
90
  alive to answer questions, not to keep editing. Do not create new commits, run
91
91
  `jj -R "$LEGION_WORKSPACE" new`, or touch tracked files once your own handoff is committed
92
- and pushed — a code change belongs to whichever phase is active now.
92
+ (and, for the implementer, pushed) — a code change belongs to whichever phase is active now.
93
93
 
94
94
  On every start, and especially after revival or re-creation, read the issue and then the
95
95
  committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legion/`:
@@ -149,9 +149,22 @@ Commit attribution is automatic: the extension exports a `JJ_CONFIG` overlay whe
149
149
  session starts, so every jj commit you make carries an `Omp-Session: <this-session-id>`
150
150
  trailer with no action from you. Do not add attribution trailers by hand.
151
151
 
152
- The jj configuration already supplies your phase's plus-addressed author and committer
153
- identity. Do not override Git identity configuration. Your session receives the credential
154
- capability it needs; invoke GitHub through the credential helper:
152
+ Your pane's environment already supplies your phase's author and committer identity
153
+ (`JJ_USER`/`JJ_EMAIL` and the Git author/committer variables, set by the daemon when it opened
154
+ the pane; the daemon also re-authors the workspace's working copy for your role at each
155
+ assignment, since `jj split`/`jj describe` keep its author). Never set or override
156
+ `user.name`/`user.email` in any jj or Git scope — not `jj config set`, not `--config`, not
157
+ `git config`: `--config` outranks the pane environment and would put the wrong App back on your
158
+ commits, and the repository-scoped jj config is one file shared by every issue workspace of the
159
+ clone. Before a push, check
160
+ `jj -R "$LEGION_WORKSPACE" log -r 'main@origin..@' -T 'author.email() ++ " | " ++ committer.email() ++ " " ++ description.first_line() ++ "\n"'`
161
+ shows your role's App in both columns **on every commit you made** — not on the whole list:
162
+ earlier phases' commits are legitimately authored by their own role's App, and a conflict-forced
163
+ rebase legitimately sets the committer of every rebased commit, other roles' included, to the
164
+ rebaser. A wrong identity on your own commit is a pane-environment problem to report to the
165
+ architect, not something to pin (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`,
166
+ Hazard 1). Your session receives the credential capability it needs; invoke GitHub through the
167
+ credential helper:
155
168
 
156
169
  ```bash
157
170
  legion gh -- <gh args…>
@@ -440,9 +453,12 @@ cd -- "$LEGION_WORKSPACE" && \
440
453
  jj -R "$LEGION_WORKSPACE" split -m "<phase>: record handoff" .legion/<phase>.json
441
454
  ```
442
455
 
443
- If the issue bookmark exists locally, advance it and push it with the provisioned
444
- credential helper. `--bookmark` also publishes the locally provisioned bookmark on its
445
- first push — a bookmark not yet tracking a remote one is tracked automatically:
456
+ **Only the implementer pushes the issue branch.** It acts as the code-writing App
457
+ (`legion-implementer[bot]`, `appRoleForLegionRole` in `packages/daemon/src/daemon/github-apps.ts`),
458
+ the one App with `contents` permission (the merger acts as the same App but pushes nothing: it
459
+ verifies and publishes READY). If you are the implementer, advance the issue bookmark and push it
460
+ with the provisioned credential helper. `--bookmark` also publishes the locally provisioned
461
+ bookmark on its first push — a bookmark not yet tracking a remote one is tracked automatically:
446
462
 
447
463
  ```bash
448
464
  cd -- "$LEGION_WORKSPACE" && \
@@ -450,16 +466,23 @@ cd -- "$LEGION_WORKSPACE" && \
450
466
  jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
451
467
  ```
452
468
 
453
- Do not report phase completion until the write, existence check, and handoff commit
454
- succeed; when an issue branch exists, its push is also required. This is the committed
455
- copy the next phase reads after revival. It is removed once, at the end of a clean review: the
456
- implementer pushes that deletion at the reviewer's direction. No other phase removes it — and
457
- once it is gone (`jj -R "$LEGION_WORKSPACE" file list -r @- .legion` prints nothing on stdout;
458
- jj warns on stderr), this
459
- gate no longer applies: a later rebase, bare-gate re-check, confirmation, or retro writes no
460
- `.legion/<phase>.json`, commits no handoff, and reports with `legion handoff complete` alone
461
- (below). Recreating `.legion/` after its deletion changes the approved head and restarts the
462
- review loop this rule exists to end.
469
+ Every other role — planner, tester, reviewer, architects — acts as the review App
470
+ (`legion-reviewer[bot]`), which cannot push: the `split` above is your last step, and the commit
471
+ rides the implementer's next push (the corrective push after a review, or the final `.legion/`
472
+ deletion). A push from one of those roles is refused — over git it reads
473
+ `remote: Repository not found.`; the REST API's form of the same refusal is
474
+ `Resource not accessible by integration` — and that refusal is expected, not a failure to report
475
+ or retry.
476
+
477
+ Do not report phase completion until the write, existence check, and handoff commit succeed —
478
+ and, for the implementer, until the push has too. This is the committed copy the next phase
479
+ reads after revival. It is removed once, at the end of a clean review: the implementer pushes
480
+ that deletion at the reviewer's direction. No other phase removes it — and once it is gone
481
+ (`jj -R "$LEGION_WORKSPACE" file list -r @- .legion` prints nothing on stdout; jj warns on
482
+ stderr), this gate no longer applies: a later rebase, bare-gate re-check, confirmation, or retro
483
+ writes no `.legion/<phase>.json`, commits no handoff, and reports with `legion handoff complete`
484
+ alone (below). Recreating `.legion/` after its deletion changes the approved head and restarts
485
+ the review loop this rule exists to end.
463
486
 
464
487
  ## Completion: report to the architect, then stay
465
488
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.24.2",
3
+ "version": "1.24.4",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [