@sjawhar/opencode-legion-envoy 4.1.1 → 5.0.0

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.
@@ -14394,168 +14394,6 @@ var EnvelopeSchema = exports_external.object({
14394
14394
  });
14395
14395
  var MINTED_DEDUPE_KEY_PATTERN = "^(?:envoy\\.role\\.forward\\.)?(?:publish|agent\\.[^.]+)\\.(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$";
14396
14396
  var mintedDedupeKey = new RegExp(MINTED_DEDUPE_KEY_PATTERN);
14397
- // ../contracts/src/handoff-schema.ts
14398
- var HANDOFF_SCHEMA_VERSION = 1;
14399
- var HANDOFF_PHASES = ["architect", "plan", "implement", "test", "review"];
14400
- var PLAN_REVIEW_MAX_ROUNDS = 3;
14401
- var PLAN_REVIEW_VERDICTS = ["approved", "rejected", "failed"];
14402
- var isoTimestamp = exports_external.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
14403
- var handoffPhase = exports_external.enum(HANDOFF_PHASES);
14404
- var nonEmpty = exports_external.string().trim().min(1);
14405
- var proofSchema = exports_external.object({
14406
- criterion: nonEmpty,
14407
- surface: nonEmpty,
14408
- command: nonEmpty,
14409
- observed: nonEmpty,
14410
- headSha: nonEmpty,
14411
- negativeControl: nonEmpty
14412
- }).passthrough();
14413
- var routingHintsSchema = exports_external.object({
14414
- skipArchitect: exports_external.boolean().optional(),
14415
- complexity: exports_external.enum(["trivial", "small", "medium", "large"]).optional(),
14416
- estimatedImplementers: exports_external.number().optional()
14417
- }).passthrough().optional();
14418
- var baseHandoffSchema = exports_external.object({
14419
- schemaVersion: exports_external.literal(HANDOFF_SCHEMA_VERSION),
14420
- phase: handoffPhase,
14421
- completed: isoTimestamp,
14422
- learningsInjected: exports_external.array(exports_external.string()).optional(),
14423
- learningsHelpful: exports_external.array(exports_external.string()).optional()
14424
- }).passthrough();
14425
- var architectSchema = baseHandoffSchema.extend({
14426
- phase: exports_external.literal("architect"),
14427
- scope: exports_external.enum(["trivial", "small", "medium", "large"]).optional(),
14428
- components: exports_external.array(exports_external.string()).optional(),
14429
- subIssues: exports_external.array(exports_external.string()).optional(),
14430
- routingHints: routingHintsSchema,
14431
- concerns: exports_external.array(exports_external.string()).optional()
14432
- });
14433
- var requiredSkillsSchema = exports_external.object({
14434
- implement: exports_external.array(exports_external.string()).optional(),
14435
- test: exports_external.array(exports_external.string()).optional(),
14436
- review: exports_external.array(exports_external.string()).optional()
14437
- }).passthrough().optional();
14438
- var gapAnalysisSchema = exports_external.object({
14439
- findings: exports_external.array(exports_external.object({ finding: exports_external.string(), answer: exports_external.string() }).passthrough()).optional(),
14440
- error: exports_external.string().optional()
14441
- }).passthrough().optional();
14442
- var planReviewSchema = exports_external.object({
14443
- verdict: exports_external.enum(PLAN_REVIEW_VERDICTS),
14444
- rounds: exports_external.number(),
14445
- remainingIssues: exports_external.array(exports_external.object({ issue: exports_external.string(), evidence: exports_external.string() }).passthrough()).optional(),
14446
- error: exports_external.string().optional()
14447
- }).passthrough().optional();
14448
- var planSchema = baseHandoffSchema.extend({
14449
- phase: exports_external.literal("plan"),
14450
- taskCount: exports_external.number().optional(),
14451
- independentTasks: exports_external.number().optional(),
14452
- routingHints: routingHintsSchema,
14453
- concerns: exports_external.array(exports_external.string()).optional(),
14454
- workflowRecommendation: exports_external.string().optional(),
14455
- requiredSkills: requiredSkillsSchema,
14456
- gapAnalysis: gapAnalysisSchema,
14457
- planReview: planReviewSchema
14458
- });
14459
- var implementSchema = baseHandoffSchema.extend({
14460
- phase: exports_external.literal("implement"),
14461
- filesChanged: exports_external.array(exports_external.string()).optional(),
14462
- proof: exports_external.array(proofSchema).min(1),
14463
- trickyParts: exports_external.array(exports_external.string()).optional(),
14464
- deviations: exports_external.array(exports_external.string()).optional(),
14465
- openQuestions: exports_external.array(exports_external.string()).optional(),
14466
- subPlanningNeeded: exports_external.boolean().optional(),
14467
- discoveredComplexity: exports_external.array(exports_external.string()).optional(),
14468
- suggestedSubWorkers: exports_external.number().optional()
14469
- });
14470
- var testSchema = baseHandoffSchema.extend({
14471
- phase: exports_external.literal("test"),
14472
- passed: exports_external.number().optional(),
14473
- failed: exports_external.number().optional(),
14474
- failures: exports_external.array(exports_external.object({ criterion: exports_external.string(), evidence: exports_external.string() }).passthrough()).optional(),
14475
- implementerProof: exports_external.object({ verdict: exports_external.enum(["verified", "rejected"]), how: nonEmpty }).passthrough(),
14476
- proof: exports_external.array(proofSchema).min(1).optional(),
14477
- documentationFeedback: exports_external.string().optional(),
14478
- observations: exports_external.array(exports_external.string()).optional()
14479
- }).refine((handoff) => (handoff.failures?.length ?? 0) > 0 || (handoff.failed ?? 0) > 0 || (handoff.proof?.length ?? 0) > 0, {
14480
- path: ["proof"],
14481
- message: "a passing test handoff needs the tester's own production-like proof"
14482
- }).refine((handoff) => (handoff.failed ?? 0) === 0 || (handoff.failures?.length ?? 0) > 0, {
14483
- path: ["failures"],
14484
- message: "a test handoff that reports failed > 0 records at least one failure"
14485
- }).refine((handoff) => handoff.implementerProof.verdict !== "rejected" || (handoff.failures?.length ?? 0) > 0, {
14486
- path: ["failures"],
14487
- message: "a rejected implementer proof is a recorded failure"
14488
- });
14489
- var reviewSchema = baseHandoffSchema.extend({
14490
- phase: exports_external.literal("review"),
14491
- critical: exports_external.number().optional(),
14492
- important: exports_external.number().optional(),
14493
- minor: exports_external.number().optional(),
14494
- verdict: exports_external.enum(["approved", "changes_requested"]).optional(),
14495
- keyFindings: exports_external.array(exports_external.object({ severity: exports_external.string(), file: exports_external.string(), description: exports_external.string() }).passthrough()).optional()
14496
- });
14497
- var nonEmptySkillList = exports_external.array(nonEmpty).min(1);
14498
- var recorded = (shape, whatToRecord) => exports_external.object(shape, {
14499
- error: (issue2) => issue2.input === undefined ? `missing \u2014 record ${whatToRecord}` : undefined
14500
- }).passthrough();
14501
- var gapAnalysisWriteSchema = recorded({
14502
- findings: exports_external.array(exports_external.object({ finding: nonEmpty, answer: nonEmpty }).passthrough()).optional(),
14503
- error: nonEmpty.optional()
14504
- }, "the gap analyst's `findings`, each with how the plan answers it (`[]` when it found none), or its failed call's `error`").refine((analysis) => analysis.findings === undefined !== (analysis.error === undefined), {
14505
- message: "record exactly one of `findings` or the failed call's `error`"
14506
- });
14507
- var planReviewWriteSchema = recorded({
14508
- verdict: exports_external.enum(PLAN_REVIEW_VERDICTS),
14509
- rounds: exports_external.number().int().min(1).max(PLAN_REVIEW_MAX_ROUNDS),
14510
- remainingIssues: exports_external.array(exports_external.object({ issue: nonEmpty, evidence: nonEmpty }).passthrough()).optional(),
14511
- error: nonEmpty.optional()
14512
- }, "the plan review's `verdict` and `rounds`, with `remainingIssues` when it was rejected or `error` when a review's call failed").superRefine((review, ctx) => {
14513
- const remaining = review.remainingIssues?.length ?? 0;
14514
- if (review.verdict === "rejected" && remaining === 0) {
14515
- ctx.addIssue({
14516
- code: "custom",
14517
- path: ["remainingIssues"],
14518
- message: "a rejected review records the blocking issues its last round named"
14519
- });
14520
- }
14521
- if (review.verdict === "rejected" && review.rounds < PLAN_REVIEW_MAX_ROUNDS) {
14522
- ctx.addIssue({
14523
- code: "custom",
14524
- path: ["rounds"],
14525
- message: `a review still rejecting after ${review.rounds} of ${PLAN_REVIEW_MAX_ROUNDS} rounds is revised and reviewed again, not recorded`
14526
- });
14527
- }
14528
- if (review.verdict === "approved" && remaining > 0) {
14529
- ctx.addIssue({
14530
- code: "custom",
14531
- path: ["remainingIssues"],
14532
- message: "an approved review leaves no blocking issue standing"
14533
- });
14534
- }
14535
- if (review.verdict === "failed" !== (review.error !== undefined)) {
14536
- ctx.addIssue({
14537
- code: "custom",
14538
- path: ["error"],
14539
- message: "a failed review records its call's error, and only a failed review does"
14540
- });
14541
- }
14542
- });
14543
- var planWriteSchema = planSchema.extend({
14544
- requiredSkills: exports_external.object({
14545
- implement: nonEmptySkillList,
14546
- test: nonEmptySkillList,
14547
- review: nonEmptySkillList
14548
- }).passthrough(),
14549
- gapAnalysis: gapAnalysisWriteSchema,
14550
- planReview: planReviewWriteSchema
14551
- });
14552
- var phaseHandoffSchema = exports_external.discriminatedUnion("phase", [
14553
- architectSchema,
14554
- planSchema,
14555
- implementSchema,
14556
- testSchema,
14557
- reviewSchema
14558
- ]);
14559
14397
  // ../contracts/src/subject.ts
14560
14398
  var AGENT_TOPIC_PREFIX = "notifications.agent.";
14561
14399
  var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
@@ -14570,283 +14408,6 @@ function dispatchDocumentSubject(project, slug, type) {
14570
14408
  function agentSubject(session) {
14571
14409
  return `${AGENT_TOPIC_PREFIX}${session}`;
14572
14410
  }
14573
-
14574
- // ../contracts/src/legion-roles.ts
14575
- var LEGION_ROLES = [
14576
- "architect",
14577
- "planner",
14578
- "implementer",
14579
- "tester",
14580
- "reviewer",
14581
- "merger"
14582
- ];
14583
-
14584
- // ../contracts/src/legion-daemon-api.ts
14585
- var nonEmptyString = exports_external.string().min(1);
14586
- var appLogin = exports_external.string().regex(/^[^[\]]+\[bot\]$/);
14587
- var legionRole = exports_external.enum(LEGION_ROLES);
14588
- var requiredUnknown = exports_external.unknown().refine((value) => value !== undefined, {
14589
- message: "Required"
14590
- });
14591
- var architectCapability = exports_external.strictObject({
14592
- tree: nonEmptyString,
14593
- sessionId: nonEmptyString,
14594
- secret: nonEmptyString
14595
- });
14596
- var controllerIssue = exports_external.strictObject({
14597
- secret: nonEmptyString,
14598
- issue: nonEmptyString
14599
- });
14600
- var TREE_STATUSES = ["queued", "active", "lingering", "dead", "launch-failed", "closed"];
14601
- var stateTmuxLocator = exports_external.strictObject({
14602
- runtime: exports_external.literal("tmux"),
14603
- tmuxSession: nonEmptyString,
14604
- tmuxWindowId: nonEmptyString,
14605
- tmuxPaneId: nonEmptyString.optional()
14606
- });
14607
- var stateK8sLocator = exports_external.strictObject({
14608
- runtime: exports_external.literal("kubernetes"),
14609
- namespace: nonEmptyString,
14610
- podName: nonEmptyString,
14611
- podUid: nonEmptyString,
14612
- pvcName: nonEmptyString
14613
- });
14614
- var stateLocator = exports_external.discriminatedUnion("runtime", [stateTmuxLocator, stateK8sLocator]);
14615
- var stateTreeLocator = exports_external.discriminatedUnion("runtime", [
14616
- stateTmuxLocator.extend({ ompSessionFile: nonEmptyString.optional() }),
14617
- stateK8sLocator.extend({ ompSessionFile: nonEmptyString.optional() })
14618
- ]);
14619
- var stateExternalControllerLocator = exports_external.strictObject({
14620
- runtime: exports_external.literal("kubernetes"),
14621
- external: exports_external.literal(true),
14622
- sessionId: nonEmptyString,
14623
- registeredAt: exports_external.number().int().nonnegative()
14624
- });
14625
- var stateIssue = exports_external.strictObject({
14626
- key: nonEmptyString,
14627
- title: exports_external.string(),
14628
- status: exports_external.enum(ISSUE_STATUSES).optional(),
14629
- children: exports_external.array(nonEmptyString),
14630
- parent: nonEmptyString.optional(),
14631
- lastAppliedSeq: exports_external.number().int().nonnegative().optional()
14632
- });
14633
- var stateWorkspaceLost = exports_external.strictObject({
14634
- at: nonEmptyString,
14635
- generation: exports_external.number().int().nonnegative(),
14636
- fromRef: nonEmptyString,
14637
- previousSessionId: nonEmptyString.optional()
14638
- });
14639
- var stateTree = exports_external.strictObject({
14640
- status: exports_external.enum(TREE_STATUSES),
14641
- generation: exports_external.number().int().nonnegative(),
14642
- launchFailures: exports_external.number().int().nonnegative(),
14643
- readyConfirmedAt: exports_external.number().optional(),
14644
- locator: stateTreeLocator.optional(),
14645
- workspaceLost: stateWorkspaceLost.optional()
14646
- });
14647
- var stateGate = exports_external.strictObject({
14648
- artifactId: nonEmptyString,
14649
- latestVersion: exports_external.number().int().positive(),
14650
- approvedVersion: exports_external.number().int().positive().optional()
14651
- });
14652
- var stateRole = exports_external.strictObject({
14653
- role: nonEmptyString,
14654
- issue: nonEmptyString.optional(),
14655
- generation: exports_external.number().int().nonnegative().optional(),
14656
- sessionId: nonEmptyString.optional(),
14657
- readyConfirmedAt: exports_external.number().optional(),
14658
- launchFailures: exports_external.number().int().nonnegative().optional(),
14659
- locator: stateLocator.optional(),
14660
- workspaceLost: stateWorkspaceLost.optional()
14661
- });
14662
- var stateQueuedWorkerIdentity = {
14663
- roleToken: nonEmptyString,
14664
- issue: nonEmptyString,
14665
- role: nonEmptyString
14666
- };
14667
- var stateQueuedWorker = exports_external.union([
14668
- exports_external.strictObject(stateQueuedWorkerIdentity),
14669
- exports_external.strictObject({
14670
- ...stateQueuedWorkerIdentity,
14671
- kind: exports_external.enum(["assignment", "catchup"]),
14672
- queuedAt: nonEmptyString
14673
- })
14674
- ]);
14675
- var LegionDaemonApi = {
14676
- State: {
14677
- response: exports_external.strictObject({
14678
- project: nonEmptyString,
14679
- version: exports_external.number().int(),
14680
- issues: exports_external.record(exports_external.string(), stateIssue),
14681
- trees: exports_external.record(exports_external.string(), stateTree),
14682
- admission: exports_external.strictObject({
14683
- cap: exports_external.number().int().nonnegative(),
14684
- active: exports_external.array(nonEmptyString),
14685
- queue: exports_external.array(nonEmptyString)
14686
- }),
14687
- gates: exports_external.record(exports_external.string(), stateGate),
14688
- controllerLocator: exports_external.union([stateTreeLocator, stateExternalControllerLocator]).optional(),
14689
- roles: exports_external.record(exports_external.string(), stateRole),
14690
- controllerPendingNotices: exports_external.number().int().nonnegative(),
14691
- pendingStatusWrites: exports_external.array(nonEmptyString),
14692
- workerAdmission: exports_external.strictObject({ queue: exports_external.array(stateQueuedWorker) })
14693
- })
14694
- },
14695
- ControllerReady: {
14696
- request: exports_external.strictObject({
14697
- secret: nonEmptyString,
14698
- sessionId: nonEmptyString,
14699
- ompSessionFile: nonEmptyString.optional(),
14700
- pluginVersion: nonEmptyString
14701
- }),
14702
- response: exports_external.object({})
14703
- },
14704
- ControllerSecret: {
14705
- request: exports_external.strictObject({}),
14706
- response: exports_external.object({ secret: nonEmptyString })
14707
- },
14708
- ProcessStarted: {
14709
- request: exports_external.strictObject({
14710
- tree: nonEmptyString,
14711
- generation: exports_external.number().int(),
14712
- rootSessionId: nonEmptyString,
14713
- agentId: nonEmptyString,
14714
- bootToken: nonEmptyString,
14715
- ompSessionFile: nonEmptyString,
14716
- pluginVersion: nonEmptyString
14717
- }),
14718
- response: exports_external.object({
14719
- roleTokens: exports_external.record(exports_external.string(), exports_external.string()),
14720
- controlSubject: nonEmptyString,
14721
- gates: exports_external.object({
14722
- design: exports_external.enum(["root-issues", "off"])
14723
- }).optional(),
14724
- secret: nonEmptyString
14725
- })
14726
- },
14727
- ProcessReady: {
14728
- request: architectCapability.extend({ generation: exports_external.number().int() }),
14729
- response: exports_external.object({})
14730
- },
14731
- ProcessExit: {
14732
- request: architectCapability.extend({ generation: exports_external.number().int() }),
14733
- response: exports_external.object({})
14734
- },
14735
- WaveRelease: {
14736
- request: architectCapability.extend({ issues: exports_external.array(nonEmptyString).optional() }),
14737
- response: exports_external.object({ released: exports_external.array(nonEmptyString) })
14738
- },
14739
- Escalate: {
14740
- request: architectCapability.extend({
14741
- kind: exports_external.enum(["re-file", "capacity", "cross-tree"]),
14742
- context: requiredUnknown
14743
- }),
14744
- response: exports_external.object({})
14745
- },
14746
- ProvisioningCredential: {
14747
- request: architectCapability.extend({ issue: nonEmptyString }),
14748
- response: exports_external.object({ token: nonEmptyString })
14749
- },
14750
- WorkerStarted: {
14751
- request: exports_external.strictObject({
14752
- tree: nonEmptyString,
14753
- issue: nonEmptyString,
14754
- role: legionRole,
14755
- bootToken: nonEmptyString,
14756
- sessionId: nonEmptyString,
14757
- agentId: nonEmptyString,
14758
- ompSessionFile: nonEmptyString,
14759
- pluginVersion: nonEmptyString
14760
- }),
14761
- response: exports_external.object({
14762
- roleToken: nonEmptyString,
14763
- secret: nonEmptyString,
14764
- gitName: nonEmptyString,
14765
- gitEmail: nonEmptyString
14766
- })
14767
- },
14768
- WorkerReady: {
14769
- request: exports_external.strictObject({
14770
- tree: nonEmptyString,
14771
- issue: nonEmptyString,
14772
- role: legionRole,
14773
- sessionId: nonEmptyString,
14774
- generation: exports_external.number().int().nonnegative(),
14775
- secret: nonEmptyString
14776
- }),
14777
- response: exports_external.object({})
14778
- },
14779
- PhaseComplete: {
14780
- request: exports_external.strictObject({
14781
- grantId: nonEmptyString,
14782
- summary: nonEmptyString
14783
- }),
14784
- response: exports_external.object({})
14785
- },
14786
- SpawnWorker: {
14787
- request: architectCapability.extend({
14788
- issue: nonEmptyString,
14789
- role: legionRole,
14790
- task: nonEmptyString,
14791
- requestId: exports_external.uuid()
14792
- }),
14793
- response: exports_external.object({
14794
- status: exports_external.enum(["spawned", "resumed", "queued"]),
14795
- roleToken: nonEmptyString
14796
- })
14797
- },
14798
- WorkerSession: {
14799
- request: exports_external.strictObject({
14800
- sessionId: nonEmptyString,
14801
- recoveryToken: nonEmptyString
14802
- }),
14803
- response: exports_external.object({
14804
- tree: nonEmptyString,
14805
- issue: nonEmptyString,
14806
- role: legionRole,
14807
- secret: nonEmptyString
14808
- })
14809
- },
14810
- IssueStatus: {
14811
- request: controllerIssue.extend({
14812
- status: exports_external.enum(ISSUE_STATUSES),
14813
- tree: nonEmptyString.optional(),
14814
- sessionId: nonEmptyString.optional()
14815
- }),
14816
- response: exports_external.object({})
14817
- },
14818
- GatesRegister: {
14819
- request: architectCapability.extend({
14820
- issue: nonEmptyString,
14821
- artifactId: exports_external.uuid(),
14822
- version: exports_external.number().int().positive()
14823
- }),
14824
- response: exports_external.object({})
14825
- },
14826
- Grant: {
14827
- request: exports_external.union([
14828
- exports_external.strictObject({
14829
- sessionId: nonEmptyString,
14830
- secret: nonEmptyString,
14831
- tree: nonEmptyString,
14832
- issue: nonEmptyString
14833
- }),
14834
- exports_external.strictObject({ sessionId: nonEmptyString, secret: nonEmptyString })
14835
- ]),
14836
- response: exports_external.object({ grantId: nonEmptyString, expiresAt: nonEmptyString })
14837
- },
14838
- GitHubToken: {
14839
- request: exports_external.strictObject({ grantId: nonEmptyString }),
14840
- response: exports_external.object({
14841
- token: nonEmptyString,
14842
- appLogin: exports_external.string().endsWith("[bot]"),
14843
- legionAppLogins: exports_external.object({ implement: appLogin, review: appLogin }).optional()
14844
- })
14845
- },
14846
- GitCredential: {
14847
- request: exports_external.strictObject({ grantId: nonEmptyString })
14848
- }
14849
- };
14850
14411
  // ../contracts/src/repo.ts
14851
14412
  function canonicalRepo(owner, repo) {
14852
14413
  return `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase().replace(/\.git$/, "")}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "4.1.1",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -125,12 +125,10 @@ committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legio
125
125
  4. `test.json`
126
126
  5. `review.json`
127
127
 
128
- Read only files that precede the assigned phase. Every handoff is validated when it is read:
129
- `validatePhaseHandoff` (`packages/contracts/src/handoff-schema.ts`) checks the
130
- file, and the ledger (`packages/daemon/src/handoff/ledger.ts`) treats a file that
131
- fails validation as missing.
132
- Undeclared fields pass validation untouched and reach the next worker; a declared field of the
133
- wrong type fails the whole file, so the `legion` tool's `handoff_read` returns null for that phase.
128
+ Read only files that precede the assigned phase. Each was held to its phase's rules when it was
129
+ written (`handoff_write`, in the completion gate below): fields the phase does not declare passed
130
+ untouched and reach the next worker. The `legion` tool's `handoff_read` returns each file as it
131
+ stands in the workspace.
134
132
  Write the phase-specific fields the next phase and the architect need, consistent with what
135
133
  predecessor phases already wrote. The durable copy lives in
136
134
  `$LEGION_WORKSPACE/.legion/<phase>.json`. If a committed handoff conflicts with memory or a prior
@@ -13,7 +13,8 @@ reviewer on every re-review, the merger before READY. Every path it cites is in
13
13
  `Accepted:` — the opener's own follow-up included — leaves the thread open, because resolution
14
14
  considers only the newest comment). The review App can reply on a thread but cannot resolve it:
15
15
  GitHub grants resolving a review thread to the pull request's author, and the implementer opens
16
- every Legion pull request (`packages/daemon/src/daemon/AGENTS.md`, GitHub Apps).
16
+ every Legion pull request (`docs/site/src/content/docs/legion/running-legion.md`, "The two
17
+ GitHub Apps").
17
18
  When `LEGION_GRANT_FILE` or `LEGION_GRANT` is set, use `legion threads resolve --pr <number> --repo <owner>/<repo>`.
18
19
  When neither is set, add `--gh` to that command, which applies the fallback's rule below through
19
20
  your own `gh`; where no `legion` command is installed, use `gh api graphql` with the session's