@sjawhar/pi-legion-envoy 5.0.1 → 5.1.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 +101 -24
- package/dist/legion.js +3 -1
- package/dist/skills/legion-architect/SKILL.md +12 -7
- package/dist/skills/legion-worker/SKILL.md +65 -33
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -34851,6 +34851,7 @@ function legionRoleClaimBridge() {
|
|
|
34851
34851
|
return bridge;
|
|
34852
34852
|
const createdBridge = {
|
|
34853
34853
|
instances: [],
|
|
34854
|
+
managedSessions: new Set,
|
|
34854
34855
|
regained: undefined
|
|
34855
34856
|
};
|
|
34856
34857
|
store[LEGION_ROLE_CLAIM_BRIDGE] = createdBridge;
|
|
@@ -34858,6 +34859,7 @@ function legionRoleClaimBridge() {
|
|
|
34858
34859
|
}
|
|
34859
34860
|
async function claimEnvoyRole(sessionID, role, context) {
|
|
34860
34861
|
const bridge = legionRoleClaimBridge();
|
|
34862
|
+
bridge.managedSessions.add(sessionID);
|
|
34861
34863
|
const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
|
|
34862
34864
|
if (instance === undefined)
|
|
34863
34865
|
throw new Error("Envoy has no bound instance for a role claim");
|
|
@@ -34975,6 +34977,21 @@ var NATS_RETRY_INTERVAL_MS = 15000;
|
|
|
34975
34977
|
var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
|
|
34976
34978
|
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
34977
34979
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
34980
|
+
var LEGION_MANAGED_ENTRY = "legion-managed-session";
|
|
34981
|
+
var ASK_REMINDER_MESSAGE = "dispatch-ask-reminder";
|
|
34982
|
+
var OPEN_ASKS_REMINDER = "You have no unanswered asks in Dispatch. If you are waiting for human input, open an ask. Otherwise ignore this reminder and continue with any remaining work. Do not reply just to acknowledge this reminder.";
|
|
34983
|
+
var ASK_OPENING_TOOLS = ["dispatch_ask", "dispatch_request_approval"];
|
|
34984
|
+
function isLegionManagedEntry(entry) {
|
|
34985
|
+
if (typeof entry !== "object" || entry === null)
|
|
34986
|
+
return false;
|
|
34987
|
+
if (!("type" in entry) || entry.type !== "custom")
|
|
34988
|
+
return false;
|
|
34989
|
+
if (!("customType" in entry) || entry.customType !== LEGION_MANAGED_ENTRY)
|
|
34990
|
+
return false;
|
|
34991
|
+
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
34992
|
+
return false;
|
|
34993
|
+
return "session_id" in entry.data && typeof entry.data.session_id === "string";
|
|
34994
|
+
}
|
|
34978
34995
|
function isRoleClaimEntry(entry) {
|
|
34979
34996
|
if (typeof entry !== "object" || entry === null)
|
|
34980
34997
|
return false;
|
|
@@ -35030,26 +35047,54 @@ function envoyExtension(pi) {
|
|
|
35030
35047
|
let claimedRoleTopic;
|
|
35031
35048
|
let activeSessionContext;
|
|
35032
35049
|
const inbox = [];
|
|
35050
|
+
let askAwareness = {
|
|
35051
|
+
session_id: "",
|
|
35052
|
+
period: 0,
|
|
35053
|
+
baseline_as_of: null,
|
|
35054
|
+
fired: false,
|
|
35055
|
+
saw_ask: false
|
|
35056
|
+
};
|
|
35057
|
+
let awarenessGeneration = 0;
|
|
35058
|
+
let askCheckInFlight = false;
|
|
35059
|
+
let legionManagedTranscript = false;
|
|
35060
|
+
const legionManaged = (id) => legionManagedTranscript || legionRoleClaimBridge().managedSessions.has(id);
|
|
35033
35061
|
const availabilityWarningSessionIDs = new Set;
|
|
35034
35062
|
const warnAskAvailability = (context, error48) => {
|
|
35035
35063
|
const sessionID2 = context.sessionManager.getSessionId();
|
|
35036
35064
|
if (availabilityWarningSessionIDs.has(sessionID2))
|
|
35037
35065
|
return;
|
|
35038
35066
|
availabilityWarningSessionIDs.add(sessionID2);
|
|
35039
|
-
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error48)});
|
|
35067
|
+
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error48)}); the stop-time ask reminder is off until it recovers`, "warning");
|
|
35040
35068
|
};
|
|
35041
|
-
const queryOpenAsks = async (requestedSessionID) => {
|
|
35069
|
+
const queryOpenAsks = async (requestedSessionID, since) => {
|
|
35042
35070
|
const config2 = activeDispatchConfig();
|
|
35043
35071
|
if (config2 === null)
|
|
35044
35072
|
return null;
|
|
35045
|
-
const snapshot = await new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
|
|
35073
|
+
const snapshot = await new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID, since);
|
|
35046
35074
|
availabilityWarningSessionIDs.delete(requestedSessionID);
|
|
35047
35075
|
return { snapshot, url: config2.url };
|
|
35048
35076
|
};
|
|
35077
|
+
const markLegionManagedSession = (targetSessionID) => {
|
|
35078
|
+
legionRoleClaimBridge().managedSessions.add(targetSessionID);
|
|
35079
|
+
if (legionManagedTranscript)
|
|
35080
|
+
return;
|
|
35081
|
+
legionManagedTranscript = true;
|
|
35082
|
+
pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
|
|
35083
|
+
};
|
|
35049
35084
|
const restoreLocalSessionState = (context) => {
|
|
35050
35085
|
sessionDirectory = context.cwd;
|
|
35051
35086
|
sessionID = context.sessionManager.getSessionId();
|
|
35052
35087
|
activeSessionContext = context;
|
|
35088
|
+
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
35089
|
+
askAwareness = {
|
|
35090
|
+
session_id: sessionID,
|
|
35091
|
+
period: 0,
|
|
35092
|
+
baseline_as_of: null,
|
|
35093
|
+
fired: false,
|
|
35094
|
+
saw_ask: false
|
|
35095
|
+
};
|
|
35096
|
+
legionManagedTranscript = branch.some(isLegionManagedEntry);
|
|
35097
|
+
awarenessGeneration++;
|
|
35053
35098
|
};
|
|
35054
35099
|
pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
35055
35100
|
registerEnvoyMessageRenderer(pi);
|
|
@@ -35399,6 +35444,7 @@ function envoyExtension(pi) {
|
|
|
35399
35444
|
if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
|
|
35400
35445
|
throw new Error(`Envoy has no active session for Legion role claim: ${targetSessionID}`);
|
|
35401
35446
|
}
|
|
35447
|
+
markLegionManagedSession(targetSessionID);
|
|
35402
35448
|
if (sessionID !== targetSessionID)
|
|
35403
35449
|
await establishSession(context);
|
|
35404
35450
|
await registerSession();
|
|
@@ -35562,33 +35608,61 @@ function envoyExtension(pi) {
|
|
|
35562
35608
|
}
|
|
35563
35609
|
}
|
|
35564
35610
|
registerEnvoyWhoamiCommand(pi, () => sessionID);
|
|
35565
|
-
pi.on("before_agent_start", async (
|
|
35611
|
+
pi.on("before_agent_start", async (event, context) => {
|
|
35566
35612
|
const id = context.sessionManager.getSessionId();
|
|
35567
|
-
if (id === "")
|
|
35613
|
+
if (id === "" || event.prompt.trim() === "" || !context.hasUI || legionManaged(id)) {
|
|
35614
|
+
return;
|
|
35615
|
+
}
|
|
35616
|
+
if (await isSubagent(context))
|
|
35568
35617
|
return;
|
|
35569
35618
|
try {
|
|
35570
35619
|
const open = await queryOpenAsks(id);
|
|
35571
|
-
if (open
|
|
35572
|
-
|
|
35573
|
-
return {
|
|
35574
|
-
message: {
|
|
35575
|
-
customType: "dispatch-open-asks",
|
|
35576
|
-
content: `Dispatch authored-ask summary:
|
|
35577
|
-
${formatOpenAsksSummary(open.snapshot, open.url)}`,
|
|
35578
|
-
display: false,
|
|
35579
|
-
attribution: "agent"
|
|
35580
|
-
}
|
|
35581
|
-
};
|
|
35620
|
+
if (open !== null)
|
|
35621
|
+
armAskAwareness(id, event.prompt, open.snapshot.as_of);
|
|
35582
35622
|
} catch (error48) {
|
|
35583
35623
|
warnAskAvailability(context, error48);
|
|
35584
|
-
|
|
35585
|
-
|
|
35586
|
-
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
35590
|
-
|
|
35591
|
-
|
|
35624
|
+
}
|
|
35625
|
+
return;
|
|
35626
|
+
});
|
|
35627
|
+
function armAskAwareness(id, prompt, asOf) {
|
|
35628
|
+
if (prompt.trim() === "")
|
|
35629
|
+
return;
|
|
35630
|
+
awarenessGeneration++;
|
|
35631
|
+
askAwareness = {
|
|
35632
|
+
session_id: id,
|
|
35633
|
+
period: (askAwareness.session_id === id ? askAwareness.period : 0) + 1,
|
|
35634
|
+
baseline_as_of: asOf,
|
|
35635
|
+
fired: false,
|
|
35636
|
+
saw_ask: false
|
|
35637
|
+
};
|
|
35638
|
+
}
|
|
35639
|
+
pi.on("agent_end", async (event, context) => {
|
|
35640
|
+
const id = context.sessionManager.getSessionId();
|
|
35641
|
+
const lastReply = event.messages?.findLast((message) => message.role === "assistant");
|
|
35642
|
+
if (event.willContinue === true || lastReply?.stopReason !== "stop" || shuttingDown || !context.hasUI || id === "" || id !== sessionID || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period === 0 || askAwareness.fired || askAwareness.saw_ask || askAwareness.baseline_as_of === null || askCheckInFlight) {
|
|
35643
|
+
return;
|
|
35644
|
+
}
|
|
35645
|
+
const period = askAwareness.period;
|
|
35646
|
+
const generation = awarenessGeneration;
|
|
35647
|
+
askCheckInFlight = true;
|
|
35648
|
+
try {
|
|
35649
|
+
let open;
|
|
35650
|
+
try {
|
|
35651
|
+
open = await queryOpenAsks(id, askAwareness.baseline_as_of);
|
|
35652
|
+
} catch (error48) {
|
|
35653
|
+
if (generation === awarenessGeneration)
|
|
35654
|
+
warnAskAvailability(context, error48);
|
|
35655
|
+
return;
|
|
35656
|
+
}
|
|
35657
|
+
if (open === null || shuttingDown || generation !== awarenessGeneration || sessionID !== id || context.sessionManager.getSessionId() !== id || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period !== period || askAwareness.fired || askAwareness.saw_ask) {
|
|
35658
|
+
return;
|
|
35659
|
+
}
|
|
35660
|
+
if (open.snapshot.count > 0 || open.snapshot.opened_since)
|
|
35661
|
+
return;
|
|
35662
|
+
askAwareness = { ...askAwareness, fired: true };
|
|
35663
|
+
pi.sendMessage({ customType: ASK_REMINDER_MESSAGE, content: OPEN_ASKS_REMINDER, display: false }, { deliverAs: "steer", triggerTurn: true });
|
|
35664
|
+
} finally {
|
|
35665
|
+
askCheckInFlight = false;
|
|
35592
35666
|
}
|
|
35593
35667
|
});
|
|
35594
35668
|
const announceFollow = createFollowAnnouncer((text) => {
|
|
@@ -35597,6 +35671,9 @@ ${formatOpenAsksSummary(open.snapshot, open.url)}`,
|
|
|
35597
35671
|
pi.on("tool_result", async (event) => {
|
|
35598
35672
|
if (event.isError)
|
|
35599
35673
|
return;
|
|
35674
|
+
if (ASK_OPENING_TOOLS.includes(event.toolName) && askAwareness.session_id === sessionID && askAwareness.period > 0 && !askAwareness.saw_ask) {
|
|
35675
|
+
askAwareness = { ...askAwareness, saw_ask: true };
|
|
35676
|
+
}
|
|
35600
35677
|
announceFollow(event.details);
|
|
35601
35678
|
});
|
|
35602
35679
|
async function execute(operation, rawParameters) {
|
package/dist/legion.js
CHANGED
|
@@ -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: "5.
|
|
16145
|
+
version: "5.1.1",
|
|
16146
16146
|
type: "module",
|
|
16147
16147
|
omp: {
|
|
16148
16148
|
extensions: [
|
|
@@ -31220,6 +31220,7 @@ function legionRoleClaimBridge() {
|
|
|
31220
31220
|
return bridge;
|
|
31221
31221
|
const createdBridge = {
|
|
31222
31222
|
instances: [],
|
|
31223
|
+
managedSessions: new Set,
|
|
31223
31224
|
regained: undefined
|
|
31224
31225
|
};
|
|
31225
31226
|
store[LEGION_ROLE_CLAIM_BRIDGE] = createdBridge;
|
|
@@ -31227,6 +31228,7 @@ function legionRoleClaimBridge() {
|
|
|
31227
31228
|
}
|
|
31228
31229
|
async function claimEnvoyRole(sessionID, role, context) {
|
|
31229
31230
|
const bridge = legionRoleClaimBridge();
|
|
31231
|
+
bridge.managedSessions.add(sessionID);
|
|
31230
31232
|
const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
|
|
31231
31233
|
if (instance === undefined)
|
|
31232
31234
|
throw new Error("Envoy has no bound instance for a role claim");
|
|
@@ -233,8 +233,7 @@ Preserve this order exactly:
|
|
|
233
233
|
|
|
234
234
|
1. tester green and review cycles complete;
|
|
235
235
|
2. on a clean review, `spawn_worker` the implementer once more to push only the `.legion/`
|
|
236
|
-
deletion
|
|
237
|
-
head. The deletion must land before that approval, which is head-pinned. An implementer
|
|
236
|
+
deletion, then the reviewer approves that head. The deletion must land before that approval, which is head-pinned. An implementer
|
|
238
237
|
completion advances the status only from `in_progress` to `testing`; this push, like retro
|
|
239
238
|
later, leaves the status where it is, so you set nothing by hand — on its `phase-complete`
|
|
240
239
|
wake, `spawn_worker` the reviewer to approve that head (a finished reviewer may already be
|
|
@@ -259,8 +258,8 @@ What returns the tree to review: a changed diff — a commit above the approved
|
|
|
259
258
|
touches anything outside `docs/solutions/`, or a rebase whose fingerprint
|
|
260
259
|
(`skill://legion-worker`'s unchanged-diff check) differs from the approved head's. What does not: retro's
|
|
261
260
|
`docs/solutions/` commit, and a rebase forced by a GitHub-reported conflict whose fingerprint
|
|
262
|
-
is unchanged. For that rebase the order is: the implementer rebases
|
|
263
|
-
fingerprints; the tester re-runs the bare gates only; the reviewer confirms and approves the new
|
|
261
|
+
is unchanged. For that rebase the order is: the implementer rebases, pushes the rebased chain with
|
|
262
|
+
`legion-worker`'s procedure for rewritten commits, and posts the before/after fingerprints; the tester re-runs the bare gates only; the reviewer confirms and approves the new
|
|
264
263
|
head by SHA (or continues its round if it had not approved); the merger republishes READY.
|
|
265
264
|
Retro does not re-run. A rebase happens only when GitHub reports `CONFLICTING`
|
|
266
265
|
(`legion gh -- pr view <n> --json mergeable,mergeStateStatus`); read that on every end-game
|
|
@@ -271,8 +270,9 @@ it. Do not let the merger publish `READY` for an obsolete approval.
|
|
|
271
270
|
If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
|
|
272
271
|
to resolve, open a `dispatch_ask` that names the thread's URL and GitHub's message for a human to
|
|
273
272
|
resolve it by hand, with options for resolved / could not; the merger does not publish while it
|
|
274
|
-
is open. That is the one review-thread step a human takes: the review App cannot resolve
|
|
275
|
-
and the implementer's and merger's runs of the command
|
|
273
|
+
is open. That is the one review-thread step a human takes: the review App cannot resolve a thread
|
|
274
|
+
on a pull request the implementer opened, and the implementer's and merger's runs of the command
|
|
275
|
+
close every accepted one.
|
|
276
276
|
|
|
277
277
|
## 7. Close
|
|
278
278
|
|
|
@@ -311,7 +311,7 @@ active phase worker.
|
|
|
311
311
|
| `worker-recovered` | Payload `{type:"worker-recovered", issue, role, fromRef, delivery?}`. The worker's tree volume was lost and the daemon replaced it from the committed handoff on `fromRef`. `delivery: "spawned"` means the current assignment was preserved on the new worker; do not resend it. `delivery: "queued"` means that preserved assignment awaits capacity; wait for `worker-started`. Without `delivery`, inspect `.legion/` and the active phase before deciding whether work needs a new assignment. |
|
|
312
312
|
| `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
|
|
313
313
|
| `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. |
|
|
314
|
-
| `pr-blocked` | Payload `{type:"pr-blocked", pr, attempts}`. `attempts` counts heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count; a push the daemon cannot classify (a listener without `changed_paths`, a list capped at 100, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. Assign a focused implementer or corrective child, then return it through testing and review; do not treat the blocked PR as final. |
|
|
314
|
+
| `pr-blocked` | Payload `{type:"pr-blocked", pr, attempts}`. `attempts` counts heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list capped at 100, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. Assign a focused implementer or corrective child, then return it through testing and review; do not treat the blocked PR as final. |
|
|
315
315
|
| `pr-merged` | Payload `{type:"pr-merged", pr, mergeCommitSha}`. The PR merged because a human merged it under the repository's rules. `spawn_worker` the **implementer** with the production-check task naming that merge commit (it resumes the same agent; a retired role has no live holder, so never `envoy_publish` for this). Its `phase-complete` is what brings you to step 7: verify the record on the pull request and this issue first, then sign off naming it and set the issue `done`. A merge is not the close. |
|
|
316
316
|
| `pr-closed-unmerged` | Decide from current scope whether to reopen the work, send a fresh implementer, or cancel it with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
|
|
317
317
|
| `issue-comment` | Interpret the comment in the issue's design context. Answer it, adjust the plan, or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
|
|
@@ -327,6 +327,11 @@ everything else in the tree, or use `dispatch_ask` for a human question; workers
|
|
|
327
327
|
Sami directly with `dispatch_ask` the same way. Do not create a wait loop for any wake
|
|
328
328
|
source.
|
|
329
329
|
|
|
330
|
+
Never yield while waiting on a human. A human is waiting on you only where an open ask sits
|
|
331
|
+
in their inbox, so open it — `dispatch_ask`, or `dispatch_request_approval` for the spec
|
|
332
|
+
gate — before you stop. Otherwise proceed: proceeding is the default, and a stop that waits
|
|
333
|
+
on nobody stalls the tree until someone notices.
|
|
334
|
+
|
|
330
335
|
## Architecture components
|
|
331
336
|
|
|
332
337
|
Bootstrap on a root whose project has an architecture source: in the root's first PR (the implement worker pushes it), write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`, `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head, so the sync and the attach below work only once that PR has merged to the source branch: until then leave the root on `inherit` (the attach would answer `400 COMPONENTS_INPUT`, the component does not exist yet). After the merge, `dispatch_architecture_sync({ project })` and attach the root with `dispatch_issue_update({ issue, components: { mode: "explicit", ids: [...] } })`. Children inherit the root's attachment; give a child its own `components` only when it changes a narrower set, and `{ mode: "none", reason }` when it is not architectural work. Attach before decomposing, and require every implementer to change the component file beside the code it describes in the same review.
|
|
@@ -232,15 +232,9 @@ commit is `plan: record handoff`.
|
|
|
232
232
|
|
|
233
233
|
## Implementer push and pull request
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
```bash
|
|
240
|
-
cd -- "$LEGION_WORKSPACE" && \
|
|
241
|
-
jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> && \
|
|
242
|
-
jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
|
|
243
|
-
```
|
|
235
|
+
The implementer opens the pull request. After its implementation commit and verification, it
|
|
236
|
+
pushes the issue branch under this exact name with the one push procedure every role uses
|
|
237
|
+
(*Every role pushes its own commits*, below).
|
|
244
238
|
|
|
245
239
|
The provisioned issue workspace configures `credential.helper` with the daemon's absolute
|
|
246
240
|
credential command, so `jj -R "$LEGION_WORKSPACE" git push` authenticates transparently
|
|
@@ -286,7 +280,7 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
286
280
|
**Fast-follow:** <one named cleanup item and where it will land>, or "none".
|
|
287
281
|
|
|
288
282
|
**Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
|
|
289
|
-
**Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, rebase onto the new base and push — the new head runs Tests against the new merge result — and cite that run in the PR body.
|
|
283
|
+
**Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, record the pushed tip, rebase onto the new base, and push with `legion-worker`'s procedure for rewritten commits — the new head runs Tests against the new merge result — and cite that run in the PR body.
|
|
290
284
|
```
|
|
291
285
|
|
|
292
286
|
**A proof** is the changed behaviour exercised on the surface a user reaches it through, recorded
|
|
@@ -314,9 +308,9 @@ this proof.
|
|
|
314
308
|
`Accepted: not a defect — <reason>`, or `Still open: <what remains>`; nothing else is an
|
|
315
309
|
acceptance, and nobody replies after an `Accepted:` (any later reply that is not itself an
|
|
316
310
|
`Accepted:` — the opener's own follow-up included — leaves the thread open, since the command
|
|
317
|
-
reads only the newest comment). The review App can reply on a thread
|
|
318
|
-
|
|
319
|
-
|
|
311
|
+
reads only the newest comment). The review App can reply on a thread but cannot resolve it:
|
|
312
|
+
GitHub grants resolving a review thread to the pull request's author, and the implementer opens
|
|
313
|
+
every Legion pull request (`packages/daemon/src/daemon/AGENTS.md`, GitHub Apps). So the
|
|
320
314
|
**implementer** runs `legion threads resolve --pr <number> --repo <owner>/<repo>` before every
|
|
321
315
|
push that answers a review (the corrective push and the final `.legion/` deletion push) and
|
|
322
316
|
pastes its output into the `Threads` section. The command resolves each unresolved thread
|
|
@@ -341,7 +335,8 @@ this proof.
|
|
|
341
335
|
tip; post one PR comment (Legion footer):
|
|
342
336
|
`rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
|
|
343
337
|
Rebase the whole chain — `jj -R "$LEGION_WORKSPACE" rebase -s 'roots(main@origin..@)' -d main@origin` —
|
|
344
|
-
so the tester's and reviewer's commits move with yours.
|
|
338
|
+
so the tester's and reviewer's commits move with yours. Record the pushed tip before it and
|
|
339
|
+
push the rebased chain with the push procedure (*Rewriting pushed commits*, below).
|
|
345
340
|
- **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." The `Fast-follow:`
|
|
346
341
|
field names naming, duplication, or wording cleanup only; anything that changes behaviour,
|
|
347
342
|
hides an error, or breaks a gate lands in this PR.
|
|
@@ -399,8 +394,7 @@ this proof.
|
|
|
399
394
|
Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
|
|
400
395
|
finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
|
|
401
396
|
Then return the issue to the architect; when clean, have the architect send the implementer
|
|
402
|
-
back to push the `.legion/` deletion
|
|
403
|
-
and approve it by name. After a conflict-forced rebase, compute the fingerprint at the
|
|
397
|
+
back to push the `.legion/` deletion, then review **that** head and approve it by name. After a conflict-forced rebase, compute the fingerprint at the
|
|
404
398
|
`commit_id` of your last submitted review and at the new head. Equal and that review was
|
|
405
399
|
`APPROVE`: submit one more `APPROVE` naming the new head by SHA, its body naming both SHAs
|
|
406
400
|
and the fingerprint — a confirmation, not a round; no thermo pass, no thread pass. Equal and
|
|
@@ -535,29 +529,61 @@ cd -- "$LEGION_WORKSPACE" && \
|
|
|
535
529
|
jj -R "$LEGION_WORKSPACE" split -m "<phase>: record handoff" .legion/<phase>.json
|
|
536
530
|
```
|
|
537
531
|
|
|
538
|
-
**
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
532
|
+
**Every role pushes its own commits.** After the handoff commit — and, for the tester, the red
|
|
533
|
+
tests it wrote — advance the issue bookmark and push it with the provisioned credential helper,
|
|
534
|
+
which authenticates as your role's App (`appRoleForLegionRole` in
|
|
535
|
+
`packages/daemon/src/daemon/github-apps.ts`). `-r @-` puts the bookmark on the commit you just
|
|
536
|
+
split off: the working copy left above it has no description, and `jj git push` refuses a
|
|
537
|
+
commit without one. `--allow-backwards` is for that local step alone: after a split the bookmark
|
|
538
|
+
can sit on the undescribed working copy above `@-`. `--bookmark` also publishes the locally
|
|
539
|
+
provisioned bookmark on its first push — a bookmark not yet tracking a remote one is tracked
|
|
540
|
+
automatically. This is the one push procedure; every push of the issue branch uses it:
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
cd -- "$LEGION_WORKSPACE" && \
|
|
544
|
+
tip_file="${TMPDIR:-/tmp}/legion-<KEY>-$LEGION_ROLE-rewritten-tip" && \
|
|
545
|
+
old=$(cat -- "$tip_file" 2>/dev/null || true) && \
|
|
546
|
+
behind=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
|
|
547
|
+
-r "remote_bookmarks(exact:\"legion/<KEY>\", exact:\"origin\") ~ (::@-${old:+ | $old})") && \
|
|
548
|
+
{ [ -z "$behind" ] || { echo "legion/<KEY>@origin is at $behind, which @- does not descend from" >&2; false; }; } && \
|
|
549
|
+
jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> -r @- --allow-backwards && \
|
|
550
|
+
jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY> && \
|
|
551
|
+
rm -f -- "$tip_file"
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
The `behind` check refuses unless `@-` descends from `legion/<KEY>@origin` (or the branch is not
|
|
555
|
+
on GitHub yet). Every issue workspace shares one clone, so another role's push moves
|
|
556
|
+
`legion/<KEY>@origin` here at once. With the flag and no check, `jj git push` then moves the
|
|
557
|
+
remote branch sideways onto your commit and drops theirs (jj 0.45.1:
|
|
558
|
+
`bookmark: legion/K [move sideways from <theirs> to <yours>]`). A clone that has not seen the other
|
|
559
|
+
push is refused by jj itself (`unexpectedly moved on the remote`).
|
|
560
|
+
|
|
561
|
+
**Rewriting pushed commits** — the conflict-forced rebase, the rebase after a retarget, or a
|
|
562
|
+
`jj squash --into` a commit already on GitHub — leaves the pushed tip outside `::@-`, so record
|
|
563
|
+
that tip first, after a fetch and while your chain still descends from it:
|
|
545
564
|
|
|
546
565
|
```bash
|
|
547
566
|
cd -- "$LEGION_WORKSPACE" && \
|
|
548
|
-
jj -R "$LEGION_WORKSPACE"
|
|
549
|
-
jj -R "$LEGION_WORKSPACE"
|
|
567
|
+
jj -R "$LEGION_WORKSPACE" git fetch && \
|
|
568
|
+
behind=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
|
|
569
|
+
-r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin") ~ ::@-') && \
|
|
570
|
+
{ [ -z "$behind" ] || { echo "legion/<KEY>@origin is at $behind, which @- does not descend from" >&2; false; }; } && \
|
|
571
|
+
jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id' \
|
|
572
|
+
-r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin")' \
|
|
573
|
+
>"${TMPDIR:-/tmp}/legion-<KEY>-$LEGION_ROLE-rewritten-tip"
|
|
550
574
|
```
|
|
551
575
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
the
|
|
576
|
+
Then rewrite, resolve, and push with the procedure above. It lets the remote branch sit on the
|
|
577
|
+
tip you recorded, which the rewrite replaced, and on nothing else: when another role pushed after
|
|
578
|
+
you recorded it, the push is refused. The push deletes the file.
|
|
579
|
+
|
|
580
|
+
Before the push, check ancestry and identity as above: the chain carries every earlier phase's
|
|
581
|
+
commits, and pushing them with yours is expected. A refusal, and a push the remote rejects, is a
|
|
582
|
+
report to the architect with the output, never a force-push. The merger makes no commit and
|
|
583
|
+
pushes nothing.
|
|
558
584
|
|
|
559
|
-
Do not report phase completion until the write, existence check,
|
|
560
|
-
|
|
585
|
+
Do not report phase completion until the write, existence check, handoff commit, and push
|
|
586
|
+
succeed. This is the committed copy the next phase
|
|
561
587
|
reads after revival. It is removed once, at the end of a clean review: the implementer pushes
|
|
562
588
|
that deletion at the reviewer's direction. No other phase removes it — and once it is gone
|
|
563
589
|
(`jj -R "$LEGION_WORKSPACE" file list -r @- .legion` prints nothing on stdout; jj warns on
|
|
@@ -590,3 +616,9 @@ When blocked on lifecycle, scope, or cross-phase matters, `envoy_publish` the ow
|
|
|
590
616
|
architect a concise message: issue, phase, verified observation, what you tried, and the
|
|
591
617
|
decision required. Reach for `dispatch_ask` yourself only for a standalone human question
|
|
592
618
|
outside that coordination.
|
|
619
|
+
|
|
620
|
+
Never yield while blocked on a decision someone else owns. Before you stop, make the block
|
|
621
|
+
visible where its owner will see it: a lifecycle, scope, or cross-phase decision goes to the
|
|
622
|
+
owning architect as above, and a standalone human question goes in `dispatch_ask`. Otherwise
|
|
623
|
+
proceed: proceeding is the default, and a phase that stops silently holds its issue until
|
|
624
|
+
someone notices.
|