@sjawhar/opencode-legion-envoy 8.3.3 → 8.3.5
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 +5 -4
- package/dist/src/server.js +2 -2
- package/package.json +1 -1
- package/skills/dispatch/references/issues.md +6 -4
- package/skills/legion-retro/SKILL.md +2 -2
- package/skills/legion-worker/SKILL.md +2 -2
- package/skills/legion-worker/references/conflicts-and-rewrites.md +23 -14
- package/skills/legion-worker/references/merge-gate.md +16 -7
package/dist/dispatch.js
CHANGED
|
@@ -14043,7 +14043,7 @@ var dispatchToolSpecs = [
|
|
|
14043
14043
|
status: "done",
|
|
14044
14044
|
reason: "Shipped in owner/repo#7; verified on the production dashboard."
|
|
14045
14045
|
},
|
|
14046
|
-
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, or attach it to " + "architecture components. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. external_links
|
|
14046
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, or attach it to " + "architecture components. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only rank, components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it; rank, the board's own order, is not " + "settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
14047
14047
|
arguments: (z2) => ({
|
|
14048
14048
|
issue: z2.string().describe(ISSUE_REFERENCE),
|
|
14049
14049
|
status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
@@ -14051,7 +14051,7 @@ var dispatchToolSpecs = [
|
|
|
14051
14051
|
title: z2.string({ min: 1, max: ISSUE_TITLE_MAX }).describe("Replacement title, at most 1,000 characters.").optional(),
|
|
14052
14052
|
labels: z2.array(z2.string({ min: 1, max: 40 }), { max: 20 }).describe("Replacement label set, at most 20 labels of up to 40 characters; replaces every existing label.").optional(),
|
|
14053
14053
|
priority: z2.number({ int: true, min: 0, max: 3 }).nullable().optional().describe("Coarse priority: 0 is P0 (highest) through 3 is P3 (lowest); null clears it."),
|
|
14054
|
-
external_links: z2.array(z2.string({ min: 1 })).describe("URLs to link;
|
|
14054
|
+
external_links: z2.array(z2.string({ min: 1 })).describe("URLs to link; a non-empty list merges into the issue's existing external links by URL, an empty list removes every link instead.").optional(),
|
|
14055
14055
|
route: z2.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
|
|
14056
14056
|
parent: z2.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
|
|
14057
14057
|
blocked_by: z2.array(z2.string({ min: 1 }), { max: MAX_ISSUE_BLOCKERS }).describe("Replacement list of issue keys in the same project this issue waits on; [] clears it. Dispatch refuses a dependency cycle.").optional(),
|
|
@@ -17515,6 +17515,7 @@ async function executeDispatchTool(input) {
|
|
|
17515
17515
|
}
|
|
17516
17516
|
const linked = before.external_links.map((link) => link.url);
|
|
17517
17517
|
const newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
|
|
17518
|
+
const sentLinks = requestedLinks === undefined ? undefined : requestedLinks.length === 0 ? [] : [...before.external_links, ...newLinks.map((url2) => ({ url: url2 }))];
|
|
17518
17519
|
let after;
|
|
17519
17520
|
try {
|
|
17520
17521
|
after = await client.updateIssue(issueKey, {
|
|
@@ -17526,7 +17527,7 @@ async function executeDispatchTool(input) {
|
|
|
17526
17527
|
...parent === undefined ? {} : { parent: parent === "" ? null : parent },
|
|
17527
17528
|
...blockedBy === undefined ? {} : { blocked_by: blockedBy },
|
|
17528
17529
|
...components === undefined ? {} : { components },
|
|
17529
|
-
...
|
|
17530
|
+
...sentLinks === undefined ? {} : { external_links: sentLinks },
|
|
17530
17531
|
actor
|
|
17531
17532
|
});
|
|
17532
17533
|
} catch (error48) {
|
|
@@ -17548,7 +17549,7 @@ async function executeDispatchTool(input) {
|
|
|
17548
17549
|
...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
|
|
17549
17550
|
...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
|
|
17550
17551
|
...requestedLinks === undefined ? [] : [
|
|
17551
|
-
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
17552
|
+
requestedLinks.length === 0 ? "external links cleared" : newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
17552
17553
|
],
|
|
17553
17554
|
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
|
|
17554
17555
|
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
package/dist/src/server.js
CHANGED
|
@@ -13900,7 +13900,7 @@ var dispatchToolSpecs = [
|
|
|
13900
13900
|
status: "done",
|
|
13901
13901
|
reason: "Shipped in owner/repo#7; verified on the production dashboard."
|
|
13902
13902
|
},
|
|
13903
|
-
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, or attach it to " + "architecture components. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. external_links
|
|
13903
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, or attach it to " + "architecture components. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only rank, components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it; rank, the board's own order, is not " + "settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
13904
13904
|
arguments: (z2) => ({
|
|
13905
13905
|
issue: z2.string().describe(ISSUE_REFERENCE),
|
|
13906
13906
|
status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
@@ -13908,7 +13908,7 @@ var dispatchToolSpecs = [
|
|
|
13908
13908
|
title: z2.string({ min: 1, max: ISSUE_TITLE_MAX }).describe("Replacement title, at most 1,000 characters.").optional(),
|
|
13909
13909
|
labels: z2.array(z2.string({ min: 1, max: 40 }), { max: 20 }).describe("Replacement label set, at most 20 labels of up to 40 characters; replaces every existing label.").optional(),
|
|
13910
13910
|
priority: z2.number({ int: true, min: 0, max: 3 }).nullable().optional().describe("Coarse priority: 0 is P0 (highest) through 3 is P3 (lowest); null clears it."),
|
|
13911
|
-
external_links: z2.array(z2.string({ min: 1 })).describe("URLs to link;
|
|
13911
|
+
external_links: z2.array(z2.string({ min: 1 })).describe("URLs to link; a non-empty list merges into the issue's existing external links by URL, an empty list removes every link instead.").optional(),
|
|
13912
13912
|
route: z2.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
|
|
13913
13913
|
parent: z2.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
|
|
13914
13914
|
blocked_by: z2.array(z2.string({ min: 1 }), { max: MAX_ISSUE_BLOCKERS }).describe("Replacement list of issue keys in the same project this issue waits on; [] clears it. Dispatch refuses a dependency cycle.").optional(),
|
package/package.json
CHANGED
|
@@ -85,7 +85,7 @@ issues you own as well as the root. An issue left at `triage` while work is unde
|
|
|
85
85
|
Waiting for the deploy lane is not a status and is never announced.
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
# PATCH /api/v1/issues/{key} — status, title, labels, priority, external links (merged by URL), route, parent, blocked_by
|
|
88
|
+
# PATCH /api/v1/issues/{key} — status, title, labels, priority, external links (merged by URL, or cleared by an empty list), route, parent, blocked_by
|
|
89
89
|
dispatch issue-update --issue PROJ-175 --status testing
|
|
90
90
|
dispatch issue-update --issue PROJ-175 --status done --reason 'Shipped in owner/repo#7; verified on the production dashboard.'
|
|
91
91
|
dispatch issue-update --issue PROJ-175 --priority 1 # 0–3; see Priority is yours to set
|
|
@@ -100,8 +100,9 @@ error names the posted message; after a timeout or a server error it also says t
|
|
|
100
100
|
landed, so read the issue's status first. A retry points its reason at the posted message rather
|
|
101
101
|
than repeating it.
|
|
102
102
|
|
|
103
|
-
The
|
|
104
|
-
empty string
|
|
103
|
+
The three clears differ: `--clear-priority` sends null, `--parent ''` and `--route ''` clear with an
|
|
104
|
+
empty string, and `--clear-external-links` / `--clear-blocked-by` send an empty list. Guessing the
|
|
105
|
+
wrong shape is a refusal either way.
|
|
105
106
|
|
|
106
107
|
Link the pull request that delivers the issue with `--external-link` when you open it; the issue page
|
|
107
108
|
renders its state and checks from that link, and `dispatch read` lists it under `External links:`.
|
|
@@ -122,7 +123,8 @@ Priority is the coarse bucket a backlog is read by: `0` is P0, the highest, thro
|
|
|
122
123
|
lowest, and `null` clears it. Agents set it (`dispatch://LEGION/artifact/issue-status-conventions-md`)
|
|
123
124
|
— on creation, and on a grooming pass over issues that have none — and say what you set and why;
|
|
124
125
|
the human overrides anything they disagree with from the dashboard. A closed
|
|
125
|
-
issue takes only `rank`, `components`,
|
|
126
|
+
issue takes only `rank`, `components`, a reopening `status` (any status but `done`), and an
|
|
127
|
+
external-links write that adds no new link (so a link can move to the issue that replaced it);
|
|
126
128
|
everything else, `priority` included, waits for the reopen (`409 ISSUE_CLOSED`). So reopen it
|
|
127
129
|
first, then set the priority — the two cannot go in one command. `rank` itself is not a flag:
|
|
128
130
|
reorder it the way [Issue reads](#board-rank-priority-and-assignee-on-a-read) describes, through `PATCH /api/v1/issues/{key}`.
|
|
@@ -133,8 +133,8 @@ merger.
|
|
|
133
133
|
they derive nothing, skip steps 2 to 4 and go on to the push.
|
|
134
134
|
2. Compute it the way they say, for the paths the pull request changes at your head, the files
|
|
135
135
|
GitHub lists on it:
|
|
136
|
-
`cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R "$LEGION_WORKSPACE" diff --from
|
|
137
|
-
lists them,
|
|
136
|
+
`base=$(gh pr view <n> -R <owner>/<repo> --json baseRefName -q '.baseRefName | @json') && cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R "$LEGION_WORKSPACE" diff --from "fork_point(${base}@origin | <head>)" --to <head> --name-only`
|
|
137
|
+
lists them, `base` the pull request's base branch as a jj string literal (never a name pasted between quotes) and `<head>` your removal commit. Where a
|
|
138
138
|
line affirms work, such as a checklist completed for a class of path, do that work for the
|
|
139
139
|
paths your commits add before you write the line: the line is a claim. That work may change
|
|
140
140
|
files under `docs/solutions/` only, folded into your docs commit before the push, since
|
|
@@ -176,8 +176,8 @@ config is one file shared by every issue workspace of the clone. Legion has two
|
|
|
176
176
|
one per role: your role's App is the **implement** App if you are the implementer or the merger,
|
|
177
177
|
and the **review** App if you are the planner, tester, reviewer, or an architect; your pane's
|
|
178
178
|
`JJ_USER` and `JJ_EMAIL` name your role's App's bot account. A planner's commits authored
|
|
179
|
-
by the review App are right. Before a push, check
|
|
180
|
-
`jj -R "$LEGION_WORKSPACE" log -r
|
|
179
|
+
by the review App are right. Before a push, check, with GitHub's default branch read as a jj string literal in the same command (never a name pasted between quotes),
|
|
180
|
+
`default=$(gh api repos/<owner>/<repo> --jq '.default_branch | @json') && jj -R "$LEGION_WORKSPACE" log -r "${default}@origin..@" -T 'author.email() ++ " | " ++ committer.email() ++ " " ++ description.first_line() ++ "\n"'`
|
|
181
181
|
shows your role's App in both columns **on every commit you made** — not on the whole list:
|
|
182
182
|
earlier phases' commits are legitimately authored by their own role's App. Their *committer* is
|
|
183
183
|
a different matter, and no longer noise to accept. Resolving a conflict rewrites nothing — it is a
|
|
@@ -11,7 +11,7 @@ Every path it cites is in sjawhar/legion.
|
|
|
11
11
|
merge, never `jj rebase`.**
|
|
12
12
|
The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
|
|
13
13
|
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never reintegrate the base to
|
|
14
|
-
pick up
|
|
14
|
+
pick up the default branch or refresh CI (a single failed CI job is re-run on its own: *A red CI job* in
|
|
15
15
|
`skill://legion-worker`). A conflict-forced rebase that leaves the branch's diff unchanged is a
|
|
16
16
|
confirmation, not a new round (see *The unchanged-diff check* below); that name is the event's,
|
|
17
17
|
kept by the rules below and the learnings that cite it, and the operation it names is always
|
|
@@ -28,10 +28,15 @@ Every path it cites is in sjawhar/legion.
|
|
|
28
28
|
`-r` all rewrite descendants; `-r` only re-parents them to fill the hole, which is worse).
|
|
29
29
|
Resolve the conflict with a forward merge instead of a rewrite — merge the branch's own
|
|
30
30
|
bookmark with the destination in one new commit, so nothing existing is rewritten and nothing
|
|
31
|
-
built on your prior commits, in this tree or another, ever moves
|
|
31
|
+
built on your prior commits, in this tree or another, ever moves. `base` is the pull request's
|
|
32
|
+
base branch (the repository's default branch unless the pull request is stacked), read as a jj
|
|
33
|
+
string literal: jj parses a bare bookmark name as an expression, and a name pasted between
|
|
34
|
+
quotes can close them itself (`release"@origin|"stable` would select `release` and `stable`), so
|
|
35
|
+
the revset is built from the serialized name alone:
|
|
32
36
|
|
|
33
37
|
```bash
|
|
34
|
-
|
|
38
|
+
base=$(gh pr view <n> -R <owner>/<repo> --json baseRefName -q '.baseRefName | @json') && \
|
|
39
|
+
jj -R "$LEGION_WORKSPACE" new legion/<KEY> "${base}@origin" -m "merge: resolve conflict against ${base}@origin"
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
Merge from the bookmark, never from `@`: a handoff split leaves `@` an empty, undescribed
|
|
@@ -44,8 +49,9 @@ Every path it cites is in sjawhar/legion.
|
|
|
44
49
|
pushes its own commits* in `skill://legion-worker`; a merge is a code push, so its head carries
|
|
45
50
|
no `skip-checks: true` line): the merge descends from both the bookmark's old position and the
|
|
46
51
|
destination, so it is a genuine fast-forward and *Rewriting pushed commits* never applies.
|
|
47
|
-
- **After a retarget.** Retargeting a pull request to a new base does not re-run Tests.
|
|
48
|
-
|
|
52
|
+
- **After a retarget.** Retargeting a pull request to a new base does not re-run Tests. Read
|
|
53
|
+
`base` again and merge the bookmark onto it in one command
|
|
54
|
+
(`base=$(gh pr view <n> -R <owner>/<repo> --json baseRefName -q '.baseRefName | @json') && jj -R "$LEGION_WORKSPACE" new legion/<KEY> "${base}@origin" -m "<message>"`) and push it the
|
|
49
55
|
ordinary way — a genuine fast-forward, never `--allow-backwards` — so the new head runs Tests
|
|
50
56
|
against the new merge result, and cite that run in the PR body.
|
|
51
57
|
|
|
@@ -55,20 +61,23 @@ The fingerprint every role compares after a conflict-forced rebase (every flag a
|
|
|
55
61
|
verified on jj 0.45.1):
|
|
56
62
|
|
|
57
63
|
```bash
|
|
64
|
+
base=$(gh pr view <n> -R <owner>/<repo> --json baseRefName -q '.baseRefName | @json') && \
|
|
58
65
|
cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && \
|
|
59
|
-
|
|
66
|
+
fingerprint=$(set -o pipefail; jj -R "$LEGION_WORKSPACE" diff --from "fork_point(${base}@origin | <head-sha>)" --to <head-sha> \
|
|
60
67
|
--git --context 0 '~(.legion | docs/solutions)' \
|
|
61
|
-
| sed -e '/^@@/d' -e '/^index /d' | sha256sum
|
|
68
|
+
| sed -e '/^@@/d' -e '/^index /d' | sha256sum) && echo "$fingerprint"
|
|
62
69
|
```
|
|
63
70
|
|
|
64
71
|
- `<head-sha>` is a full commit SHA; a jj commit id is the git SHA GitHub shows.
|
|
65
|
-
- `fork_point(
|
|
66
|
-
the old base for the pre-rebase head, the new base for the rebased one, so one command
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- A head the rebase hid is still addressable by its SHA in the shared workspace.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
- `fork_point(${base}@origin | <head-sha>)` is the base the branch was cut from *at that head*:
|
|
73
|
+
the old base for the pre-rebase head, the new base for the rebased one, so one command serves
|
|
74
|
+
both sides. `base` is the pull request's base branch as a jj string literal, as in the merge
|
|
75
|
+
above.
|
|
76
|
+
- A head the rebase hid is still addressable by its SHA in the shared workspace. The check
|
|
77
|
+
prints a fingerprint only when every step of it succeeded: the base read, the fetch and the
|
|
78
|
+
diff (`pipefail` keeps the diff's failure through `sed` and `sha256sum`, which would otherwise
|
|
79
|
+
hash empty input to `e3b0c442…`). A nonzero exit or no printed fingerprint (a failed read of the
|
|
80
|
+
base, or a SHA the workspace cannot resolve) counts as a changed diff — never as unchanged.
|
|
72
81
|
- `--context 0` drops context lines; the `sed` drops `@@` hunk headers (line positions move
|
|
73
82
|
on a rebase) and `index` lines (blob ids move when the base's copy of a file changed). What
|
|
74
83
|
is left is exactly the added and removed lines per file.
|
|
@@ -25,12 +25,20 @@ the head voids it.
|
|
|
25
25
|
every commit above that push's head changes only `.legion/` (a push that changes only
|
|
26
26
|
`.legion/` carries `skip-checks: true`, so GitHub runs no workflow for it). With `<o>/<r>`,
|
|
27
27
|
`<n>`, `<base>`, `<headRefName>` and `<headRefOid>` from
|
|
28
|
-
`gh pr view <n> --json baseRefName,headRefOid,headRefName`, four reads. The required set
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
`gh pr view <n> --json baseRefName,headRefOid,headRefName`, four reads. The required set is the
|
|
29
|
+
base branch's rulesets' required checks and its branch protection's, when it has any. Both
|
|
30
|
+
branch endpoints take the base as a URL path, each segment escaped and each `/` kept (a raw `#`
|
|
31
|
+
or `%` reads another branch), built in the same command from the name GitHub returns, never
|
|
32
|
+
pasted or carried over from an earlier command:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
base=$(gh pr view <n> -R <o>/<r> --json baseRefName -q .baseRefName) && \
|
|
36
|
+
base_path=$(jq -rn --arg b "$base" '$b | split("/") | map(@uri) | join("/")') && \
|
|
37
|
+
echo rulesets: && gh api "repos/<o>/<r>/rules/branches/$base_path" --jq '.[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context' && \
|
|
38
|
+
echo protection: && gh api "repos/<o>/<r>/branches/$base_path" --jq '.protection.required_status_checks.checks[].context'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A read GitHub refuses stops the command there, and is reported to the architect, never guessed. The CI head, the head of the
|
|
34
42
|
branch's newest `pull_request` run
|
|
35
43
|
(`gh api 'repos/<o>/<r>/actions/runs?branch=<headRefName>&event=pull_request&per_page=30' --jq '.workflow_runs[0].head_sha'`,
|
|
36
44
|
newest first); the PR body's `CI:` line names runs at that same commit
|
|
@@ -62,7 +70,8 @@ the head voids it.
|
|
|
62
70
|
`APPROVE`: submit one more `APPROVE` naming the new head by SHA, its body naming both SHAs
|
|
63
71
|
and the fingerprint — a confirmation, not a round; no thermo pass, no thread pass. Equal and
|
|
64
72
|
that review was `COMMENT` or `REQUEST_CHANGES`: continue that round against the new head;
|
|
65
|
-
nothing restarts. Different: a new round — thermo again,
|
|
73
|
+
nothing restarts. Different, or either run printed no fingerprint: a new round — thermo again,
|
|
74
|
+
one review.
|
|
66
75
|
- Answer every thread you opened, and every thread a bot opened that is none of Legion's role
|
|
67
76
|
Apps, as `skill://legion-worker/references/review-threads.md` says; the same reference says
|
|
68
77
|
when every thread is settled enough to approve, and resolving one never gates your approval.
|