code-mower 0.5.0b5__py3-none-any.whl

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.
Files changed (185) hide show
  1. code_mower/__init__.py +3 -0
  2. code_mower/adapters/__init__.py +39 -0
  3. code_mower/adapters/_base.py +148 -0
  4. code_mower/adapters/cursor_bugbot.py +97 -0
  5. code_mower/adapters/gitar.py +111 -0
  6. code_mower/adapters/greptile.py +191 -0
  7. code_mower/adapters/qodo.py +140 -0
  8. code_mower/antigravity_cli_audit_pr.py +241 -0
  9. code_mower/audit_handoff_log.py +345 -0
  10. code_mower/audit_labeler_lib.py +311 -0
  11. code_mower/audit_progress.py +203 -0
  12. code_mower/blind_review_artifacts.py +562 -0
  13. code_mower/blind_review_coordinator.py +276 -0
  14. code_mower/bootstrap.py +524 -0
  15. code_mower/builder_experiment.py +539 -0
  16. code_mower/calibration/__init__.py +181 -0
  17. code_mower/calibration/arms.py +267 -0
  18. code_mower/calibration/auto_discovery.py +348 -0
  19. code_mower/calibration/commands.py +192 -0
  20. code_mower/calibration/context_inputs.py +199 -0
  21. code_mower/calibration/corpus.py +91 -0
  22. code_mower/calibration/evidence.py +20 -0
  23. code_mower/calibration/evidence_report.py +360 -0
  24. code_mower/calibration/identity.py +27 -0
  25. code_mower/calibration/metrics.py +18 -0
  26. code_mower/calibration/overlap.py +103 -0
  27. code_mower/calibration/planning.py +309 -0
  28. code_mower/calibration/policy.py +207 -0
  29. code_mower/calibration/results.py +315 -0
  30. code_mower/calibration/run_results.py +147 -0
  31. code_mower/calibration/run_status.py +64 -0
  32. code_mower/calibration/runner.py +360 -0
  33. code_mower/calibration/truth.py +188 -0
  34. code_mower/calibration/value_report.py +142 -0
  35. code_mower/checks.py +402 -0
  36. code_mower/claude_audit_pr.py +1126 -0
  37. code_mower/claude_cli_bounce.py +303 -0
  38. code_mower/claude_cli_environment.py +73 -0
  39. code_mower/clear_stale.py +374 -0
  40. code_mower/cli.py +537 -0
  41. code_mower/cloud.py +674 -0
  42. code_mower/cloud_client/__init__.py +173 -0
  43. code_mower/cloud_client/bundle.py +155 -0
  44. code_mower/cloud_client/doctor.py +206 -0
  45. code_mower/cloud_client/dogfood.py +78 -0
  46. code_mower/cloud_client/endpoints.py +114 -0
  47. code_mower/cloud_client/errors.py +7 -0
  48. code_mower/cloud_client/events.py +278 -0
  49. code_mower/cloud_client/export.py +272 -0
  50. code_mower/cloud_client/git_metadata.py +46 -0
  51. code_mower/cloud_client/manifest.py +39 -0
  52. code_mower/cloud_client/operations.py +448 -0
  53. code_mower/cloud_client/reports.py +45 -0
  54. code_mower/cloud_client/setup.py +205 -0
  55. code_mower/cloud_client/upload.py +97 -0
  56. code_mower/code_mower_calibration.py +598 -0
  57. code_mower/code_mower_context_packs.py +591 -0
  58. code_mower/code_mower_merge.py +227 -0
  59. code_mower/code_mower_telemetry.py +561 -0
  60. code_mower/coderabbit_cli_audit_pr.py +526 -0
  61. code_mower/codex_audit_env_preflight.py +220 -0
  62. code_mower/codex_audit_pr.py +1738 -0
  63. code_mower/codex_audit_schema_smoke.py +160 -0
  64. code_mower/codex_audit_verdict.schema.json +44 -0
  65. code_mower/config.py +655 -0
  66. code_mower/doctor.py +161 -0
  67. code_mower/doctor_checks/__init__.py +104 -0
  68. code_mower/doctor_checks/cloud.py +129 -0
  69. code_mower/doctor_checks/common.py +215 -0
  70. code_mower/doctor_checks/github.py +128 -0
  71. code_mower/doctor_checks/github_actions.py +11 -0
  72. code_mower/doctor_checks/github_actions_cost.py +99 -0
  73. code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
  74. code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
  75. code_mower/doctor_checks/github_actions_failure_models.py +47 -0
  76. code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
  77. code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
  78. code_mower/doctor_checks/github_actions_failures.py +103 -0
  79. code_mower/doctor_checks/github_actions_permissions.py +55 -0
  80. code_mower/doctor_checks/github_api.py +79 -0
  81. code_mower/doctor_checks/github_branch.py +56 -0
  82. code_mower/doctor_checks/github_config.py +25 -0
  83. code_mower/doctor_checks/github_provider.py +61 -0
  84. code_mower/doctor_checks/github_repo.py +120 -0
  85. code_mower/doctor_checks/groups.py +36 -0
  86. code_mower/doctor_checks/models.py +96 -0
  87. code_mower/doctor_checks/output.py +86 -0
  88. code_mower/doctor_checks/presets.py +64 -0
  89. code_mower/doctor_checks/privacy.py +20 -0
  90. code_mower/doctor_checks/provider_api_model.py +138 -0
  91. code_mower/doctor_checks/provider_api_model_openai.py +29 -0
  92. code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
  93. code_mower/doctor_checks/provider_env.py +113 -0
  94. code_mower/doctor_checks/provider_env_required.py +56 -0
  95. code_mower/doctor_checks/provider_env_tokens.py +100 -0
  96. code_mower/doctor_checks/provider_local_cli.py +162 -0
  97. code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
  98. code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
  99. code_mower/doctor_checks/provider_probe.py +20 -0
  100. code_mower/doctor_checks/provider_probe_auth.py +52 -0
  101. code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
  102. code_mower/doctor_checks/provider_probe_json.py +45 -0
  103. code_mower/doctor_checks/provider_probe_remediation.py +39 -0
  104. code_mower/doctor_checks/providers.py +159 -0
  105. code_mower/doctor_checks/registry.py +69 -0
  106. code_mower/doctor_checks/runner.py +188 -0
  107. code_mower/doctor_checks/runtime.py +89 -0
  108. code_mower/doctor_checks/runtime_github_auth.py +148 -0
  109. code_mower/gemini_cli_audit_pr.py +897 -0
  110. code_mower/hermes_cli_audit_pr.py +436 -0
  111. code_mower/init.py +888 -0
  112. code_mower/lane_configs/__init__.py +37 -0
  113. code_mower/lane_configs/aider.py +32 -0
  114. code_mower/lane_configs/antigravity_cli.py +35 -0
  115. code_mower/lane_configs/claude.py +35 -0
  116. code_mower/lane_configs/codex.py +32 -0
  117. code_mower/lane_configs/devin.py +33 -0
  118. code_mower/lane_configs/gemini_cli.py +35 -0
  119. code_mower/lane_configs/hermes_cli.py +35 -0
  120. code_mower/lane_configs/local_llm.py +31 -0
  121. code_mower/local_llm_audit_pr.py +1364 -0
  122. code_mower/local_llm_bakeoff.py +458 -0
  123. code_mower/local_llm_calibration.py +441 -0
  124. code_mower/local_llm_profiles.py +66 -0
  125. code_mower/migration.py +508 -0
  126. code_mower/migration_install.py +292 -0
  127. code_mower/migration_mirror.py +392 -0
  128. code_mower/migration_readiness.py +237 -0
  129. code_mower/migration_rehearsal.py +718 -0
  130. code_mower/next_steps.py +441 -0
  131. code_mower/package.py +673 -0
  132. code_mower/package_content.py +444 -0
  133. code_mower/package_manifest.py +452 -0
  134. code_mower/package_paths.py +53 -0
  135. code_mower/package_rendering.py +90 -0
  136. code_mower/package_static.py +585 -0
  137. code_mower/prompts.py +267 -0
  138. code_mower/provider_registry.py +469 -0
  139. code_mower/provider_runners/__init__.py +60 -0
  140. code_mower/provider_runners/comments.py +31 -0
  141. code_mower/provider_runners/git.py +46 -0
  142. code_mower/provider_runners/github_auth.py +61 -0
  143. code_mower/provider_runners/github_pr.py +120 -0
  144. code_mower/provider_runners/process.py +58 -0
  145. code_mower/provider_runners/repo_paths.py +23 -0
  146. code_mower/provider_runners/text_schema.py +41 -0
  147. code_mower/provider_runners/verdict_artifacts.py +103 -0
  148. code_mower/provider_runners/workspace.py +57 -0
  149. code_mower/release_readiness.py +549 -0
  150. code_mower/reviewer_metrics.py +389 -0
  151. code_mower/saas_reviewer_labeler.py +809 -0
  152. code_mower/secrets.py +89 -0
  153. code_mower/templates/builder-experiment.example.json +55 -0
  154. code_mower/templates/calibration-corpus.example.json +129 -0
  155. code_mower/templates/calibration-corpus.json +129 -0
  156. code_mower/templates/code-mower.example.yml +423 -0
  157. code_mower/templates/context-packs.example.json +150 -0
  158. code_mower/templates/lane_prompts/base-audit.md +22 -0
  159. code_mower/templates/lane_prompts/calibration-policy.md +21 -0
  160. code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
  161. code_mower/templates/lane_prompts/docs-design.md +12 -0
  162. code_mower/templates/lane_prompts/generic-programming.md +21 -0
  163. code_mower/templates/lane_prompts/operability.md +22 -0
  164. code_mower/templates/lane_prompts/package-runtime.md +12 -0
  165. code_mower/templates/lane_prompts/security-threat-model.md +22 -0
  166. code_mower/templates/product-support/code_mower +216 -0
  167. code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
  168. code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
  169. code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
  170. code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
  171. code_mower/templates/product-support/safe_gh_comment.py +96 -0
  172. code_mower/templates/providers.yml +454 -0
  173. code_mower/templates/reviewer-spend.example.json +28 -0
  174. code_mower/templates/reviewer-value-report.example.md +20 -0
  175. code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
  176. code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
  177. code_mower/trailer_comment_labeler.py +207 -0
  178. code_mower/versioning.py +32 -0
  179. code_mower-0.5.0b5.dist-info/METADATA +302 -0
  180. code_mower-0.5.0b5.dist-info/RECORD +185 -0
  181. code_mower-0.5.0b5.dist-info/WHEEL +5 -0
  182. code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
  183. code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
  184. code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
  185. code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env python3
2
+ """Clear stale terminal audit labels after a pull request head changes."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import sys
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any, Optional, Sequence
13
+
14
+ if __package__ and __package__.startswith("code_mower"):
15
+ from .audit_labeler_lib import (
16
+ GitHubToken,
17
+ IssueCommentPaginationLimitExceeded,
18
+ LabelDecision,
19
+ apply_label_decision,
20
+ fetch_issue_comments,
21
+ fetch_issue_labels,
22
+ fetch_pull_request,
23
+ extract_reviewed_sha,
24
+ github_request_with_fallback,
25
+ load_json,
26
+ sha_matches_reviewed_head,
27
+ )
28
+ from .lane_configs import load_lane_config
29
+ from .trailer_comment_labeler import classify_audit_comment
30
+ else:
31
+ try:
32
+ from tools.audit_labeler_lib import (
33
+ GitHubToken,
34
+ IssueCommentPaginationLimitExceeded,
35
+ LabelDecision,
36
+ apply_label_decision,
37
+ fetch_issue_comments,
38
+ fetch_issue_labels,
39
+ fetch_pull_request,
40
+ extract_reviewed_sha,
41
+ github_request_with_fallback,
42
+ load_json,
43
+ sha_matches_reviewed_head,
44
+ )
45
+ from tools.lane_configs import load_lane_config
46
+ from tools.trailer_comment_labeler import classify_audit_comment
47
+ except ImportError: # pragma: no cover - direct `python tools/foo.py` execution
48
+ from audit_labeler_lib import (
49
+ GitHubToken,
50
+ IssueCommentPaginationLimitExceeded,
51
+ LabelDecision,
52
+ apply_label_decision,
53
+ fetch_issue_comments,
54
+ fetch_issue_labels,
55
+ fetch_pull_request,
56
+ extract_reviewed_sha,
57
+ github_request_with_fallback,
58
+ load_json,
59
+ sha_matches_reviewed_head,
60
+ )
61
+ from lane_configs import load_lane_config
62
+ from trailer_comment_labeler import classify_audit_comment
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class TrustedTerminalComment:
67
+ status: str
68
+ reviewed_sha: Optional[str]
69
+ author: str
70
+ created_at: str
71
+
72
+
73
+ def _status_label(config: Any, status: str) -> str:
74
+ return config.done_label if status == "done" else config.blocked_label
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class StaleClearResult:
79
+ decision: Optional[LabelDecision]
80
+ reason: str
81
+ requeue_added: bool = False
82
+ dispatch_requested: bool = False
83
+
84
+
85
+ def _comment_created_at(comment: dict[str, Any]) -> str:
86
+ return str(comment.get("created_at") or "")
87
+
88
+
89
+ def _extract_reviewed_sha(body: str) -> Optional[str]:
90
+ # Keep this local wrapper so no-SHA fallback comments never preserve terminal labels.
91
+ return extract_reviewed_sha(body)
92
+
93
+
94
+ def latest_trusted_terminal_comment(
95
+ comments: Sequence[dict[str, Any]],
96
+ *,
97
+ config: Any,
98
+ ) -> Optional[TrustedTerminalComment]:
99
+ trusted_authors = config.comment_authors()
100
+ ordered = sorted(comments, key=_comment_created_at, reverse=True)
101
+ for comment in ordered:
102
+ author = str(((comment.get("user") or {}).get("login") or ""))
103
+ if author.lower() not in trusted_authors:
104
+ continue
105
+ body = str(comment.get("body") or "")
106
+ status = classify_audit_comment(body, config)
107
+ if status not in {"done", "blocked"}:
108
+ continue
109
+ reviewed_sha = _extract_reviewed_sha(body)
110
+ return TrustedTerminalComment(
111
+ status=status,
112
+ reviewed_sha=reviewed_sha,
113
+ author=author,
114
+ created_at=_comment_created_at(comment),
115
+ )
116
+ return None
117
+
118
+
119
+ def resolve_stale_clear_decision(
120
+ *,
121
+ issue_number: int,
122
+ current_head_sha: str,
123
+ labels: Sequence[str],
124
+ comments: Sequence[dict[str, Any]],
125
+ config: Any,
126
+ ) -> StaleClearResult:
127
+ label_set = set(labels)
128
+ terminal_labels = {config.done_label, config.blocked_label}
129
+ present_terminal = terminal_labels.intersection(label_set)
130
+ if not present_terminal:
131
+ return StaleClearResult(None, "no terminal audit labels present")
132
+
133
+ current_comment = latest_trusted_terminal_comment(comments, config=config)
134
+ if (
135
+ current_comment
136
+ and current_comment.reviewed_sha
137
+ and sha_matches_reviewed_head(
138
+ current_comment.reviewed_sha,
139
+ current_head_sha,
140
+ )
141
+ ):
142
+ expected_label = _status_label(config, current_comment.status)
143
+ wrong_terminal_labels = tuple(sorted(present_terminal - {expected_label}))
144
+ if expected_label in label_set and not wrong_terminal_labels:
145
+ return StaleClearResult(
146
+ None,
147
+ (
148
+ f"terminal label is current: {current_comment.status} by "
149
+ f"{current_comment.author} for {current_comment.reviewed_sha}"
150
+ ),
151
+ )
152
+ return StaleClearResult(
153
+ LabelDecision(
154
+ issue_number=issue_number,
155
+ add_label=expected_label,
156
+ remove_labels=(config.needs_label, *wrong_terminal_labels),
157
+ reviewed_sha=current_comment.reviewed_sha,
158
+ reason=(
159
+ f"repaired terminal label mismatch for current head "
160
+ f"{current_head_sha}: expected {expected_label}"
161
+ ),
162
+ ),
163
+ (
164
+ "terminal audit label mismatch for current trusted "
165
+ f"{current_comment.status} result"
166
+ ),
167
+ )
168
+
169
+ decision = LabelDecision(
170
+ issue_number=issue_number,
171
+ add_label=config.needs_label,
172
+ remove_labels=(config.done_label, config.blocked_label),
173
+ reviewed_sha=current_comment.reviewed_sha if current_comment else None,
174
+ reason=(
175
+ f"cleared stale terminal labels after head moved to {current_head_sha}"
176
+ ),
177
+ )
178
+ return StaleClearResult(
179
+ decision,
180
+ "terminal audit label is stale or lacks a current trusted Head SHA",
181
+ requeue_added=config.needs_label not in label_set,
182
+ )
183
+
184
+
185
+ def _event_pr_context(event: dict[str, Any]) -> tuple[int, Optional[str], list[str]]:
186
+ pull_request = event.get("pull_request") or {}
187
+ issue_number = int(pull_request.get("number") or event.get("number") or 0)
188
+ head_sha = ((pull_request.get("head") or {}).get("sha") or None)
189
+ labels = [
190
+ str(label.get("name") or "")
191
+ for label in pull_request.get("labels") or []
192
+ if str(label.get("name") or "")
193
+ ]
194
+ return issue_number, head_sha, labels
195
+
196
+
197
+ def _render_dispatch_input(template: str, *, issue_number: int, head_sha: str, lane: str) -> str:
198
+ return (
199
+ template.replace("{pr}", str(issue_number))
200
+ .replace("{pr_number}", str(issue_number))
201
+ .replace("{head}", head_sha)
202
+ .replace("{head_sha}", head_sha)
203
+ .replace("{lane}", lane)
204
+ )
205
+
206
+
207
+ def _parse_dispatch_inputs(
208
+ entries: Sequence[str],
209
+ *,
210
+ issue_number: int,
211
+ head_sha: str,
212
+ lane: str,
213
+ ) -> dict[str, str]:
214
+ inputs: dict[str, str] = {}
215
+ for entry in entries:
216
+ if "=" not in entry:
217
+ raise ValueError(f"dispatch input must be name=value: {entry}")
218
+ name, value = entry.split("=", 1)
219
+ name = name.strip()
220
+ if not name:
221
+ raise ValueError(f"dispatch input has an empty name: {entry}")
222
+ inputs[name] = _render_dispatch_input(
223
+ value,
224
+ issue_number=issue_number,
225
+ head_sha=head_sha,
226
+ lane=lane,
227
+ )
228
+ return inputs
229
+
230
+
231
+ def dispatch_workflow(
232
+ repo: str,
233
+ workflow: str,
234
+ *,
235
+ ref: str,
236
+ inputs: dict[str, str],
237
+ tokens: Sequence[GitHubToken],
238
+ ) -> None:
239
+ github_request_with_fallback(
240
+ "POST",
241
+ f"/repos/{repo}/actions/workflows/{workflow}/dispatches",
242
+ tokens=tokens,
243
+ body={"ref": ref, "inputs": inputs},
244
+ )
245
+
246
+
247
+ def _default_dispatch_ref(event: dict[str, Any]) -> str:
248
+ pull_request = event.get("pull_request") or {}
249
+ base = pull_request.get("base") or {}
250
+ repo = event.get("repository") or {}
251
+ return str(base.get("ref") or repo.get("default_branch") or os.environ.get("GITHUB_REF_NAME") or "main")
252
+
253
+
254
+ def main(argv: Optional[Sequence[str]] = None) -> int:
255
+ parser = argparse.ArgumentParser(description=__doc__)
256
+ parser.add_argument("--lane", required=True, help="Trailer audit lane from lane_configs/.")
257
+ parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", ""))
258
+ parser.add_argument("--pr", type=int, default=0)
259
+ parser.add_argument("--head-sha", default="")
260
+ parser.add_argument("--event-path", default=os.environ.get("GITHUB_EVENT_PATH", ""))
261
+ parser.add_argument("--page-cap", type=int, default=5)
262
+ parser.add_argument("--dry-run", action="store_true")
263
+ parser.add_argument("--json", action="store_true")
264
+ parser.add_argument(
265
+ "--dispatch-workflow",
266
+ default="",
267
+ help="optional workflow filename/id to workflow_dispatch after a new stale requeue",
268
+ )
269
+ parser.add_argument("--dispatch-ref", default="")
270
+ parser.add_argument(
271
+ "--dispatch-input",
272
+ action="append",
273
+ default=[],
274
+ help="workflow_dispatch input as name=value; supports {pr}, {head}, and {lane}",
275
+ )
276
+ args = parser.parse_args(argv)
277
+
278
+ config = load_lane_config(args.lane)
279
+ tokens = config.github_tokens_from_env()
280
+ if not args.dry_run and not tokens:
281
+ token_names = " or ".join(config.token_env_vars)
282
+ print(f"error: {token_names} is required", file=sys.stderr)
283
+ return 1
284
+
285
+ event = load_json(Path(args.event_path)) if args.event_path else {}
286
+ repo = args.repo or str((event.get("repository") or {}).get("full_name") or "")
287
+ if not repo:
288
+ print("error: --repo or GITHUB_REPOSITORY is required", file=sys.stderr)
289
+ return 1
290
+
291
+ event_issue_number, event_head_sha, event_labels = _event_pr_context(event)
292
+ issue_number = args.pr or event_issue_number
293
+ current_head_sha = args.head_sha or event_head_sha or ""
294
+ labels = event_labels
295
+ if not args.dry_run:
296
+ if not issue_number:
297
+ print("error: --pr or pull_request event is required", file=sys.stderr)
298
+ return 1
299
+ pull_request = fetch_pull_request(repo, issue_number, tokens=tokens)
300
+ current_head_sha = str((pull_request.get("head") or {}).get("sha") or current_head_sha)
301
+ labels = fetch_issue_labels(repo, issue_number, tokens=tokens)
302
+ try:
303
+ comments = fetch_issue_comments(
304
+ repo,
305
+ issue_number,
306
+ tokens=tokens,
307
+ page_cap=args.page_cap,
308
+ )
309
+ except IssueCommentPaginationLimitExceeded as exc:
310
+ # Conservative for merge-authority lanes: if we cannot inspect enough
311
+ # comments to prove a terminal label is current, clear and requeue
312
+ # rather than leaving stale approval authoritative.
313
+ print(f"warning: {exc}; using conservative stale requeue", file=sys.stderr)
314
+ comments = []
315
+ else:
316
+ comments = list((event.get("comments") or []))
317
+
318
+ if not issue_number or not current_head_sha:
319
+ print("error: PR number and head SHA are required", file=sys.stderr)
320
+ return 1
321
+
322
+ result = resolve_stale_clear_decision(
323
+ issue_number=issue_number,
324
+ current_head_sha=current_head_sha,
325
+ labels=labels,
326
+ comments=comments,
327
+ config=config,
328
+ )
329
+
330
+ dispatch_requested = False
331
+ if result.decision is not None and not args.dry_run:
332
+ apply_label_decision(repo, result.decision, tokens=tokens)
333
+ if args.dispatch_workflow and result.requeue_added:
334
+ dispatch_ref = args.dispatch_ref or _default_dispatch_ref(event)
335
+ dispatch_inputs = _parse_dispatch_inputs(
336
+ args.dispatch_input,
337
+ issue_number=issue_number,
338
+ head_sha=current_head_sha,
339
+ lane=config.name,
340
+ )
341
+ dispatch_workflow(
342
+ repo,
343
+ args.dispatch_workflow,
344
+ ref=dispatch_ref,
345
+ inputs=dispatch_inputs,
346
+ tokens=tokens,
347
+ )
348
+ dispatch_requested = True
349
+
350
+ output = {
351
+ "lane": config.name,
352
+ "repo": repo,
353
+ "pr": issue_number,
354
+ "head_sha": current_head_sha,
355
+ "reason": result.reason,
356
+ "decision": result.decision.__dict__ if result.decision else None,
357
+ "requeue_added": result.requeue_added,
358
+ "dispatch_requested": dispatch_requested,
359
+ }
360
+ if args.json or args.dry_run:
361
+ print(json.dumps(output, indent=2, sort_keys=True))
362
+ elif result.decision:
363
+ print(
364
+ f"applied: add {result.decision.add_label}; remove "
365
+ f"{', '.join(result.decision.remove_labels)} on {repo}#{issue_number} "
366
+ f"({result.reason})"
367
+ )
368
+ else:
369
+ print(f"skip: {result.reason}")
370
+ return 0
371
+
372
+
373
+ if __name__ == "__main__":
374
+ raise SystemExit(main())