@sjawhar/opencode-legion-envoy 8.20.5 → 8.20.7
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/dispatch.js
CHANGED
|
@@ -13927,7 +13927,7 @@ function dispatchToolSchema(spec, z2, opts) {
|
|
|
13927
13927
|
const shape = spec.arguments(z2);
|
|
13928
13928
|
const strict = opts?.strict ?? spec.strict;
|
|
13929
13929
|
const schemaOptions = strict === undefined ? undefined : { strict };
|
|
13930
|
-
return spec.validation === undefined ? z2.object(shape, schemaOptions) : z2.refineObject(shape, spec.validation.check, spec.validation.message, schemaOptions);
|
|
13930
|
+
return spec.validation === undefined ? z2.object(shape, schemaOptions) : z2.refineObject(shape, spec.validation.check, spec.validation.refusal ?? spec.validation.message, schemaOptions);
|
|
13931
13931
|
}
|
|
13932
13932
|
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only dispatch issue with --external creates a native issue.";
|
|
13933
13933
|
var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
|
|
@@ -13969,6 +13969,23 @@ var commentValidation = {
|
|
|
13969
13969
|
},
|
|
13970
13970
|
message: `${commentOwner.message} turn requires reply_to_ask.`
|
|
13971
13971
|
};
|
|
13972
|
+
var ISSUE_UPDATE_FIELDS_RULE = "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, or repo.";
|
|
13973
|
+
var ISSUE_UPDATE_DONE_RULE = "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts";
|
|
13974
|
+
var ISSUE_UPDATE_REASON_RULE = "reason goes only with status done";
|
|
13975
|
+
var ISSUE_UPDATE_RULE = `${ISSUE_UPDATE_FIELDS_RULE} ${ISSUE_UPDATE_DONE_RULE}; ${ISSUE_UPDATE_REASON_RULE}.`;
|
|
13976
|
+
function issueUpdateProblem(value) {
|
|
13977
|
+
const input = value;
|
|
13978
|
+
if (input.status === "done") {
|
|
13979
|
+
if (typeof input.reason !== "string" || input.reason.trim() === "") {
|
|
13980
|
+
return `${ISSUE_UPDATE_DONE_RULE}.`;
|
|
13981
|
+
}
|
|
13982
|
+
} else if (input.reason !== undefined) {
|
|
13983
|
+
const status = typeof input.status === "string" ? `status ${input.status}` : "an update without status done";
|
|
13984
|
+
return `${ISSUE_UPDATE_REASON_RULE}: it is the note posted on the issue as it closes, and ${status} closes nothing. Drop reason, or set status done to close the issue.`;
|
|
13985
|
+
}
|
|
13986
|
+
const changesAField = typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || Array.isArray(input.blocked_by) || typeof input.components === "object" && input.components !== null || typeof input.repo === "string";
|
|
13987
|
+
return changesAField ? undefined : ISSUE_UPDATE_FIELDS_RULE;
|
|
13988
|
+
}
|
|
13972
13989
|
var messageValidation = {
|
|
13973
13990
|
check: (value) => {
|
|
13974
13991
|
const input = value;
|
|
@@ -14070,12 +14087,9 @@ var dispatchToolSpecs = [
|
|
|
14070
14087
|
repo: z2.string().describe("The repository the issue targets, owner/name, mapped to the issue's project in Settings; an empty string clears it, so the issue inherits its parent's or the project's default again.").optional()
|
|
14071
14088
|
}),
|
|
14072
14089
|
validation: {
|
|
14073
|
-
check: (value) =>
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
return reasonFits && (typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || Array.isArray(input.blocked_by) || typeof input.components === "object" && input.components !== null || typeof input.repo === "string");
|
|
14077
|
-
},
|
|
14078
|
-
message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, or repo. " + "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts; reason goes only with status done."
|
|
14090
|
+
check: (value) => issueUpdateProblem(value) === undefined,
|
|
14091
|
+
message: ISSUE_UPDATE_RULE,
|
|
14092
|
+
refusal: (value) => issueUpdateProblem(value) ?? ISSUE_UPDATE_RULE
|
|
14079
14093
|
},
|
|
14080
14094
|
strict: true
|
|
14081
14095
|
},
|
|
@@ -14529,7 +14543,7 @@ function zodSchemaApi(zod) {
|
|
|
14529
14543
|
let schema = api2.object(shape);
|
|
14530
14544
|
if (opts.strict)
|
|
14531
14545
|
schema = schema.strict();
|
|
14532
|
-
return schema.refine(check2, message);
|
|
14546
|
+
return schema.refine(check2, typeof message === "string" ? message : { error: (issue2) => message(issue2.input) });
|
|
14533
14547
|
}
|
|
14534
14548
|
};
|
|
14535
14549
|
}
|
|
@@ -14715,7 +14729,10 @@ function flagTable(tool, fields) {
|
|
|
14715
14729
|
}
|
|
14716
14730
|
}
|
|
14717
14731
|
add("--help", { action: "help", text: "Prints this help." });
|
|
14718
|
-
add("--dry-run", {
|
|
14732
|
+
add("--dry-run", {
|
|
14733
|
+
action: "dry-run",
|
|
14734
|
+
text: "Checks the arguments as the call would, then prints them as JSON and sends nothing."
|
|
14735
|
+
});
|
|
14719
14736
|
return flags;
|
|
14720
14737
|
}
|
|
14721
14738
|
function parseCommand(argv, io) {
|
|
@@ -14971,7 +14988,7 @@ function commandsHelp() {
|
|
|
14971
14988
|
return ` ${commandName(spec.name).padEnd(width)} ${sentence}`;
|
|
14972
14989
|
}),
|
|
14973
14990
|
"",
|
|
14974
|
-
"Run dispatch <command> --help for its flags and an example; --dry-run
|
|
14991
|
+
"Run dispatch <command> --help for its flags and an example; --dry-run checks a command's arguments as the call would and prints them as given, sending nothing.",
|
|
14975
14992
|
""
|
|
14976
14993
|
].join(`
|
|
14977
14994
|
`);
|
|
@@ -17568,6 +17585,34 @@ function withAccount(error48, account) {
|
|
|
17568
17585
|
const told = /[.!?]$/.test(message) ? `${message} ${account.charAt(0).toUpperCase()}${account.slice(1)}` : `${message}; ${account}`;
|
|
17569
17586
|
return new Error(told, { cause: error48 });
|
|
17570
17587
|
}
|
|
17588
|
+
function restatesMissingOwner(issue2) {
|
|
17589
|
+
return issue2.code === "invalid_type" && issue2.path.length === 1 && issue2.path[0] === "issue" && issue2.input === undefined || issue2.code === "custom" && issue2.path.length === 0 && (issue2.message.startsWith("Exactly one of issue and project is required") || issue2.message.startsWith("issue is required unless in_reply_to"));
|
|
17590
|
+
}
|
|
17591
|
+
function dispatchServerUrls(config2) {
|
|
17592
|
+
return config2.publicUrl === undefined ? [config2.url] : [config2.url, config2.publicUrl];
|
|
17593
|
+
}
|
|
17594
|
+
async function checkDispatchCall(input) {
|
|
17595
|
+
const { tool, args, cwd } = input;
|
|
17596
|
+
const problems = [];
|
|
17597
|
+
const owner = await resolveOwnerArguments(tool, args, cwd, input.env ?? process.env, input.exec ?? defaultExec, input.serverUrls, problems);
|
|
17598
|
+
const conflicts = issueFreeTools[tool] === true ? [] : ownerConflictProblems(tool, args, owner.ref, optionalString(owner.args, "ref"));
|
|
17599
|
+
problems.push(...conflicts);
|
|
17600
|
+
const ownerMissing = issueFreeTools[tool] !== true && (owner.owner === null || conflicts.length > 0);
|
|
17601
|
+
const schema = toolSchema(tool);
|
|
17602
|
+
const parsed = schema.safeParse(owner.args, { reportInput: true });
|
|
17603
|
+
if (!parsed.success) {
|
|
17604
|
+
const issues = ownerMissing ? parsed.error.issues.filter((issue2) => !restatesMissingOwner(issue2)) : parsed.error.issues;
|
|
17605
|
+
problems.push(...formatZodIssues(issues, schema, tool));
|
|
17606
|
+
}
|
|
17607
|
+
problems.push(...argumentProblems(tool, owner.args));
|
|
17608
|
+
const pictures = pictureSendingTools[tool] === true ? await localPictures(owner.args.images, cwd, problems) : [];
|
|
17609
|
+
return {
|
|
17610
|
+
problems,
|
|
17611
|
+
owner,
|
|
17612
|
+
args: parsed.success ? parsed.data : owner.args,
|
|
17613
|
+
pictures
|
|
17614
|
+
};
|
|
17615
|
+
}
|
|
17571
17616
|
async function executeDispatchTool(input) {
|
|
17572
17617
|
const { url: configUrl, token: configToken } = input.config;
|
|
17573
17618
|
const linkBase = input.config.publicUrl ?? configUrl;
|
|
@@ -17584,21 +17629,14 @@ async function executeDispatchTool(input) {
|
|
|
17584
17629
|
}, { preconnect: baseFetch.preconnect });
|
|
17585
17630
|
const env = input.env ?? process.env;
|
|
17586
17631
|
const exec = input.exec ?? defaultExec;
|
|
17587
|
-
const
|
|
17588
|
-
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
const
|
|
17594
|
-
|
|
17595
|
-
const issues = parsed.error.issues.filter((issue3) => !ownerMissing || !(issue3.code === "invalid_type" && issue3.path.length === 1 && issue3.path[0] === "issue" && issue3.input === undefined || issue3.code === "custom" && issue3.path.length === 0 && (issue3.message.startsWith("Exactly one of issue and project is required") || issue3.message.startsWith("issue is required unless in_reply_to"))));
|
|
17596
|
-
problems.push(...formatZodIssues(issues, schema, input.tool));
|
|
17597
|
-
}
|
|
17598
|
-
problems.push(...argumentProblems(input.tool, ownerArguments.args));
|
|
17599
|
-
const pictures = pictureSendingTools[input.tool] === true ? await localPictures(ownerArguments.args.images, input.cwd, problems) : [];
|
|
17600
|
-
if (problems.length > 0)
|
|
17601
|
-
throw new ToolInputError(input.tool, problems);
|
|
17632
|
+
const checked = await checkDispatchCall({
|
|
17633
|
+
...input,
|
|
17634
|
+
serverUrls: dispatchServerUrls(input.config)
|
|
17635
|
+
});
|
|
17636
|
+
if (checked.problems.length > 0)
|
|
17637
|
+
throw new ToolInputError(input.tool, checked.problems);
|
|
17638
|
+
const ownerArguments = checked.owner;
|
|
17639
|
+
const pictures = checked.pictures;
|
|
17602
17640
|
const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
17603
17641
|
if (input.tool === "dispatch_open_asks") {
|
|
17604
17642
|
const client2 = dispatchClient();
|
|
@@ -17627,7 +17665,7 @@ async function executeDispatchTool(input) {
|
|
|
17627
17665
|
details: { session: sessionId, owner: owner2, service }
|
|
17628
17666
|
};
|
|
17629
17667
|
}
|
|
17630
|
-
const args =
|
|
17668
|
+
const args = checked.args;
|
|
17631
17669
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
17632
17670
|
const client = dispatchClient();
|
|
17633
17671
|
const refDocument = ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : ownerArguments.ref?.artifact;
|
|
@@ -19717,11 +19755,23 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
19717
19755
|
print(messageFor(error48));
|
|
19718
19756
|
return USAGE;
|
|
19719
19757
|
}
|
|
19758
|
+
const file2 = (env.DISPATCH_HOST === undefined ? null : handedConfigFile(env)) ?? sessionConfigFile(host, env);
|
|
19720
19759
|
if (parsed.dryRun) {
|
|
19760
|
+
const settings = file2.path === null ? null : sessionDispatchSettings(file2, env, { cwd: io.cwd }).dispatch;
|
|
19761
|
+
const { problems: problems2 } = await checkDispatchCall({
|
|
19762
|
+
tool: parsed.tool,
|
|
19763
|
+
args: parsed.args,
|
|
19764
|
+
cwd: io.cwd,
|
|
19765
|
+
env,
|
|
19766
|
+
serverUrls: settings?.enabled === true ? dispatchServerUrls(settings) : []
|
|
19767
|
+
});
|
|
19768
|
+
if (problems2.length > 0) {
|
|
19769
|
+
print(new ToolInputError(parsed.tool, problems2).message);
|
|
19770
|
+
return REFUSED;
|
|
19771
|
+
}
|
|
19721
19772
|
print(JSON.stringify(parsed.args, null, 2));
|
|
19722
19773
|
return OK;
|
|
19723
19774
|
}
|
|
19724
|
-
const file2 = (env.DISPATCH_HOST === undefined ? null : handedConfigFile(env)) ?? sessionConfigFile(host, env);
|
|
19725
19775
|
if (file2.path === null) {
|
|
19726
19776
|
print(`dispatch: ${file2.error}`);
|
|
19727
19777
|
return USAGE;
|
package/dist/src/server.js
CHANGED
|
@@ -13826,6 +13826,23 @@ var commentValidation = {
|
|
|
13826
13826
|
},
|
|
13827
13827
|
message: `${commentOwner.message} turn requires reply_to_ask.`
|
|
13828
13828
|
};
|
|
13829
|
+
var ISSUE_UPDATE_FIELDS_RULE = "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, or repo.";
|
|
13830
|
+
var ISSUE_UPDATE_DONE_RULE = "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts";
|
|
13831
|
+
var ISSUE_UPDATE_REASON_RULE = "reason goes only with status done";
|
|
13832
|
+
var ISSUE_UPDATE_RULE = `${ISSUE_UPDATE_FIELDS_RULE} ${ISSUE_UPDATE_DONE_RULE}; ${ISSUE_UPDATE_REASON_RULE}.`;
|
|
13833
|
+
function issueUpdateProblem(value) {
|
|
13834
|
+
const input = value;
|
|
13835
|
+
if (input.status === "done") {
|
|
13836
|
+
if (typeof input.reason !== "string" || input.reason.trim() === "") {
|
|
13837
|
+
return `${ISSUE_UPDATE_DONE_RULE}.`;
|
|
13838
|
+
}
|
|
13839
|
+
} else if (input.reason !== undefined) {
|
|
13840
|
+
const status = typeof input.status === "string" ? `status ${input.status}` : "an update without status done";
|
|
13841
|
+
return `${ISSUE_UPDATE_REASON_RULE}: it is the note posted on the issue as it closes, and ${status} closes nothing. Drop reason, or set status done to close the issue.`;
|
|
13842
|
+
}
|
|
13843
|
+
const changesAField = typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || Array.isArray(input.blocked_by) || typeof input.components === "object" && input.components !== null || typeof input.repo === "string";
|
|
13844
|
+
return changesAField ? undefined : ISSUE_UPDATE_FIELDS_RULE;
|
|
13845
|
+
}
|
|
13829
13846
|
var messageValidation = {
|
|
13830
13847
|
check: (value) => {
|
|
13831
13848
|
const input = value;
|
|
@@ -13927,12 +13944,9 @@ var dispatchToolSpecs = [
|
|
|
13927
13944
|
repo: z2.string().describe("The repository the issue targets, owner/name, mapped to the issue's project in Settings; an empty string clears it, so the issue inherits its parent's or the project's default again.").optional()
|
|
13928
13945
|
}),
|
|
13929
13946
|
validation: {
|
|
13930
|
-
check: (value) =>
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
return reasonFits && (typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || Array.isArray(input.blocked_by) || typeof input.components === "object" && input.components !== null || typeof input.repo === "string");
|
|
13934
|
-
},
|
|
13935
|
-
message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, or repo. " + "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts; reason goes only with status done."
|
|
13947
|
+
check: (value) => issueUpdateProblem(value) === undefined,
|
|
13948
|
+
message: ISSUE_UPDATE_RULE,
|
|
13949
|
+
refusal: (value) => issueUpdateProblem(value) ?? ISSUE_UPDATE_RULE
|
|
13936
13950
|
},
|
|
13937
13951
|
strict: true
|
|
13938
13952
|
},
|
|
@@ -14382,7 +14396,7 @@ function zodSchemaApi(zod) {
|
|
|
14382
14396
|
let schema = api2.object(shape);
|
|
14383
14397
|
if (opts.strict)
|
|
14384
14398
|
schema = schema.strict();
|
|
14385
|
-
return schema.refine(check2, message);
|
|
14399
|
+
return schema.refine(check2, typeof message === "string" ? message : { error: (issue2) => message(issue2.input) });
|
|
14386
14400
|
}
|
|
14387
14401
|
};
|
|
14388
14402
|
}
|
package/package.json
CHANGED
package/skills/envoy/SKILL.md
CHANGED
|
@@ -167,7 +167,9 @@ advertises.
|
|
|
167
167
|
Subscribe to `notifications.github.example-org.example-repo.pr.42.>` and end the turn. The single
|
|
168
168
|
`pr.42.checks` event whose `sha` is the current head wakes you when it settles (an earlier
|
|
169
169
|
commit's settlement can arrive first); a `pr.42` `closed` event with
|
|
170
|
-
`merged: true` tells you the PR merged. Do not create `gh` pollers.
|
|
170
|
+
`merged: true` tells you the PR merged. Do not create `gh` pollers. A Legion implementer waits this
|
|
171
|
+
way only on a re-run of a red its task names; otherwise it pushes and completes its phase, and the
|
|
172
|
+
daemon reads CI for it (`skill://legion-worker`).
|
|
171
173
|
Settlement waits for the commit to be quiet for a few seconds, every reported check run to finish,
|
|
172
174
|
and every recorded GitHub check suite to be `completed`. It covers those reported checks and suites
|
|
173
175
|
for that commit, not GitHub's required-checks set; until then, a silent subscription is normal.
|
|
@@ -445,7 +445,7 @@ active phase worker.
|
|
|
445
445
|
| `design-approved` | Its `version` is the approved one. A human approved the root spec document at its current version; the gate is open. Proceed to section 2 — except on a root whose catch-up said `handed_over`: its workers already run their phases, so start nothing; the approval releases any READY the gate refused. One whose `version` is the version a human's own answer to a decision block wrote, after an earlier approval (Dispatch carried that approval to it, section 1), opens nothing new: carry on, and release no wave because of it. |
|
|
446
446
|
| `design-changes-requested` | Its `version` and `reason`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec as the reason asks and request approval again as section 1 says; stay parked; the gate is closed. |
|
|
447
447
|
| `phase-finished` | The daemon has already moved the issue to its next phase by its fixed table and started that phase's role; you start nothing. Read the committed handoff for the finishing phase; if it shows unresolved gaps, tell the role now working the issue (`envoy_publish` to its role topic). A `planner` notice that names a departure from the spec's design defers to section 1's full condition: when the approved Summary, Acceptance, scope and settled decisions still hold, the plan is the record; otherwise change the root spec and request approval again as section 1 says. A `reviewer` notice whose GitHub review is `CHANGES_REQUESTED` needs nothing from you: the daemon has returned the issue to `implementing` (Dispatch `in_progress`) and started the **implementer**, whose correction goes through the tester and the reviewer again, never straight to retro. A `reviewer` notice with an `APPROVED` review means the issue is in `accepting` and the acceptance is yours (section 6): read the evidence, then `sign_off` or send it back with `request_backward_move`. A `tester` notice with `verdict: "fail"` — its handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof — has gone back to the **implementer** by the daemon's table; never supply the proof from another role. A worker that reports no surface reaches the changed path sends that report instead of completing its phase, so the daemon starts nothing more on that issue and the worker stays idle in its session, not suspended: file a child issue in this tree to build the surface (infrastructure, tooling, or a skill), and when that child's `child-closed` arrives, tell the waiting worker to continue with `envoy_publish` to its role topic. That report is never a reason to advance the phase. |
|
|
448
|
-
| `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict 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 the listener stopped at 100 paths or 32,768 runes of text, 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. The notice moves nothing, and no later CI red within the attempt that ran the count out (the counted head, and every head pushed on top of it until a push lands on a head CI judged green, the tester's red tests and handoff onto the held red included) sends the work back from `testing`, `reviewing`, `accepting`, `retro` or `merging` either: the issue stays in its phase until you move it, or a worker's own decision does (a tester's `fail`, a reviewer's request for changes). A head pushed onto green CI leaves that attempt, so its red still comes as a `checks-red`. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `accepting`, where no worker holds a phase, the move is yours: `request_backward_move` with `issue`, naming the failing checks. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch ask` naming the failing checks for the human who merges, as section 6 does for a conflict there. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names a conflict (`the head conflicts with <base>`), the required checks that failed (`CI is red at <head>`), or the merge queue's failed merge and its failing run (`the merge queue took pull request #<n> out at READY's head …`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker; your own send-back from `accepting` by the reason you gave. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed, or when the merge queue fails the merge) are the
|
|
448
|
+
| `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict 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 the listener stopped at 100 paths or 32,768 runes of text, 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. The notice moves nothing, and no later CI red within the attempt that ran the count out (the counted head, and every head pushed on top of it until a push lands on a head CI judged green, the tester's red tests and handoff onto the held red included) sends the work back from `testing`, `reviewing`, `accepting`, `retro` or `merging` either: the issue stays in its phase until you move it, or a worker's own decision does (a tester's `fail`, a reviewer's request for changes). A head pushed onto green CI leaves that attempt, so its red still comes as a `checks-red`. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `accepting`, where no worker holds a phase, the move is yours: `request_backward_move` with `issue`, naming the failing checks. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch ask` naming the failing checks for the human who merges, as section 6 does for a conflict there. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names a conflict (`the head conflicts with <base>`), the required checks that failed (`CI is red at <head>`), or the merge queue's failed merge and its failing run (`the merge queue took pull request #<n> out at READY's head …`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker; your own send-back from `accepting` by the reason you gave. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed, or when the merge queue fails the merge) are the rounds the cap is for. Each `review_round_cap` notice is a decision you make in the turn it wakes, never one left for the next round to make: read what every counted round found, choose one of the three below, act on it in the same turn, and post it as a `dispatch comment` on the issue naming the rounds you read and the choice. **Narrow**: the change itself is sound but each round finds more around it; tell the implementer and the reviewer (`envoy_publish` to each one's role topic) which acceptance lines the next round answers; a correctness finding is still fixed in this pull request, and only naming, duplication or wording cleanup moves to the `Fast-follow:` line. **Split**: part of the work cannot settle in this pull request; file that part as an issue of its own, never a child of this one (a parent waits in `accepting` for every child, and the split part usually builds on this pull request's unmerged code): `dispatch issue --project <project> --title '<outcome>' --blocked-by <this issue> --spec-file -`, a child of this issue's parent when it has one (`--parent <parent>`, released with `release_children` once this issue closes). Tell the implementer and the reviewer that this pull request no longer carries it. **Accept the rounds**: each round found less than the one before it, and the last found no defect in the change itself (a stale line, a comment that states an old default); let the loop run and say why in the comment. A later notice is a new decision: accept again only when its round found less than the round you last accepted, and otherwise narrow or split. A finding that changes what the spec accepts is a decision block in the root spec (section 1), and `dispatch ask` is only for a decision that needs a human's authority; neither replaces the choice. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` (section 6) is a conflict return. The merger's move back to `reviewing` for a READY refusal (`READY_UNREVIEWED_CHANGE`, or `READY_NOT_APPROVED` when its task names no approved head), and the reviewer's no-fix request for changes that follows it so the tester runs the head, are not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
|
|
449
449
|
| `pr-merged` | Its `reason` names the pull request. The PR merged, under the repository's rules, before the issue reached `awaiting_merge`. The workflow runs on and asks no one to merge it: the merge closes the issue the moment it reaches `awaiting_merge` (section 7), and you start nothing; the production check after the close is the implementer's, on the pull request, and asks nothing of you. Until then the phases run as they would, your acceptance included. |
|
|
450
450
|
| `pr-closed-unmerged` | Decide from current scope whether the work is reopened, started over (`park_child` then `rerun_child`, for a child), or ended with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
|
|
451
451
|
| `merge-refused` | Its `reason` names the pull request, READY's head, what the daemon asked GitHub to do (merge, enqueue, or enable auto-merge for it), and GitHub's refusal, which a retry would meet again (squash merges disabled, a permission the implement App lacks). The daemon has stopped submitting that merge; the issue stays in `awaiting_merge`. The cause is a repository setting or an App permission, a person's to change: ask one with `dispatch ask`, naming the refusal and the pull request at READY's head, which that person merges under the repository's rules once it is fixed. The merge closes the issue. Never bypass a rule. |
|
|
@@ -298,6 +298,8 @@ line), the full definition of a proof, what the tester verifies, and the simplif
|
|
|
298
298
|
- **No deferrals.** A finding that changes
|
|
299
299
|
behaviour, hides an error, or breaks a gate is fixed in this pull request; naming, duplication,
|
|
300
300
|
or wording cleanup is batched into the one `Fast-follow:` line instead of iterating per push.
|
|
301
|
+
- **The implementer pushes, then completes; it never waits for, watches or polls CI**, and never
|
|
302
|
+
sleeps in a loop: `skill://legion-worker/references/pr-body.md` (*Never waiting on CI*).
|
|
301
303
|
- **A red CI job** that failed on its own is re-run with
|
|
302
304
|
`gh run rerun <run-id> --failed`, never by pushing a new commit or bringing in the base.
|
|
303
305
|
- **A conflict or a retarget** is the only reason to bring the base into the branch, always as a
|
|
@@ -50,6 +50,28 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
50
50
|
**Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
The `CI` line names the runs GitHub started for the code head, by run id, with each run's state
|
|
54
|
+
as GitHub shows it when the line is written: `success`, a failure, or `queued` / `in progress`
|
|
55
|
+
at <head-sha>, or `no run yet` when GitHub has started none. The implementer writes it at its completion from one `gh run list --commit <head-sha>`
|
|
56
|
+
and never waits for a run to finish to fill it in: the daemon reads every settlement itself, and a
|
|
57
|
+
check or workflow the base branch requires that settles red sends the work back to `implementing`,
|
|
58
|
+
before or after that completion. A later role that reads CI on GitHub updates the line's states to
|
|
59
|
+
what it read.
|
|
60
|
+
|
|
61
|
+
## Never waiting on CI
|
|
62
|
+
|
|
63
|
+
The implementer runs the repository's fast local checks for the paths it changed before the push,
|
|
64
|
+
then pushes and completes. It never waits for, watches or polls the pull request's remote CI
|
|
65
|
+
(`gh run watch`, `gh pr checks --watch`, `sleep` around `gh run view`) and never sleeps in a loop.
|
|
66
|
+
Under the Go daemon a check or workflow the base branch requires that settles red on the head,
|
|
67
|
+
before the completion or after it, sends the work back to `implementing` with the failing checks
|
|
68
|
+
named, until `max_fix_attempts` is spent, when the architect decides. The one CI result an implementer waits for is a re-run: when its task names a red that
|
|
69
|
+
failed on its own, it re-runs the failed jobs once (`gh run rerun <run-id> --failed`) and completes once
|
|
70
|
+
the re-run settles green, woken by the pull request's checks event (`skill://envoy`, *Waiting for
|
|
71
|
+
CI or a merge*: subscribe, then end the turn), never by a sleep loop. A completion while that red
|
|
72
|
+
still stands is sent back, at once for a required check and at the daemon's next read of the runs
|
|
73
|
+
(within two minutes) for a required workflow.
|
|
74
|
+
|
|
53
75
|
## The brief for the human
|
|
54
76
|
|
|
55
77
|
`## For the reviewer` is written for the person who merges; `## Verification` below it stays the
|
|
@@ -96,7 +118,9 @@ and the tester's proof below are both this proof.
|
|
|
96
118
|
second and the concurrent run, the party it must refuse — judge whether the implementer's tests
|
|
97
119
|
would catch the ways it breaks, writing the red test for one that none would, and
|
|
98
120
|
read CI for everything static: lint, types and the unit and integration suites are CI's, cited
|
|
99
|
-
from the `CI` line, and no CI lane is rerun as evidence.
|
|
121
|
+
from the `CI` line, and no CI lane is rerun as evidence. A lane the line shows `queued` or
|
|
122
|
+
`in progress` is cited as such: the tester never waits for it, since a required red that settles
|
|
123
|
+
sends the work back from `testing` and the reviewer reads the head's checks before it approves.
|
|
100
124
|
A test handoff whose predecessor carried no proof is a test failure, not a gap for the tester to fill:
|
|
101
125
|
record it in `failures` with `implementerProof.verdict: "rejected"`, complete the phase with
|
|
102
126
|
`verdict: "fail"`, and the daemon returns the issue to the implementer — the agent that developed the change owns
|