@sjawhar/opencode-legion-envoy 8.20.4 → 8.20.6
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
|
@@ -132,6 +132,8 @@ else:
|
|
|
132
132
|
`issues.<KEY>.architect.state` is `failed` and whose `issues.<KEY>.phase` is not `done`, exactly
|
|
133
133
|
as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
|
|
134
134
|
nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
|
|
135
|
+
A root at phase `done` with a running tree's `status` (`in_progress`, `testing`, `needs_review`,
|
|
136
|
+
`retro`) is one Legion let go while someone else works it (`unslotted`): name it in your summary.
|
|
135
137
|
2. List the project's triage issues handed to Legion with
|
|
136
138
|
`dispatch issues --project <PROJECT> --status triage --label legion --limit 250 --offset 0`.
|
|
137
139
|
When its first line ends `(showing 1-250 of N)`, read the next page with `--offset 250`, and so
|
|
@@ -164,18 +166,17 @@ tree's architect once its root is admitted. `set_status` to `todo` admits a root
|
|
|
164
166
|
outside a live tree (admitted as a root of its own), only while it carries the label, so an issue
|
|
165
167
|
you hand over or file for Legion to run carries it first (`--label` on `dispatch issue-update` or
|
|
166
168
|
`dispatch issue`). Taking the label off a waiting root drops it from the waiting line; taking it
|
|
167
|
-
off an admitted tree does not stop it.
|
|
169
|
+
off an admitted tree does not stop it ([reading admission](skill://legion-controller/references/admission.md)).
|
|
168
170
|
|
|
169
171
|
## Trees waiting on a root claim (Go daemon)
|
|
170
172
|
|
|
171
|
-
A Go root architect whose claim on its root issue was refused starts nothing and waits, holding
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`issues.<KEY>.architect.locator.claim` in `read_state`. A claim that is its architect's
|
|
173
|
+
A Go root architect whose claim on its root issue was refused starts nothing and waits, holding its slot,
|
|
174
|
+
until the claim is free; nothing tells it when a session holder lets go without replying. So every turn
|
|
175
|
+
rechecks them ([Turn discipline](#turn-discipline)), the daemon's `tick` included, which comes on its
|
|
176
|
+
interval even with every slot taken: read each root in `admission.active` whose `issues.<KEY>.phase` is
|
|
177
|
+
still `admitted` with `dispatch read`. When its `Claimed by:` line is `nobody` or ends `· not running`,
|
|
178
|
+
tell that tree's architect to claim again with `envoy_publish` to `notifications.role.` followed by its
|
|
179
|
+
claim token, `issues.<KEY>.architect.locator.claim` in `read_state`. A claim that is its architect's
|
|
179
180
|
own, or one that still holds, needs nothing.
|
|
180
181
|
|
|
181
182
|
## Keeping the slots full (Go daemon)
|
|
@@ -217,22 +218,20 @@ skipped (the last row of the table below). With no scope stated, every issue of
|
|
|
217
218
|
scope.
|
|
218
219
|
|
|
219
220
|
**How many.** Read the daemon's state with `read_state`. The free slots are `admission.cap` minus the roots in
|
|
220
|
-
`admission.active` and in `admission.waiting` (a waiting root takes the next slot before anything
|
|
221
|
-
you add). With none free, stop.
|
|
221
|
+
`admission.active` and in `admission.waiting` (a waiting root takes the next slot before anything you add). With none free, stop.
|
|
222
222
|
|
|
223
223
|
**Candidates.** The project's open `todo` issues, roots and children alike, that have no children
|
|
224
224
|
at all and do not carry the `legion` label. Ready work is `todo` (`skill://dispatch`, "Choosing
|
|
225
225
|
what to work on"): take the top ready issue, highest priority first, then board rank. An issue that
|
|
226
226
|
waits on a deploy or a decision belongs in `backlog`, so the walk takes nothing from `backlog` or
|
|
227
|
-
`triage`. The Go daemon runs
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
last, keeping the listing's order within each, which is the board's rank:
|
|
227
|
+
`triage`. The Go daemon runs only labelled roots, so every root it ran since the daemon required
|
|
228
|
+
the label carries it: a labelled root in `todo` is the daemon's to admit or queue, one in `triage`
|
|
229
|
+
is yours to triage (step 2 above), and one anywhere else was parked by Legion or by a person. A
|
|
230
|
+
child you take becomes a root of its own: the daemon admits a labelled `todo` child whose tree
|
|
231
|
+
Legion does not run as a new tree. The `dispatch issues` rows show no labels and no children, so
|
|
232
|
+
the listing below filters neither: both are checked on each candidate (the table's first rows).
|
|
233
|
+
List the `todo` issues one priority at a time, `--priority 0` first, then `1`, `2`, `3`, and
|
|
234
|
+
`none` (no priority) last, keeping the listing's order within each, which is the board's rank:
|
|
236
235
|
|
|
237
236
|
```text
|
|
238
237
|
dispatch issues --project <PROJECT> --status todo --priority 0 --limit 250 --offset 0
|
|
@@ -406,6 +405,7 @@ highest priority first, then board rank ([Keeping the slots full](#keeping-the-s
|
|
|
406
405
|
| New issue created in the Dispatch project, status `triage`: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above); the start procedure above catches one sent while no controller ran | issue key + triage context (incl. pre-existing children) | Triage: `set_status` to `todo` to admit, or to `backlog`/`icebox` to park |
|
|
407
406
|
| `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `read_state`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
408
407
|
| `todo on <KEY>` from the Go daemon (payload `{kind: "todo"}`) | an issue not handed to Legion that changed while in `todo` and a slot stood free, sent half a minute later | Verify a free slot, then walk the whole `todo` list ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
408
|
+
| `unslotted on <KEY>` from the Go daemon (payload `{kind: "unslotted", reason}`) | a root that held no slot and waited for none: a person or another session moved it from `todo` to the status `reason` names while it waited, so the daemon took it out of the workflow (`issues.<KEY>.phase` is `done`) and starts nothing for it ([reading admission](skill://legion-controller/references/admission.md)) | Re-admit nothing yourself: the move was someone else's, and `dispatch read` shows who claims it now. Name it in your summary to the human ("<KEY> left Legion's waiting line: it was set to <status> while it waited for a slot, so Legion let it go; `todo` with the `legion` label queues it again") |
|
|
409
409
|
| `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `read_state`, and the live capabilities a running session's check failed, each with the detail naming the session (pod or pane), role, issue and fact, and no `configLine` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides a deployment row; the operator or the issue's architect fixes a live row's session; you take no other action) |
|
|
410
410
|
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `dispatch ask` only for a human to-do |
|
|
411
411
|
| Mention | GitHub PR @mention text | Answer, or route to the owning issue's architect role |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Reading admission
|
|
2
|
+
|
|
3
|
+
`read_state` says where each issue Legion records stands, and its `phase` alone does not: an issue
|
|
4
|
+
is `admitted` from the moment the daemon records it until its first phase starts, whether it runs,
|
|
5
|
+
waits, or neither. Read its key against `admission` and its tree's root.
|
|
6
|
+
|
|
7
|
+
| What `read_state` shows | What it means | What you do |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| The key is in `admission.active` | A root holding a slot: its tree runs, `admitted` until its architect starts the first phase | Nothing, beyond [trees waiting on a root claim](skill://legion-controller/SKILL.md#trees-waiting-on-a-root-claim-go-daemon) |
|
|
10
|
+
| The key is in `admission.waiting` | A root handed to Legion in `todo`, queued in Dispatch rank order for the next free slot | Nothing: it takes a slot before anything you add |
|
|
11
|
+
| `phase: admitted`, in neither list, and Dispatch shows a `child_of` parent | A child: it runs or waits with its tree, the root its `child_of` chain reaches. Under a parked root (its `phase` is `done`) it is parked with it | Leave it to its tree. It runs again when its root does, or as a root of its own once it is `todo` with the `legion` label |
|
|
12
|
+
| `phase: admitted`, `status: todo`, in neither list, no parent | A root a person took out of the line by taking its label off | Nothing: the label puts it back in the line, and a person's label is their decision |
|
|
13
|
+
| `phase: done` while `status` is `in_progress`, `testing`, `needs_review` or `retro` | Someone other than Legion moved it to a running tree's status while Legion was not running it. A root that waited for a slot is taken out of the workflow at that move, since no slot would ever reach it, and the daemon sends the `unslotted` wake; a parked child stays parked. Legion starts nothing for either | Re-admit nothing yourself: `dispatch read` shows who works it now. Name it in your summary; `todo` with the `legion` label queues it again |
|
|
14
|
+
|
|
15
|
+
A root in the workflow with no slot that is neither waiting nor paused by its label does not stay
|
|
16
|
+
that way: every pass of admission (a Dispatch event, the daemon's boot read, its tick) takes it out
|
|
17
|
+
of the workflow and sends the `unslotted` wake. The wake is live only, so a controller that starts
|
|
18
|
+
later finds such a root by the row above, as its start procedure does.
|