claude-dev-env 2.0.2 → 2.2.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.
- package/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_rules_shared.py +126 -47
- package/hooks/blocking/config/CLAUDE.md +2 -0
- package/hooks/blocking/config/verified_commit_context_constants.py +21 -0
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +14 -0
- package/hooks/blocking/conftest.py +36 -30
- package/hooks/blocking/convergence_gate_blocker.py +76 -8
- package/hooks/blocking/plain_language_blocker.py +8 -0
- package/hooks/blocking/state_description_blocker.py +4 -1
- package/hooks/blocking/test_code_rules_shared.py +120 -20
- package/hooks/blocking/test_convergence_gate_blocker.py +146 -0
- package/hooks/blocking/test_plain_language_blocker.py +15 -0
- package/hooks/blocking/test_state_description_blocker.py +15 -0
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +18 -0
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +134 -0
- package/hooks/blocking/tests/test_verified_commit_gate.py +41 -0
- package/hooks/blocking/verified_commit_config_bootstrap.py +22 -10
- package/hooks/blocking/verified_commit_gate.py +113 -568
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +28 -0
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +1 -0
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +174 -0
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +53 -0
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +80 -0
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +170 -0
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +205 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +10 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +94 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +17 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +38 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +71 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +61 -0
- package/hooks/hooks_constants/CLAUDE.md +4 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +36 -0
- package/hooks/hooks_constants/harness_scratchpad_constants.py +10 -9
- package/hooks/hooks_constants/mypy_integration_constants.py +16 -0
- package/hooks/validators/mypy_integration.py +145 -24
- package/hooks/validators/python_antipattern_checks.py +16 -0
- package/hooks/validators/run_all_validators.py +9 -3
- package/hooks/validators/test_mypy_integration.py +154 -0
- package/hooks/validators/test_python_antipattern_checks.py +112 -1
- package/hooks/validators/test_run_all_validators_pretooluse.py +16 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/verified-commit-gate-skip.md +28 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -13
- package/skills/_shared/pr-loop/portable-driver.md +150 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +3 -0
- package/skills/_shared/pr-loop/scripts/build_converge_task_list.py +310 -0
- package/skills/_shared/pr-loop/scripts/portable_converge_driver.py +1637 -0
- package/skills/_shared/pr-loop/scripts/select_converge_pacer.py +215 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +3 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +56 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +47 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +181 -0
- package/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +140 -0
- package/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +1119 -0
- package/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +207 -0
- package/skills/auditing-claude-config/CLAUDE.md +2 -1
- package/skills/auditing-claude-config/SKILL.md +114 -176
- package/skills/auditing-claude-config/reference/probe-hook.md +74 -0
- package/skills/autoconverge/CLAUDE.md +6 -3
- package/skills/autoconverge/SKILL.md +421 -346
- package/skills/autoconverge/reference/CLAUDE.md +1 -0
- package/skills/autoconverge/reference/convergence.md +5 -5
- package/skills/autoconverge/reference/copilot-findings.md +51 -0
- package/skills/autoconverge/reference/multi-pr.md +33 -2
- package/skills/autoconverge/reference/stop-conditions.md +9 -6
- package/skills/autoconverge/test_portable_pacer_gate.py +54 -0
- package/skills/closeout/SKILL.md +7 -9
- package/skills/copilot-finding-triage/SKILL.md +21 -11
- package/skills/copilot-review/CLAUDE.md +3 -2
- package/skills/copilot-review/SKILL.md +119 -155
- package/skills/copilot-review/templates/subagent-prompt.md +49 -0
- package/skills/fresh-branch/CLAUDE.md +6 -9
- package/skills/fresh-branch/SKILL.md +84 -33
- package/skills/fresh-branch/scripts/create_fresh_branch.py +445 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/__init__.py +1 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +74 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +670 -0
- package/skills/pr-converge/CLAUDE.md +4 -3
- package/skills/pr-converge/SKILL.md +469 -422
- package/skills/pr-converge/reference/CLAUDE.md +1 -0
- package/skills/pr-converge/reference/multi-pr-orchestration.md +5 -1
- package/skills/pr-converge/reference/per-tick.md +51 -24
- package/skills/pr-converge/reference/progress-checklist.md +168 -0
- package/skills/pr-converge/test_portable_pacer_gate.py +67 -0
- package/skills/pr-converge/test_step5_host_branch.py +8 -6
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -3
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -115
- package/skills/privacy-hygiene/reference/sweep-procedure.md +62 -0
- package/skills/session-log/CLAUDE.md +2 -1
- package/skills/session-log/SKILL.md +4 -26
- package/skills/session-log/templates/frontmatter.md +40 -0
- package/skills/skill-builder/CLAUDE.md +8 -7
- package/skills/skill-builder/SKILL.md +26 -11
- package/skills/skill-builder/references/CLAUDE.md +3 -1
- package/skills/skill-builder/references/delegation-map.md +21 -12
- package/skills/skill-builder/references/description-field.md +9 -11
- package/skills/skill-builder/references/deterministic-elements.md +218 -0
- package/skills/skill-builder/references/self-audit-checklist.md +62 -45
- package/skills/skill-builder/references/skill-modularity.md +8 -9
- package/skills/skill-builder/templates/CLAUDE.md +2 -2
- package/skills/skill-builder/templates/gap-analysis.md +15 -0
- package/skills/skill-builder/workflows/CLAUDE.md +5 -5
- package/skills/skill-builder/workflows/improve-skill.md +18 -9
- package/skills/skill-builder/workflows/new-skill.md +23 -15
- package/skills/skill-builder/workflows/polish-skill.md +28 -21
|
@@ -0,0 +1,1637 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""portable_converge_driver phase machine (deterministic only).
|
|
3
|
+
|
|
4
|
+
The agent never invents phase transitions, wait delays, clean stamps, or
|
|
5
|
+
ready decisions from prose. It runs this CLI, reads JSON, and either:
|
|
6
|
+
|
|
7
|
+
* executes the named ``commands`` (shell helpers already scripted), or
|
|
8
|
+
* performs the single judgment step named in ``next`` (fix implementation,
|
|
9
|
+
worker spawn body) and reports outcomes back through this CLI.
|
|
10
|
+
|
|
11
|
+
::
|
|
12
|
+
|
|
13
|
+
portable_converge_driver.py open-run --skill autoconverge \\
|
|
14
|
+
--has-workflow 0 --has-schedule-wakeup 0 \\
|
|
15
|
+
--owner O --repo R --pr-number N --cwd DIR --state-dir DIR \\
|
|
16
|
+
[--session-model third-party] [--copilot-down 0|1] [--bugbot-down 0|1] \\
|
|
17
|
+
[--codex-down 0|1] [--codex-required 0|1]
|
|
18
|
+
|
|
19
|
+
portable_converge_driver.py after-code-review --state-file PATH \\
|
|
20
|
+
--returncode 0 --dirty-tree 0 --current-head SHA \\
|
|
21
|
+
--served-command ''|in_session|chain-command
|
|
22
|
+
|
|
23
|
+
portable_converge_driver.py after-bugteam --state-file PATH \\
|
|
24
|
+
--pushed 0|1 --converged 0|1 --current-head SHA
|
|
25
|
+
|
|
26
|
+
portable_converge_driver.py after-bugbot --state-file PATH \\
|
|
27
|
+
--classification clean|dirty|absent|down --current-head SHA \\
|
|
28
|
+
[--inline-lag 0|1]
|
|
29
|
+
|
|
30
|
+
portable_converge_driver.py after-codex --state-file PATH \\
|
|
31
|
+
--classification clean|dirty|down --current-head SHA
|
|
32
|
+
|
|
33
|
+
portable_converge_driver.py after-copilot-wait --state-file PATH \\
|
|
34
|
+
--review-surfaced 0|1 --classification clean|dirty|absent|down \\
|
|
35
|
+
--current-head SHA
|
|
36
|
+
|
|
37
|
+
portable_converge_driver.py after-ready-check --state-file PATH \\
|
|
38
|
+
--check-exit 0|1|2 --current-head SHA
|
|
39
|
+
|
|
40
|
+
portable_converge_driver.py show-state --state-file PATH
|
|
41
|
+
|
|
42
|
+
Successful code-review serve: returncode 0 only. ``served_command`` is
|
|
43
|
+
informational (empty / ``in_session`` / chain argv) and does not decide
|
|
44
|
+
success. Non-zero returncode is always a failed serve.
|
|
45
|
+
|
|
46
|
+
After Bugbot clean or down, the machine runs the Codex step (or the
|
|
47
|
+
codex-down / not-required waiver) before COPILOT_WAIT or check_ready.
|
|
48
|
+
|
|
49
|
+
When ``--codex-down`` is off, open-run and every codex decision point probe
|
|
50
|
+
weekly usage through ``codex_usage_probe`` so ``codex_required`` matches
|
|
51
|
+
``check_convergence`` (CLI ``--codex-required 1`` still forces the step on).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from __future__ import annotations
|
|
55
|
+
|
|
56
|
+
import argparse
|
|
57
|
+
import json
|
|
58
|
+
import os
|
|
59
|
+
import subprocess
|
|
60
|
+
import sys
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from typing import Mapping
|
|
63
|
+
|
|
64
|
+
_self_dir = Path(__file__).resolve().parent
|
|
65
|
+
if str(_self_dir) not in sys.path:
|
|
66
|
+
sys.path.insert(0, str(_self_dir))
|
|
67
|
+
|
|
68
|
+
from build_converge_task_list import build_converge_task_list # noqa: E402
|
|
69
|
+
from select_converge_pacer import ( # noqa: E402
|
|
70
|
+
parse_bool_flag,
|
|
71
|
+
select_converge_pacer,
|
|
72
|
+
)
|
|
73
|
+
from skills_pr_loop_constants.pacer_constants import ( # noqa: E402
|
|
74
|
+
CLI_HAS_SCHEDULE_WAKEUP_FLAG,
|
|
75
|
+
CLI_HAS_WORKFLOW_FLAG,
|
|
76
|
+
CLI_SKILL_FLAG,
|
|
77
|
+
PACER_PORTABLE,
|
|
78
|
+
)
|
|
79
|
+
from skills_pr_loop_constants.portable_driver_constants import ( # noqa: E402
|
|
80
|
+
ALL_CLASSIFICATIONS,
|
|
81
|
+
ALL_CODEX_CLASSIFICATIONS,
|
|
82
|
+
ALL_COPILOT_CLASSIFICATIONS,
|
|
83
|
+
ALL_GIT_REV_PARSE_HEAD_ARGV,
|
|
84
|
+
BLOCKER_BUGBOT_WAIT_CAP,
|
|
85
|
+
BLOCKER_COPILOT_WAIT_CAP,
|
|
86
|
+
BLOCKER_INLINE_LAG_CAP,
|
|
87
|
+
BLOCKER_NOT_PORTABLE,
|
|
88
|
+
BLOCKER_PREFLIGHT,
|
|
89
|
+
BLOCKER_REVIEW_FAILED,
|
|
90
|
+
BUGBOT_ABSENT_WAIT_HARD_CAP,
|
|
91
|
+
BUGBOT_INLINE_LAG_WAIT_SECONDS,
|
|
92
|
+
CHECK_CONVERGENCE_BUGBOT_DOWN_FLAG,
|
|
93
|
+
CHECK_CONVERGENCE_CODEX_CLEAN_AT_FLAG,
|
|
94
|
+
CHECK_CONVERGENCE_CODEX_DOWN_FLAG,
|
|
95
|
+
CHECK_CONVERGENCE_COPILOT_DOWN_FLAG,
|
|
96
|
+
CHECK_CONVERGENCE_OWNER_FLAG,
|
|
97
|
+
CHECK_CONVERGENCE_PR_NUMBER_FLAG,
|
|
98
|
+
CHECK_CONVERGENCE_RELATIVE_PATH,
|
|
99
|
+
CHECK_CONVERGENCE_REPO_FLAG,
|
|
100
|
+
CLASSIFICATION_ABSENT,
|
|
101
|
+
CLASSIFICATION_DIRTY,
|
|
102
|
+
CLASSIFICATION_DOWN,
|
|
103
|
+
CLI_BUGBOT_DOWN_FLAG,
|
|
104
|
+
CLI_CHECK_CONVERGENCE_EXIT_FLAG,
|
|
105
|
+
CLI_CLASSIFICATION_FLAG,
|
|
106
|
+
CLI_CODEX_DOWN_FLAG,
|
|
107
|
+
CLI_CODEX_REQUIRED_FLAG,
|
|
108
|
+
CLI_CONVERGED_FLAG,
|
|
109
|
+
CLI_COPILOT_DOWN_FLAG,
|
|
110
|
+
CLI_CURRENT_HEAD_FLAG,
|
|
111
|
+
CLI_CWD_FLAG,
|
|
112
|
+
CLI_DESCRIPTION,
|
|
113
|
+
CLI_DIRTY_TREE_FLAG,
|
|
114
|
+
CLI_INLINE_LAG_FLAG,
|
|
115
|
+
CLI_OWNER_FLAG,
|
|
116
|
+
CLI_PR_NUMBER_FLAG,
|
|
117
|
+
CLI_PUSHED_FLAG,
|
|
118
|
+
CLI_REPO_FLAG,
|
|
119
|
+
CLI_RETURNCODE_FLAG,
|
|
120
|
+
CLI_REVIEW_SURFACED_FLAG,
|
|
121
|
+
CLI_SERVED_COMMAND_FLAG,
|
|
122
|
+
CLI_SESSION_MODEL_FLAG,
|
|
123
|
+
CLI_STATE_DIR_FLAG,
|
|
124
|
+
CLI_STATE_FILE_FLAG,
|
|
125
|
+
CODEX_REVIEW_SCRIPTS_DIRNAME,
|
|
126
|
+
CODEX_REVIEW_SKILL_DIRNAME,
|
|
127
|
+
COMMAND_AFTER_BUGBOT,
|
|
128
|
+
COMMAND_AFTER_BUGTEAM,
|
|
129
|
+
COMMAND_AFTER_CODE_REVIEW,
|
|
130
|
+
COMMAND_AFTER_CODEX,
|
|
131
|
+
COMMAND_AFTER_COPILOT_WAIT,
|
|
132
|
+
COMMAND_AFTER_READY_CHECK,
|
|
133
|
+
COMMAND_OPEN_RUN,
|
|
134
|
+
COMMAND_SHOW_STATE,
|
|
135
|
+
COPILOT_WAIT_HARD_CAP,
|
|
136
|
+
DEFAULT_SESSION_MODEL,
|
|
137
|
+
DEFAULT_WAIT_SECONDS,
|
|
138
|
+
EXIT_CONTRACT_ERROR,
|
|
139
|
+
EXIT_SUCCESS,
|
|
140
|
+
EXIT_USAGE_ERROR,
|
|
141
|
+
INLINE_LAG_STREAK_CAP,
|
|
142
|
+
INVOKE_CODE_REVIEW_RELATIVE_PATH,
|
|
143
|
+
PORTABLE_SCRIPTS_TO_SKILLS_PARENT_HOPS,
|
|
144
|
+
NEXT_APPLY_FIXES,
|
|
145
|
+
NEXT_CHECK_READY,
|
|
146
|
+
NEXT_MARK_READY,
|
|
147
|
+
NEXT_POLL_WAIT,
|
|
148
|
+
NEXT_REQUEST_COPILOT,
|
|
149
|
+
NEXT_RUN_BUGBOT_GATE,
|
|
150
|
+
NEXT_RUN_BUGTEAM,
|
|
151
|
+
NEXT_RUN_CODE_REVIEW,
|
|
152
|
+
NEXT_RUN_CODEX,
|
|
153
|
+
NEXT_STOP_BLOCKED,
|
|
154
|
+
PHASE_BLOCKED,
|
|
155
|
+
PHASE_BUGBOT,
|
|
156
|
+
PHASE_BUGTEAM,
|
|
157
|
+
PHASE_CODE_REVIEW,
|
|
158
|
+
PHASE_COPILOT_WAIT,
|
|
159
|
+
PHASE_READY,
|
|
160
|
+
PREFLIGHT_WORKTREE_SCRIPT_NAME,
|
|
161
|
+
RESULT_KEY_BLOCKER,
|
|
162
|
+
RESULT_KEY_BUGBOT_DOWN,
|
|
163
|
+
RESULT_KEY_CODEX_DOWN,
|
|
164
|
+
RESULT_KEY_COMMANDS,
|
|
165
|
+
RESULT_KEY_COPILOT_DOWN,
|
|
166
|
+
RESULT_KEY_CURRENT_HEAD,
|
|
167
|
+
RESULT_KEY_ENTRY_SKILL,
|
|
168
|
+
RESULT_KEY_MESSAGE,
|
|
169
|
+
RESULT_KEY_NEXT,
|
|
170
|
+
RESULT_KEY_OWNER,
|
|
171
|
+
RESULT_KEY_PACER,
|
|
172
|
+
RESULT_KEY_PHASE,
|
|
173
|
+
RESULT_KEY_PR_NUMBER,
|
|
174
|
+
RESULT_KEY_REPO,
|
|
175
|
+
RESULT_KEY_STATE_FILE,
|
|
176
|
+
RESULT_KEY_STATUS,
|
|
177
|
+
RESULT_KEY_TICK_COUNT,
|
|
178
|
+
RESULT_KEY_WAIT_SECONDS,
|
|
179
|
+
STATE_FILENAME,
|
|
180
|
+
STATE_JSON_INDENT,
|
|
181
|
+
MARK_READY_GH_BINARY,
|
|
182
|
+
MARK_READY_PR_TOKEN,
|
|
183
|
+
MARK_READY_REPO_FLAG,
|
|
184
|
+
MARK_READY_SUBCOMMAND,
|
|
185
|
+
STATE_KEY_CODEX_CLEAN_AT,
|
|
186
|
+
STATE_KEY_CODEX_DOWN,
|
|
187
|
+
STATE_KEY_CODEX_REQUIRED,
|
|
188
|
+
STATE_KEY_CWD,
|
|
189
|
+
STATE_KEY_PENDING_NEXT,
|
|
190
|
+
STATE_KEY_PENDING_WAIT_SECONDS,
|
|
191
|
+
STATE_KEY_SESSION_MODEL,
|
|
192
|
+
STATE_STAGING_SUFFIX,
|
|
193
|
+
STATUS_ERROR,
|
|
194
|
+
STATUS_OK,
|
|
195
|
+
)
|
|
196
|
+
from skills_pr_loop_constants.preflight_constants import ( # noqa: E402
|
|
197
|
+
MODE_ARG_FLAG,
|
|
198
|
+
MODE_STRICT,
|
|
199
|
+
OWNER_ARG_FLAG,
|
|
200
|
+
REPO_ARG_FLAG,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
_skills_directory_for_codex = Path(__file__).resolve().parent
|
|
204
|
+
_remaining_codex_path_hops = PORTABLE_SCRIPTS_TO_SKILLS_PARENT_HOPS
|
|
205
|
+
while _remaining_codex_path_hops > 0:
|
|
206
|
+
_skills_directory_for_codex = _skills_directory_for_codex.parent
|
|
207
|
+
_remaining_codex_path_hops -= 1
|
|
208
|
+
_codex_scripts_directory = (
|
|
209
|
+
_skills_directory_for_codex
|
|
210
|
+
/ CODEX_REVIEW_SKILL_DIRNAME
|
|
211
|
+
/ CODEX_REVIEW_SCRIPTS_DIRNAME
|
|
212
|
+
)
|
|
213
|
+
_codex_scripts_text = str(_codex_scripts_directory)
|
|
214
|
+
if _codex_scripts_text not in sys.path:
|
|
215
|
+
sys.path.insert(0, _codex_scripts_text)
|
|
216
|
+
|
|
217
|
+
from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
|
|
218
|
+
USAGE_REPORT_KEY_PERCENT_LEFT,
|
|
219
|
+
)
|
|
220
|
+
from codex_usage_probe import ( # noqa: E402
|
|
221
|
+
is_codex_review_required,
|
|
222
|
+
probe_weekly_usage_via_subprocess,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
_EMPTY_COMMANDS: list[str] = []
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _home_path(*parts: str) -> Path:
|
|
229
|
+
return Path.home().joinpath(*parts)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _emit(all_payload: Mapping[str, object]) -> None:
|
|
233
|
+
print(json.dumps(dict(all_payload), sort_keys=True))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _error_payload(message: str, **extra: object) -> dict[str, object]:
|
|
237
|
+
all_payload: dict[str, object] = {
|
|
238
|
+
RESULT_KEY_STATUS: STATUS_ERROR,
|
|
239
|
+
RESULT_KEY_MESSAGE: message,
|
|
240
|
+
}
|
|
241
|
+
all_payload.update(extra)
|
|
242
|
+
return all_payload
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def load_state(state_file: Path) -> dict[str, object]:
|
|
246
|
+
"""Load loop state JSON from disk.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
state_file: Path to ``pr-converge-state.json``.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Parsed state mapping.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
ValueError: When the file is missing or not a JSON object.
|
|
256
|
+
"""
|
|
257
|
+
if not state_file.is_file():
|
|
258
|
+
raise ValueError(f"state file missing: {state_file}")
|
|
259
|
+
loaded = json.loads(state_file.read_text(encoding="utf-8"))
|
|
260
|
+
if not isinstance(loaded, dict):
|
|
261
|
+
raise ValueError(f"state file is not a JSON object: {state_file}")
|
|
262
|
+
return loaded
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def save_state(state_file: Path, all_state: Mapping[str, object]) -> None:
|
|
266
|
+
"""Write loop state JSON atomically.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
state_file: Destination path.
|
|
270
|
+
all_state: State mapping to persist.
|
|
271
|
+
"""
|
|
272
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
staging_path = state_file.with_suffix(state_file.suffix + STATE_STAGING_SUFFIX)
|
|
274
|
+
staging_path.write_text(
|
|
275
|
+
json.dumps(dict(all_state), indent=STATE_JSON_INDENT, sort_keys=True)
|
|
276
|
+
+ "\n",
|
|
277
|
+
encoding="utf-8",
|
|
278
|
+
)
|
|
279
|
+
os.replace(staging_path, state_file)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def build_initial_state(
|
|
283
|
+
*,
|
|
284
|
+
current_head: str,
|
|
285
|
+
entry_skill: str,
|
|
286
|
+
is_copilot_down: bool,
|
|
287
|
+
is_bugbot_down: bool,
|
|
288
|
+
owner: str,
|
|
289
|
+
repo: str,
|
|
290
|
+
pr_number: int,
|
|
291
|
+
is_codex_down: bool = False,
|
|
292
|
+
is_codex_required: bool = False,
|
|
293
|
+
session_model: str = DEFAULT_SESSION_MODEL,
|
|
294
|
+
cwd_path: str | None = None,
|
|
295
|
+
) -> dict[str, object]:
|
|
296
|
+
"""Build the initial portable loop state for a PR head.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
current_head: Current PR head SHA.
|
|
300
|
+
entry_skill: ``pr-converge`` or ``autoconverge``.
|
|
301
|
+
is_copilot_down: Copilot gate skipped for the whole run.
|
|
302
|
+
is_bugbot_down: Bugbot gate skipped for the whole run.
|
|
303
|
+
owner: Repository owner.
|
|
304
|
+
repo: Repository name.
|
|
305
|
+
pr_number: Pull request number.
|
|
306
|
+
is_codex_down: Codex gate skipped (disabled or opted out).
|
|
307
|
+
is_codex_required: Codex is required by usage/policy for this run.
|
|
308
|
+
session_model: Alias passed to invoke_code_review on re-entry.
|
|
309
|
+
cwd_path: PR worktree path for invoke_code_review --cwd.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
New state mapping at ``CODE_REVIEW``.
|
|
313
|
+
"""
|
|
314
|
+
return {
|
|
315
|
+
"phase": PHASE_CODE_REVIEW,
|
|
316
|
+
"tick_count": 0,
|
|
317
|
+
"bugbot_clean_at": None,
|
|
318
|
+
"code_review_clean_at": None,
|
|
319
|
+
"bugteam_clean_at": None,
|
|
320
|
+
"copilot_clean_at": None,
|
|
321
|
+
STATE_KEY_CODEX_CLEAN_AT: None,
|
|
322
|
+
"merge_state_status": None,
|
|
323
|
+
"current_head": current_head,
|
|
324
|
+
"copilot_wait_count": 0,
|
|
325
|
+
"bugbot_wait_count": 0,
|
|
326
|
+
"copilot_down": is_copilot_down,
|
|
327
|
+
"bugbot_down": is_bugbot_down,
|
|
328
|
+
STATE_KEY_CODEX_DOWN: is_codex_down,
|
|
329
|
+
STATE_KEY_CODEX_REQUIRED: is_codex_required,
|
|
330
|
+
"inline_lag_streak": 0,
|
|
331
|
+
"pacer": PACER_PORTABLE,
|
|
332
|
+
"entry_skill": entry_skill,
|
|
333
|
+
"owner": owner,
|
|
334
|
+
"repo": repo,
|
|
335
|
+
"pr_number": pr_number,
|
|
336
|
+
"blocker": None,
|
|
337
|
+
STATE_KEY_PENDING_NEXT: NEXT_RUN_CODE_REVIEW,
|
|
338
|
+
STATE_KEY_SESSION_MODEL: session_model,
|
|
339
|
+
STATE_KEY_CWD: cwd_path,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _reset_push_invalidated_markers(all_state: dict[str, object]) -> None:
|
|
344
|
+
all_state["bugbot_clean_at"] = None
|
|
345
|
+
all_state["code_review_clean_at"] = None
|
|
346
|
+
all_state["bugteam_clean_at"] = None
|
|
347
|
+
all_state["copilot_clean_at"] = None
|
|
348
|
+
all_state[STATE_KEY_CODEX_CLEAN_AT] = None
|
|
349
|
+
all_state["merge_state_status"] = None
|
|
350
|
+
all_state["copilot_wait_count"] = 0
|
|
351
|
+
all_state["bugbot_wait_count"] = 0
|
|
352
|
+
all_state["inline_lag_streak"] = 0
|
|
353
|
+
all_state["bugbot_down"] = False
|
|
354
|
+
all_state[STATE_KEY_CODEX_DOWN] = False
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _sync_task_list_from_down_flags(all_state: dict[str, object]) -> None:
|
|
358
|
+
all_task_list = build_converge_task_list(
|
|
359
|
+
is_bugbot_down=bool(all_state.get("bugbot_down")),
|
|
360
|
+
is_copilot_down=bool(all_state.get("copilot_down")),
|
|
361
|
+
is_codex_down=bool(all_state.get(STATE_KEY_CODEX_DOWN)),
|
|
362
|
+
is_codex_required=bool(all_state.get(STATE_KEY_CODEX_REQUIRED)),
|
|
363
|
+
)
|
|
364
|
+
all_state["tasks"] = all_task_list["tasks"]
|
|
365
|
+
all_state["final_task_id"] = all_task_list["final_task_id"]
|
|
366
|
+
all_state["runnable_review_ids"] = all_task_list["runnable_review_ids"]
|
|
367
|
+
all_state["done_when"] = all_task_list["done_when"]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def probe_weekly_usage_requires_codex() -> bool:
|
|
371
|
+
"""Return whether live weekly Codex usage requires a review stamp.
|
|
372
|
+
|
|
373
|
+
Matches ``check_convergence``: unknown percent and probe failures never
|
|
374
|
+
require a review. A known percent above the shared threshold does.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
True only when the weekly probe exits successfully with a percent
|
|
378
|
+
that ``is_codex_review_required`` accepts as required.
|
|
379
|
+
"""
|
|
380
|
+
try:
|
|
381
|
+
usage_report = probe_weekly_usage_via_subprocess()
|
|
382
|
+
except (
|
|
383
|
+
FileNotFoundError,
|
|
384
|
+
OSError,
|
|
385
|
+
subprocess.TimeoutExpired,
|
|
386
|
+
subprocess.SubprocessError,
|
|
387
|
+
json.JSONDecodeError,
|
|
388
|
+
ValueError,
|
|
389
|
+
TypeError,
|
|
390
|
+
KeyError,
|
|
391
|
+
):
|
|
392
|
+
return False
|
|
393
|
+
raw_percent = usage_report.get(USAGE_REPORT_KEY_PERCENT_LEFT)
|
|
394
|
+
if isinstance(raw_percent, bool):
|
|
395
|
+
return False
|
|
396
|
+
if isinstance(raw_percent, (int, float)):
|
|
397
|
+
return is_codex_review_required(float(raw_percent))
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def resolve_codex_required_flag(
|
|
402
|
+
*,
|
|
403
|
+
is_codex_down: bool,
|
|
404
|
+
is_codex_required: bool,
|
|
405
|
+
) -> bool:
|
|
406
|
+
"""Resolve whether Codex is a runnable review for this run.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
is_codex_down: Codex gate skipped (disabled or opted out).
|
|
410
|
+
is_codex_required: CLI force-on (``--codex-required 1``).
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
False when Codex is down; otherwise the CLI force-on flag or the
|
|
414
|
+
live weekly usage probe (same rule as ``check_convergence``).
|
|
415
|
+
"""
|
|
416
|
+
if is_codex_down:
|
|
417
|
+
return False
|
|
418
|
+
if is_codex_required:
|
|
419
|
+
return True
|
|
420
|
+
return probe_weekly_usage_requires_codex()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _ensure_codex_required_matches_usage(all_state: dict[str, object]) -> None:
|
|
424
|
+
"""Promote ``codex_required`` when live usage requires a stamp.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
all_state: Mutable loop state; may set ``codex_required`` and refresh
|
|
428
|
+
the task list when the probe requires Codex.
|
|
429
|
+
"""
|
|
430
|
+
if all_state.get(STATE_KEY_CODEX_DOWN):
|
|
431
|
+
return
|
|
432
|
+
if all_state.get(STATE_KEY_CODEX_REQUIRED):
|
|
433
|
+
return
|
|
434
|
+
if not probe_weekly_usage_requires_codex():
|
|
435
|
+
return
|
|
436
|
+
all_state[STATE_KEY_CODEX_REQUIRED] = True
|
|
437
|
+
_sync_task_list_from_down_flags(all_state)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _maybe_reset_on_head_change(
|
|
441
|
+
all_state: dict[str, object], current_head: str
|
|
442
|
+
) -> None:
|
|
443
|
+
prior_head = all_state.get("current_head")
|
|
444
|
+
if (
|
|
445
|
+
isinstance(prior_head, str)
|
|
446
|
+
and prior_head
|
|
447
|
+
and prior_head != current_head
|
|
448
|
+
):
|
|
449
|
+
_reset_push_invalidated_markers(all_state)
|
|
450
|
+
_sync_task_list_from_down_flags(all_state)
|
|
451
|
+
all_state["current_head"] = current_head
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _code_review_commands(
|
|
455
|
+
*,
|
|
456
|
+
cwd_path: Path,
|
|
457
|
+
session_model: str,
|
|
458
|
+
) -> list[str]:
|
|
459
|
+
invoke_path = _home_path(*INVOKE_CODE_REVIEW_RELATIVE_PATH.split("/"))
|
|
460
|
+
return [
|
|
461
|
+
"python",
|
|
462
|
+
str(invoke_path),
|
|
463
|
+
"--cwd",
|
|
464
|
+
str(cwd_path),
|
|
465
|
+
"--session-model",
|
|
466
|
+
session_model,
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _code_review_commands_from_state(
|
|
471
|
+
all_state: Mapping[str, object],
|
|
472
|
+
) -> list[str]:
|
|
473
|
+
stored_cwd = all_state.get(STATE_KEY_CWD)
|
|
474
|
+
stored_session_model = all_state.get(STATE_KEY_SESSION_MODEL)
|
|
475
|
+
if isinstance(stored_cwd, str) and stored_cwd:
|
|
476
|
+
cwd_path = Path(stored_cwd)
|
|
477
|
+
else:
|
|
478
|
+
cwd_path = Path.cwd()
|
|
479
|
+
if isinstance(stored_session_model, str) and stored_session_model:
|
|
480
|
+
session_model = stored_session_model
|
|
481
|
+
else:
|
|
482
|
+
session_model = DEFAULT_SESSION_MODEL
|
|
483
|
+
return _code_review_commands(
|
|
484
|
+
cwd_path=cwd_path, session_model=session_model
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _mark_ready_commands(all_state: Mapping[str, object]) -> list[str]:
|
|
489
|
+
owner = str(all_state["owner"])
|
|
490
|
+
repo = str(all_state["repo"])
|
|
491
|
+
return [
|
|
492
|
+
MARK_READY_GH_BINARY,
|
|
493
|
+
MARK_READY_PR_TOKEN,
|
|
494
|
+
MARK_READY_SUBCOMMAND,
|
|
495
|
+
str(all_state["pr_number"]),
|
|
496
|
+
MARK_READY_REPO_FLAG,
|
|
497
|
+
f"{owner}/{repo}",
|
|
498
|
+
]
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _check_convergence_commands(
|
|
502
|
+
*,
|
|
503
|
+
owner: str,
|
|
504
|
+
repo: str,
|
|
505
|
+
pr_number: int,
|
|
506
|
+
is_copilot_down: bool,
|
|
507
|
+
is_bugbot_down: bool,
|
|
508
|
+
is_codex_down: bool,
|
|
509
|
+
codex_clean_at: str | None,
|
|
510
|
+
) -> list[str]:
|
|
511
|
+
check_path = _home_path(*CHECK_CONVERGENCE_RELATIVE_PATH.split("/"))
|
|
512
|
+
all_arguments = [
|
|
513
|
+
"python",
|
|
514
|
+
str(check_path),
|
|
515
|
+
CHECK_CONVERGENCE_OWNER_FLAG,
|
|
516
|
+
owner,
|
|
517
|
+
CHECK_CONVERGENCE_REPO_FLAG,
|
|
518
|
+
repo,
|
|
519
|
+
CHECK_CONVERGENCE_PR_NUMBER_FLAG,
|
|
520
|
+
str(pr_number),
|
|
521
|
+
]
|
|
522
|
+
if is_copilot_down:
|
|
523
|
+
all_arguments.append(CHECK_CONVERGENCE_COPILOT_DOWN_FLAG)
|
|
524
|
+
if is_bugbot_down:
|
|
525
|
+
all_arguments.append(CHECK_CONVERGENCE_BUGBOT_DOWN_FLAG)
|
|
526
|
+
if is_codex_down:
|
|
527
|
+
all_arguments.append(CHECK_CONVERGENCE_CODEX_DOWN_FLAG)
|
|
528
|
+
if codex_clean_at:
|
|
529
|
+
all_arguments.extend(
|
|
530
|
+
[CHECK_CONVERGENCE_CODEX_CLEAN_AT_FLAG, codex_clean_at]
|
|
531
|
+
)
|
|
532
|
+
return all_arguments
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _state_pr_number(all_state: Mapping[str, object]) -> int:
|
|
536
|
+
return int(str(all_state["pr_number"]))
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _codex_clean_at_or_none(all_state: Mapping[str, object]) -> str | None:
|
|
540
|
+
maybe_stamp = all_state.get(STATE_KEY_CODEX_CLEAN_AT)
|
|
541
|
+
if isinstance(maybe_stamp, str) and maybe_stamp:
|
|
542
|
+
return maybe_stamp
|
|
543
|
+
return None
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _check_convergence_commands_from_state(
|
|
547
|
+
all_state: Mapping[str, object],
|
|
548
|
+
) -> list[str]:
|
|
549
|
+
return _check_convergence_commands(
|
|
550
|
+
owner=str(all_state["owner"]),
|
|
551
|
+
repo=str(all_state["repo"]),
|
|
552
|
+
pr_number=_state_pr_number(all_state),
|
|
553
|
+
is_copilot_down=bool(all_state.get("copilot_down")),
|
|
554
|
+
is_bugbot_down=bool(all_state.get("bugbot_down")),
|
|
555
|
+
is_codex_down=bool(all_state.get(STATE_KEY_CODEX_DOWN)),
|
|
556
|
+
codex_clean_at=_codex_clean_at_or_none(all_state),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _base_ok_payload(
|
|
561
|
+
all_state: Mapping[str, object], state_file: Path
|
|
562
|
+
) -> dict[str, object]:
|
|
563
|
+
return {
|
|
564
|
+
RESULT_KEY_STATUS: STATUS_OK,
|
|
565
|
+
RESULT_KEY_PHASE: all_state["phase"],
|
|
566
|
+
RESULT_KEY_STATE_FILE: str(state_file),
|
|
567
|
+
RESULT_KEY_CURRENT_HEAD: all_state.get("current_head"),
|
|
568
|
+
RESULT_KEY_TICK_COUNT: all_state.get("tick_count"),
|
|
569
|
+
RESULT_KEY_PACER: all_state.get("pacer", PACER_PORTABLE),
|
|
570
|
+
RESULT_KEY_ENTRY_SKILL: all_state.get("entry_skill"),
|
|
571
|
+
RESULT_KEY_OWNER: all_state.get("owner"),
|
|
572
|
+
RESULT_KEY_REPO: all_state.get("repo"),
|
|
573
|
+
RESULT_KEY_PR_NUMBER: all_state.get("pr_number"),
|
|
574
|
+
RESULT_KEY_COPILOT_DOWN: bool(all_state.get("copilot_down")),
|
|
575
|
+
RESULT_KEY_BUGBOT_DOWN: bool(all_state.get("bugbot_down")),
|
|
576
|
+
RESULT_KEY_CODEX_DOWN: bool(all_state.get(STATE_KEY_CODEX_DOWN)),
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _store_pending_wait_seconds(
|
|
581
|
+
all_state: dict[str, object],
|
|
582
|
+
*,
|
|
583
|
+
wait_seconds: int | None,
|
|
584
|
+
) -> None:
|
|
585
|
+
if wait_seconds is None:
|
|
586
|
+
all_state.pop(STATE_KEY_PENDING_WAIT_SECONDS, None)
|
|
587
|
+
return
|
|
588
|
+
all_state[STATE_KEY_PENDING_WAIT_SECONDS] = wait_seconds
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _resolve_poll_wait_seconds(all_state: Mapping[str, object]) -> int:
|
|
592
|
+
pending_wait_seconds = all_state.get(STATE_KEY_PENDING_WAIT_SECONDS)
|
|
593
|
+
if isinstance(pending_wait_seconds, int):
|
|
594
|
+
return pending_wait_seconds
|
|
595
|
+
return DEFAULT_WAIT_SECONDS
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _finish_ok(
|
|
599
|
+
all_state: dict[str, object],
|
|
600
|
+
state_file: Path,
|
|
601
|
+
*,
|
|
602
|
+
next_action: str,
|
|
603
|
+
all_commands: list[str],
|
|
604
|
+
wait_seconds: int | None,
|
|
605
|
+
blocker: str | None,
|
|
606
|
+
all_extra_fields: Mapping[str, object] | None,
|
|
607
|
+
) -> tuple[dict[str, object], int]:
|
|
608
|
+
all_state[STATE_KEY_PENDING_NEXT] = next_action
|
|
609
|
+
_store_pending_wait_seconds(all_state, wait_seconds=wait_seconds)
|
|
610
|
+
resolved_commands = all_commands
|
|
611
|
+
if next_action == NEXT_RUN_CODE_REVIEW:
|
|
612
|
+
resolved_commands = _code_review_commands_from_state(all_state)
|
|
613
|
+
save_state(state_file, all_state)
|
|
614
|
+
all_payload = _base_ok_payload(all_state, state_file)
|
|
615
|
+
all_payload[RESULT_KEY_NEXT] = next_action
|
|
616
|
+
all_payload[RESULT_KEY_COMMANDS] = resolved_commands
|
|
617
|
+
if wait_seconds is not None:
|
|
618
|
+
all_payload[RESULT_KEY_WAIT_SECONDS] = wait_seconds
|
|
619
|
+
if blocker is not None:
|
|
620
|
+
all_payload[RESULT_KEY_BLOCKER] = blocker
|
|
621
|
+
if all_extra_fields is not None:
|
|
622
|
+
all_payload.update(dict(all_extra_fields))
|
|
623
|
+
return all_payload, EXIT_SUCCESS
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _run_preflight(
|
|
627
|
+
*,
|
|
628
|
+
cwd_path: Path,
|
|
629
|
+
owner: str,
|
|
630
|
+
repo: str,
|
|
631
|
+
) -> tuple[dict[str, object] | None, int]:
|
|
632
|
+
preflight_script = _self_dir / PREFLIGHT_WORKTREE_SCRIPT_NAME
|
|
633
|
+
preflight_process = subprocess.run(
|
|
634
|
+
[
|
|
635
|
+
sys.executable,
|
|
636
|
+
str(preflight_script),
|
|
637
|
+
OWNER_ARG_FLAG,
|
|
638
|
+
owner,
|
|
639
|
+
REPO_ARG_FLAG,
|
|
640
|
+
repo,
|
|
641
|
+
MODE_ARG_FLAG,
|
|
642
|
+
MODE_STRICT,
|
|
643
|
+
],
|
|
644
|
+
cwd=str(cwd_path),
|
|
645
|
+
check=False,
|
|
646
|
+
capture_output=True,
|
|
647
|
+
text=True,
|
|
648
|
+
)
|
|
649
|
+
if preflight_process.returncode == 0:
|
|
650
|
+
return None, EXIT_SUCCESS
|
|
651
|
+
return (
|
|
652
|
+
_error_payload(
|
|
653
|
+
preflight_process.stdout.strip()
|
|
654
|
+
or preflight_process.stderr.strip()
|
|
655
|
+
or "preflight failed",
|
|
656
|
+
blocker=BLOCKER_PREFLIGHT,
|
|
657
|
+
),
|
|
658
|
+
EXIT_CONTRACT_ERROR,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _read_git_head(
|
|
663
|
+
cwd_path: Path,
|
|
664
|
+
) -> tuple[str | None, dict[str, object] | None, int]:
|
|
665
|
+
head_process = subprocess.run(
|
|
666
|
+
list(ALL_GIT_REV_PARSE_HEAD_ARGV),
|
|
667
|
+
cwd=str(cwd_path),
|
|
668
|
+
check=False,
|
|
669
|
+
capture_output=True,
|
|
670
|
+
text=True,
|
|
671
|
+
)
|
|
672
|
+
if head_process.returncode != 0:
|
|
673
|
+
return (
|
|
674
|
+
None,
|
|
675
|
+
_error_payload(
|
|
676
|
+
"git rev-parse HEAD failed", blocker=BLOCKER_PREFLIGHT
|
|
677
|
+
),
|
|
678
|
+
EXIT_CONTRACT_ERROR,
|
|
679
|
+
)
|
|
680
|
+
return head_process.stdout.strip(), None, EXIT_SUCCESS
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _seed_open_run_task_list_and_finish(
|
|
684
|
+
*,
|
|
685
|
+
current_head: str,
|
|
686
|
+
entry_skill: str,
|
|
687
|
+
is_copilot_down: bool,
|
|
688
|
+
is_bugbot_down: bool,
|
|
689
|
+
is_codex_down: bool,
|
|
690
|
+
is_codex_required: bool,
|
|
691
|
+
owner: str,
|
|
692
|
+
repo: str,
|
|
693
|
+
pr_number: int,
|
|
694
|
+
state_file: Path,
|
|
695
|
+
cwd_path: Path,
|
|
696
|
+
session_model: str,
|
|
697
|
+
) -> tuple[dict[str, object], int]:
|
|
698
|
+
all_task_list = build_converge_task_list(
|
|
699
|
+
is_bugbot_down=is_bugbot_down,
|
|
700
|
+
is_copilot_down=is_copilot_down,
|
|
701
|
+
is_codex_down=is_codex_down,
|
|
702
|
+
is_codex_required=is_codex_required,
|
|
703
|
+
)
|
|
704
|
+
all_state = build_initial_state(
|
|
705
|
+
current_head=current_head,
|
|
706
|
+
entry_skill=entry_skill,
|
|
707
|
+
is_copilot_down=is_copilot_down,
|
|
708
|
+
is_bugbot_down=is_bugbot_down,
|
|
709
|
+
is_codex_down=is_codex_down,
|
|
710
|
+
is_codex_required=is_codex_required,
|
|
711
|
+
owner=owner,
|
|
712
|
+
repo=repo,
|
|
713
|
+
pr_number=pr_number,
|
|
714
|
+
session_model=session_model,
|
|
715
|
+
cwd_path=str(cwd_path),
|
|
716
|
+
)
|
|
717
|
+
all_state["tasks"] = all_task_list["tasks"]
|
|
718
|
+
all_state["final_task_id"] = all_task_list["final_task_id"]
|
|
719
|
+
all_state["runnable_review_ids"] = all_task_list["runnable_review_ids"]
|
|
720
|
+
all_state["done_when"] = all_task_list["done_when"]
|
|
721
|
+
return _finish_ok(
|
|
722
|
+
all_state,
|
|
723
|
+
state_file,
|
|
724
|
+
next_action=NEXT_RUN_CODE_REVIEW,
|
|
725
|
+
all_commands=_code_review_commands(
|
|
726
|
+
cwd_path=cwd_path, session_model=session_model
|
|
727
|
+
),
|
|
728
|
+
wait_seconds=None,
|
|
729
|
+
blocker=None,
|
|
730
|
+
all_extra_fields=all_task_list,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def run_open_run(
|
|
735
|
+
*,
|
|
736
|
+
entry_skill: str,
|
|
737
|
+
has_workflow: bool,
|
|
738
|
+
has_schedule_wakeup: bool,
|
|
739
|
+
owner: str,
|
|
740
|
+
repo: str,
|
|
741
|
+
pr_number: int,
|
|
742
|
+
cwd_path: Path,
|
|
743
|
+
state_dir: Path,
|
|
744
|
+
session_model: str,
|
|
745
|
+
is_copilot_down: bool,
|
|
746
|
+
is_bugbot_down: bool,
|
|
747
|
+
is_codex_down: bool = False,
|
|
748
|
+
is_codex_required: bool = False,
|
|
749
|
+
) -> tuple[dict[str, object], int]:
|
|
750
|
+
"""Select pacer, require portable, preflight worktree, seed state + tasks.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
entry_skill: Converge entry skill name.
|
|
754
|
+
has_workflow: Host exposes Workflow.
|
|
755
|
+
has_schedule_wakeup: Host exposes ScheduleWakeup.
|
|
756
|
+
owner: Repository owner.
|
|
757
|
+
repo: Repository name.
|
|
758
|
+
pr_number: Pull request number.
|
|
759
|
+
cwd_path: PR worktree path.
|
|
760
|
+
state_dir: Directory for ``pr-converge-state.json``.
|
|
761
|
+
session_model: Alias passed to invoke_code_review.
|
|
762
|
+
is_copilot_down: Skip Copilot as a runnable review.
|
|
763
|
+
is_bugbot_down: Skip Bugbot as a runnable review.
|
|
764
|
+
is_codex_down: Skip Codex as a runnable review.
|
|
765
|
+
is_codex_required: Codex is required by usage/policy for this run.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
Payload and process exit code. Payload includes the scripted task list
|
|
769
|
+
from ``build_converge_task_list.py``; final task is all runnable reviews
|
|
770
|
+
CLEAN on the same HEAD.
|
|
771
|
+
"""
|
|
772
|
+
selection = select_converge_pacer(
|
|
773
|
+
entry_skill=entry_skill,
|
|
774
|
+
has_workflow=has_workflow,
|
|
775
|
+
has_schedule_wakeup=has_schedule_wakeup,
|
|
776
|
+
)
|
|
777
|
+
if selection.pacer != PACER_PORTABLE:
|
|
778
|
+
return (
|
|
779
|
+
_error_payload(
|
|
780
|
+
"open-run is only for pacer=portable",
|
|
781
|
+
blocker=BLOCKER_NOT_PORTABLE,
|
|
782
|
+
pacer=selection.pacer,
|
|
783
|
+
),
|
|
784
|
+
EXIT_CONTRACT_ERROR,
|
|
785
|
+
)
|
|
786
|
+
maybe_error, preflight_exit = _run_preflight(
|
|
787
|
+
cwd_path=cwd_path, owner=owner, repo=repo
|
|
788
|
+
)
|
|
789
|
+
if maybe_error is not None:
|
|
790
|
+
return maybe_error, preflight_exit
|
|
791
|
+
current_head, maybe_head_error, head_exit = _read_git_head(cwd_path)
|
|
792
|
+
if maybe_head_error is not None or current_head is None:
|
|
793
|
+
return maybe_head_error or _error_payload(
|
|
794
|
+
"git rev-parse HEAD failed", blocker=BLOCKER_PREFLIGHT
|
|
795
|
+
), head_exit
|
|
796
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
797
|
+
resolved_codex_required = resolve_codex_required_flag(
|
|
798
|
+
is_codex_down=is_codex_down,
|
|
799
|
+
is_codex_required=is_codex_required,
|
|
800
|
+
)
|
|
801
|
+
return _seed_open_run_task_list_and_finish(
|
|
802
|
+
current_head=current_head,
|
|
803
|
+
entry_skill=selection.entry_skill,
|
|
804
|
+
is_copilot_down=is_copilot_down,
|
|
805
|
+
is_bugbot_down=is_bugbot_down,
|
|
806
|
+
is_codex_down=is_codex_down,
|
|
807
|
+
is_codex_required=resolved_codex_required,
|
|
808
|
+
owner=owner,
|
|
809
|
+
repo=repo,
|
|
810
|
+
pr_number=pr_number,
|
|
811
|
+
state_file=state_dir / STATE_FILENAME,
|
|
812
|
+
cwd_path=cwd_path,
|
|
813
|
+
session_model=session_model,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _is_successful_serve(*, returncode: int) -> bool:
|
|
818
|
+
"""Return True when code-review completed a successful serve.
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
returncode: Helper process exit code.
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
Whether the serve counts as successful for phase advance.
|
|
825
|
+
"""
|
|
826
|
+
return returncode == 0
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def run_after_code_review(
|
|
830
|
+
*,
|
|
831
|
+
state_file: Path,
|
|
832
|
+
returncode: int,
|
|
833
|
+
is_dirty_tree: bool,
|
|
834
|
+
served_command: str,
|
|
835
|
+
current_head: str,
|
|
836
|
+
) -> tuple[dict[str, object], int]:
|
|
837
|
+
"""Advance state from a code-review helper result.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
state_file: Loop state path.
|
|
841
|
+
returncode: Helper process exit code.
|
|
842
|
+
is_dirty_tree: Working tree dirty after review.
|
|
843
|
+
served_command: Informational only (empty, ``in_session``, or chain
|
|
844
|
+
argv). Success is decided solely by ``returncode == 0``.
|
|
845
|
+
current_head: SHA after the review step.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
Payload and process exit code.
|
|
849
|
+
"""
|
|
850
|
+
all_state = load_state(state_file)
|
|
851
|
+
_maybe_reset_on_head_change(all_state, current_head)
|
|
852
|
+
all_state["tick_count"] = int(all_state.get("tick_count") or 0) + 1
|
|
853
|
+
|
|
854
|
+
del served_command
|
|
855
|
+
is_successful_serve = _is_successful_serve(returncode=returncode)
|
|
856
|
+
if not is_successful_serve:
|
|
857
|
+
all_state["phase"] = PHASE_CODE_REVIEW
|
|
858
|
+
all_state["blocker"] = BLOCKER_REVIEW_FAILED
|
|
859
|
+
return _finish_ok(
|
|
860
|
+
all_state,
|
|
861
|
+
state_file,
|
|
862
|
+
next_action=NEXT_RUN_CODE_REVIEW,
|
|
863
|
+
all_commands=_EMPTY_COMMANDS,
|
|
864
|
+
wait_seconds=None,
|
|
865
|
+
blocker=BLOCKER_REVIEW_FAILED,
|
|
866
|
+
all_extra_fields=None,
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
if is_dirty_tree:
|
|
870
|
+
all_state["phase"] = PHASE_CODE_REVIEW
|
|
871
|
+
all_state["code_review_clean_at"] = None
|
|
872
|
+
return _finish_ok(
|
|
873
|
+
all_state,
|
|
874
|
+
state_file,
|
|
875
|
+
next_action=NEXT_APPLY_FIXES,
|
|
876
|
+
all_commands=_EMPTY_COMMANDS,
|
|
877
|
+
wait_seconds=None,
|
|
878
|
+
blocker=None,
|
|
879
|
+
all_extra_fields=None,
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
all_state["code_review_clean_at"] = current_head
|
|
883
|
+
all_state["phase"] = PHASE_BUGTEAM
|
|
884
|
+
all_state["blocker"] = None
|
|
885
|
+
return _finish_ok(
|
|
886
|
+
all_state,
|
|
887
|
+
state_file,
|
|
888
|
+
next_action=NEXT_RUN_BUGTEAM,
|
|
889
|
+
all_commands=_EMPTY_COMMANDS,
|
|
890
|
+
wait_seconds=None,
|
|
891
|
+
blocker=None,
|
|
892
|
+
all_extra_fields=None,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _needs_codex_step(all_state: Mapping[str, object]) -> bool:
|
|
897
|
+
if all_state.get(STATE_KEY_CODEX_DOWN):
|
|
898
|
+
return False
|
|
899
|
+
current_head = all_state.get("current_head")
|
|
900
|
+
if (
|
|
901
|
+
isinstance(current_head, str)
|
|
902
|
+
and all_state.get(STATE_KEY_CODEX_CLEAN_AT) == current_head
|
|
903
|
+
):
|
|
904
|
+
return False
|
|
905
|
+
if all_state.get(STATE_KEY_CODEX_REQUIRED):
|
|
906
|
+
return True
|
|
907
|
+
return probe_weekly_usage_requires_codex()
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _advance_after_codex_resolved(
|
|
911
|
+
all_state: dict[str, object],
|
|
912
|
+
state_file: Path,
|
|
913
|
+
) -> tuple[dict[str, object], int]:
|
|
914
|
+
if all_state.get("copilot_down"):
|
|
915
|
+
all_state["phase"] = PHASE_READY
|
|
916
|
+
return _finish_ok(
|
|
917
|
+
all_state,
|
|
918
|
+
state_file,
|
|
919
|
+
next_action=NEXT_CHECK_READY,
|
|
920
|
+
all_commands=_check_convergence_commands_from_state(all_state),
|
|
921
|
+
wait_seconds=None,
|
|
922
|
+
blocker=None,
|
|
923
|
+
all_extra_fields=None,
|
|
924
|
+
)
|
|
925
|
+
all_state["phase"] = PHASE_COPILOT_WAIT
|
|
926
|
+
return _finish_ok(
|
|
927
|
+
all_state,
|
|
928
|
+
state_file,
|
|
929
|
+
next_action=NEXT_REQUEST_COPILOT,
|
|
930
|
+
all_commands=_EMPTY_COMMANDS,
|
|
931
|
+
wait_seconds=None,
|
|
932
|
+
blocker=None,
|
|
933
|
+
all_extra_fields=None,
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _advance_after_bugbot_resolved(
|
|
938
|
+
all_state: dict[str, object],
|
|
939
|
+
state_file: Path,
|
|
940
|
+
) -> tuple[dict[str, object], int]:
|
|
941
|
+
_ensure_codex_required_matches_usage(all_state)
|
|
942
|
+
if _needs_codex_step(all_state):
|
|
943
|
+
all_state["phase"] = PHASE_BUGBOT
|
|
944
|
+
return _finish_ok(
|
|
945
|
+
all_state,
|
|
946
|
+
state_file,
|
|
947
|
+
next_action=NEXT_RUN_CODEX,
|
|
948
|
+
all_commands=_EMPTY_COMMANDS,
|
|
949
|
+
wait_seconds=None,
|
|
950
|
+
blocker=None,
|
|
951
|
+
all_extra_fields=None,
|
|
952
|
+
)
|
|
953
|
+
return _advance_after_codex_resolved(all_state, state_file)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _after_bugteam_converged(
|
|
957
|
+
all_state: dict[str, object],
|
|
958
|
+
state_file: Path,
|
|
959
|
+
current_head: str,
|
|
960
|
+
) -> tuple[dict[str, object], int]:
|
|
961
|
+
all_state["bugteam_clean_at"] = current_head
|
|
962
|
+
if not all_state.get("bugbot_down"):
|
|
963
|
+
all_state["phase"] = PHASE_BUGBOT
|
|
964
|
+
return _finish_ok(
|
|
965
|
+
all_state,
|
|
966
|
+
state_file,
|
|
967
|
+
next_action=NEXT_RUN_BUGBOT_GATE,
|
|
968
|
+
all_commands=_EMPTY_COMMANDS,
|
|
969
|
+
wait_seconds=None,
|
|
970
|
+
blocker=None,
|
|
971
|
+
all_extra_fields=None,
|
|
972
|
+
)
|
|
973
|
+
return _advance_after_bugbot_resolved(all_state, state_file)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def run_after_bugteam(
|
|
977
|
+
*,
|
|
978
|
+
state_file: Path,
|
|
979
|
+
is_pushed: bool,
|
|
980
|
+
is_converged: bool,
|
|
981
|
+
current_head: str,
|
|
982
|
+
) -> tuple[dict[str, object], int]:
|
|
983
|
+
"""Advance state from a bugteam outcome.
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
state_file: Loop state path.
|
|
987
|
+
is_pushed: Bugteam pushed commits.
|
|
988
|
+
is_converged: Bugteam reported clean with no push.
|
|
989
|
+
current_head: SHA after bugteam.
|
|
990
|
+
|
|
991
|
+
Returns:
|
|
992
|
+
Payload and process exit code.
|
|
993
|
+
"""
|
|
994
|
+
all_state = load_state(state_file)
|
|
995
|
+
_maybe_reset_on_head_change(all_state, current_head)
|
|
996
|
+
all_state["tick_count"] = int(all_state.get("tick_count") or 0) + 1
|
|
997
|
+
|
|
998
|
+
if is_pushed:
|
|
999
|
+
_reset_push_invalidated_markers(all_state)
|
|
1000
|
+
_sync_task_list_from_down_flags(all_state)
|
|
1001
|
+
all_state["phase"] = PHASE_CODE_REVIEW
|
|
1002
|
+
return _finish_ok(
|
|
1003
|
+
all_state,
|
|
1004
|
+
state_file,
|
|
1005
|
+
next_action=NEXT_RUN_CODE_REVIEW,
|
|
1006
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1007
|
+
wait_seconds=None,
|
|
1008
|
+
blocker=None,
|
|
1009
|
+
all_extra_fields=None,
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
if is_converged:
|
|
1013
|
+
return _after_bugteam_converged(all_state, state_file, current_head)
|
|
1014
|
+
|
|
1015
|
+
all_state["phase"] = PHASE_CODE_REVIEW
|
|
1016
|
+
return _finish_ok(
|
|
1017
|
+
all_state,
|
|
1018
|
+
state_file,
|
|
1019
|
+
next_action=NEXT_APPLY_FIXES,
|
|
1020
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1021
|
+
wait_seconds=None,
|
|
1022
|
+
blocker=None,
|
|
1023
|
+
all_extra_fields=None,
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _after_bugbot_down(
|
|
1028
|
+
all_state: dict[str, object],
|
|
1029
|
+
state_file: Path,
|
|
1030
|
+
) -> tuple[dict[str, object], int]:
|
|
1031
|
+
all_state["bugbot_down"] = True
|
|
1032
|
+
_sync_task_list_from_down_flags(all_state)
|
|
1033
|
+
return _advance_after_bugbot_resolved(all_state, state_file)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _after_bugbot_dirty(
|
|
1037
|
+
all_state: dict[str, object],
|
|
1038
|
+
state_file: Path,
|
|
1039
|
+
*,
|
|
1040
|
+
is_inline_lag: bool,
|
|
1041
|
+
) -> tuple[dict[str, object], int]:
|
|
1042
|
+
if not is_inline_lag:
|
|
1043
|
+
all_state["inline_lag_streak"] = 0
|
|
1044
|
+
all_state["phase"] = PHASE_CODE_REVIEW
|
|
1045
|
+
return _finish_ok(
|
|
1046
|
+
all_state,
|
|
1047
|
+
state_file,
|
|
1048
|
+
next_action=NEXT_APPLY_FIXES,
|
|
1049
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1050
|
+
wait_seconds=None,
|
|
1051
|
+
blocker=None,
|
|
1052
|
+
all_extra_fields=None,
|
|
1053
|
+
)
|
|
1054
|
+
streak = int(all_state.get("inline_lag_streak") or 0) + 1
|
|
1055
|
+
all_state["inline_lag_streak"] = streak
|
|
1056
|
+
if streak >= INLINE_LAG_STREAK_CAP:
|
|
1057
|
+
all_state["phase"] = PHASE_BLOCKED
|
|
1058
|
+
all_state["blocker"] = BLOCKER_INLINE_LAG_CAP
|
|
1059
|
+
return _finish_ok(
|
|
1060
|
+
all_state,
|
|
1061
|
+
state_file,
|
|
1062
|
+
next_action=NEXT_STOP_BLOCKED,
|
|
1063
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1064
|
+
wait_seconds=None,
|
|
1065
|
+
blocker=BLOCKER_INLINE_LAG_CAP,
|
|
1066
|
+
all_extra_fields=None,
|
|
1067
|
+
)
|
|
1068
|
+
all_state["phase"] = PHASE_BUGBOT
|
|
1069
|
+
return _finish_ok(
|
|
1070
|
+
all_state,
|
|
1071
|
+
state_file,
|
|
1072
|
+
next_action=NEXT_POLL_WAIT,
|
|
1073
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1074
|
+
wait_seconds=BUGBOT_INLINE_LAG_WAIT_SECONDS,
|
|
1075
|
+
blocker=None,
|
|
1076
|
+
all_extra_fields=None,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _after_bugbot_clean(
|
|
1081
|
+
all_state: dict[str, object],
|
|
1082
|
+
state_file: Path,
|
|
1083
|
+
current_head: str,
|
|
1084
|
+
) -> tuple[dict[str, object], int]:
|
|
1085
|
+
all_state["bugbot_clean_at"] = current_head
|
|
1086
|
+
all_state["inline_lag_streak"] = 0
|
|
1087
|
+
return _advance_after_bugbot_resolved(all_state, state_file)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def run_after_codex(
|
|
1091
|
+
*,
|
|
1092
|
+
state_file: Path,
|
|
1093
|
+
classification: str,
|
|
1094
|
+
current_head: str,
|
|
1095
|
+
) -> tuple[dict[str, object], int]:
|
|
1096
|
+
"""Advance state from a Codex review step outcome.
|
|
1097
|
+
|
|
1098
|
+
Args:
|
|
1099
|
+
state_file: Loop state path.
|
|
1100
|
+
classification: clean, dirty, or down.
|
|
1101
|
+
current_head: Current PR head SHA.
|
|
1102
|
+
|
|
1103
|
+
Returns:
|
|
1104
|
+
Payload and process exit code.
|
|
1105
|
+
"""
|
|
1106
|
+
if classification not in ALL_CODEX_CLASSIFICATIONS:
|
|
1107
|
+
return (
|
|
1108
|
+
_error_payload(f"unknown classification: {classification!r}"),
|
|
1109
|
+
EXIT_USAGE_ERROR,
|
|
1110
|
+
)
|
|
1111
|
+
all_state = load_state(state_file)
|
|
1112
|
+
_maybe_reset_on_head_change(all_state, current_head)
|
|
1113
|
+
all_state["tick_count"] = int(all_state.get("tick_count") or 0) + 1
|
|
1114
|
+
|
|
1115
|
+
if classification == CLASSIFICATION_DIRTY:
|
|
1116
|
+
_reset_push_invalidated_markers(all_state)
|
|
1117
|
+
_sync_task_list_from_down_flags(all_state)
|
|
1118
|
+
all_state["phase"] = PHASE_CODE_REVIEW
|
|
1119
|
+
return _finish_ok(
|
|
1120
|
+
all_state,
|
|
1121
|
+
state_file,
|
|
1122
|
+
next_action=NEXT_APPLY_FIXES,
|
|
1123
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1124
|
+
wait_seconds=None,
|
|
1125
|
+
blocker=None,
|
|
1126
|
+
all_extra_fields=None,
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
if classification == CLASSIFICATION_DOWN:
|
|
1130
|
+
all_state[STATE_KEY_CODEX_DOWN] = True
|
|
1131
|
+
_sync_task_list_from_down_flags(all_state)
|
|
1132
|
+
return _advance_after_codex_resolved(all_state, state_file)
|
|
1133
|
+
|
|
1134
|
+
all_state[STATE_KEY_CODEX_CLEAN_AT] = current_head
|
|
1135
|
+
return _advance_after_codex_resolved(all_state, state_file)
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def run_after_bugbot(
|
|
1139
|
+
*,
|
|
1140
|
+
state_file: Path,
|
|
1141
|
+
classification: str,
|
|
1142
|
+
current_head: str,
|
|
1143
|
+
is_inline_lag: bool,
|
|
1144
|
+
) -> tuple[dict[str, object], int]:
|
|
1145
|
+
"""Advance state from a Bugbot terminal-gate outcome.
|
|
1146
|
+
|
|
1147
|
+
Args:
|
|
1148
|
+
state_file: Loop state path.
|
|
1149
|
+
classification: clean, dirty, absent, or down.
|
|
1150
|
+
current_head: Current PR head SHA.
|
|
1151
|
+
is_inline_lag: Body dirty but zero inline findings.
|
|
1152
|
+
|
|
1153
|
+
Returns:
|
|
1154
|
+
Payload and process exit code.
|
|
1155
|
+
"""
|
|
1156
|
+
if classification not in ALL_CLASSIFICATIONS:
|
|
1157
|
+
return (
|
|
1158
|
+
_error_payload(f"unknown classification: {classification!r}"),
|
|
1159
|
+
EXIT_USAGE_ERROR,
|
|
1160
|
+
)
|
|
1161
|
+
all_state = load_state(state_file)
|
|
1162
|
+
_maybe_reset_on_head_change(all_state, current_head)
|
|
1163
|
+
all_state["tick_count"] = int(all_state.get("tick_count") or 0) + 1
|
|
1164
|
+
|
|
1165
|
+
if classification == CLASSIFICATION_DOWN:
|
|
1166
|
+
return _after_bugbot_down(all_state, state_file)
|
|
1167
|
+
if classification == CLASSIFICATION_DIRTY:
|
|
1168
|
+
return _after_bugbot_dirty(
|
|
1169
|
+
all_state, state_file, is_inline_lag=is_inline_lag
|
|
1170
|
+
)
|
|
1171
|
+
if classification == CLASSIFICATION_ABSENT:
|
|
1172
|
+
wait_count = int(all_state.get("bugbot_wait_count") or 0) + 1
|
|
1173
|
+
all_state["bugbot_wait_count"] = wait_count
|
|
1174
|
+
if wait_count >= BUGBOT_ABSENT_WAIT_HARD_CAP:
|
|
1175
|
+
all_state["phase"] = PHASE_BLOCKED
|
|
1176
|
+
all_state["blocker"] = BLOCKER_BUGBOT_WAIT_CAP
|
|
1177
|
+
return _finish_ok(
|
|
1178
|
+
all_state,
|
|
1179
|
+
state_file,
|
|
1180
|
+
next_action=NEXT_STOP_BLOCKED,
|
|
1181
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1182
|
+
wait_seconds=None,
|
|
1183
|
+
blocker=BLOCKER_BUGBOT_WAIT_CAP,
|
|
1184
|
+
all_extra_fields=None,
|
|
1185
|
+
)
|
|
1186
|
+
all_state["phase"] = PHASE_BUGBOT
|
|
1187
|
+
return _finish_ok(
|
|
1188
|
+
all_state,
|
|
1189
|
+
state_file,
|
|
1190
|
+
next_action=NEXT_POLL_WAIT,
|
|
1191
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1192
|
+
wait_seconds=DEFAULT_WAIT_SECONDS,
|
|
1193
|
+
blocker=None,
|
|
1194
|
+
all_extra_fields=None,
|
|
1195
|
+
)
|
|
1196
|
+
return _after_bugbot_clean(all_state, state_file, current_head)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _after_copilot_not_surfaced(
|
|
1200
|
+
all_state: dict[str, object],
|
|
1201
|
+
state_file: Path,
|
|
1202
|
+
) -> tuple[dict[str, object], int]:
|
|
1203
|
+
wait_count = int(all_state.get("copilot_wait_count") or 0) + 1
|
|
1204
|
+
all_state["copilot_wait_count"] = wait_count
|
|
1205
|
+
if wait_count >= COPILOT_WAIT_HARD_CAP:
|
|
1206
|
+
all_state["phase"] = PHASE_BLOCKED
|
|
1207
|
+
all_state["blocker"] = BLOCKER_COPILOT_WAIT_CAP
|
|
1208
|
+
return _finish_ok(
|
|
1209
|
+
all_state,
|
|
1210
|
+
state_file,
|
|
1211
|
+
next_action=NEXT_STOP_BLOCKED,
|
|
1212
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1213
|
+
wait_seconds=None,
|
|
1214
|
+
blocker=BLOCKER_COPILOT_WAIT_CAP,
|
|
1215
|
+
all_extra_fields=None,
|
|
1216
|
+
)
|
|
1217
|
+
all_state["phase"] = PHASE_COPILOT_WAIT
|
|
1218
|
+
return _finish_ok(
|
|
1219
|
+
all_state,
|
|
1220
|
+
state_file,
|
|
1221
|
+
next_action=NEXT_POLL_WAIT,
|
|
1222
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1223
|
+
wait_seconds=DEFAULT_WAIT_SECONDS,
|
|
1224
|
+
blocker=None,
|
|
1225
|
+
all_extra_fields=None,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def _after_copilot_clean(
|
|
1230
|
+
all_state: dict[str, object],
|
|
1231
|
+
state_file: Path,
|
|
1232
|
+
current_head: str,
|
|
1233
|
+
) -> tuple[dict[str, object], int]:
|
|
1234
|
+
all_state["copilot_clean_at"] = current_head
|
|
1235
|
+
all_state["phase"] = PHASE_READY
|
|
1236
|
+
return _finish_ok(
|
|
1237
|
+
all_state,
|
|
1238
|
+
state_file,
|
|
1239
|
+
next_action=NEXT_CHECK_READY,
|
|
1240
|
+
all_commands=_check_convergence_commands_from_state(all_state),
|
|
1241
|
+
wait_seconds=None,
|
|
1242
|
+
blocker=None,
|
|
1243
|
+
all_extra_fields=None,
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def _after_copilot_down(
|
|
1248
|
+
all_state: dict[str, object],
|
|
1249
|
+
state_file: Path,
|
|
1250
|
+
) -> tuple[dict[str, object], int]:
|
|
1251
|
+
all_state["copilot_down"] = True
|
|
1252
|
+
_sync_task_list_from_down_flags(all_state)
|
|
1253
|
+
all_state["phase"] = PHASE_READY
|
|
1254
|
+
return _finish_ok(
|
|
1255
|
+
all_state,
|
|
1256
|
+
state_file,
|
|
1257
|
+
next_action=NEXT_CHECK_READY,
|
|
1258
|
+
all_commands=_check_convergence_commands_from_state(all_state),
|
|
1259
|
+
wait_seconds=None,
|
|
1260
|
+
blocker=None,
|
|
1261
|
+
all_extra_fields=None,
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _after_copilot_surfaced(
|
|
1266
|
+
all_state: dict[str, object],
|
|
1267
|
+
state_file: Path,
|
|
1268
|
+
*,
|
|
1269
|
+
classification: str,
|
|
1270
|
+
current_head: str,
|
|
1271
|
+
) -> tuple[dict[str, object], int]:
|
|
1272
|
+
if classification not in ALL_COPILOT_CLASSIFICATIONS:
|
|
1273
|
+
return (
|
|
1274
|
+
_error_payload(f"unknown classification: {classification!r}"),
|
|
1275
|
+
EXIT_USAGE_ERROR,
|
|
1276
|
+
)
|
|
1277
|
+
if classification == CLASSIFICATION_DIRTY:
|
|
1278
|
+
all_state["copilot_wait_count"] = 0
|
|
1279
|
+
all_state["phase"] = PHASE_CODE_REVIEW
|
|
1280
|
+
return _finish_ok(
|
|
1281
|
+
all_state,
|
|
1282
|
+
state_file,
|
|
1283
|
+
next_action=NEXT_APPLY_FIXES,
|
|
1284
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1285
|
+
wait_seconds=None,
|
|
1286
|
+
blocker=None,
|
|
1287
|
+
all_extra_fields=None,
|
|
1288
|
+
)
|
|
1289
|
+
if classification == CLASSIFICATION_ABSENT:
|
|
1290
|
+
return _after_copilot_not_surfaced(all_state, state_file)
|
|
1291
|
+
if classification == CLASSIFICATION_DOWN:
|
|
1292
|
+
return _after_copilot_down(all_state, state_file)
|
|
1293
|
+
return _after_copilot_clean(all_state, state_file, current_head)
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
def run_after_copilot_wait(
|
|
1297
|
+
*,
|
|
1298
|
+
state_file: Path,
|
|
1299
|
+
is_review_surfaced: bool,
|
|
1300
|
+
classification: str,
|
|
1301
|
+
current_head: str,
|
|
1302
|
+
) -> tuple[dict[str, object], int]:
|
|
1303
|
+
"""Advance state from a Copilot wait poll.
|
|
1304
|
+
|
|
1305
|
+
Args:
|
|
1306
|
+
state_file: Loop state path.
|
|
1307
|
+
is_review_surfaced: A Copilot review exists at current_head.
|
|
1308
|
+
classification: clean, dirty, absent, or down when surfaced.
|
|
1309
|
+
current_head: Current PR head SHA.
|
|
1310
|
+
|
|
1311
|
+
Returns:
|
|
1312
|
+
Payload and process exit code.
|
|
1313
|
+
"""
|
|
1314
|
+
all_state = load_state(state_file)
|
|
1315
|
+
_maybe_reset_on_head_change(all_state, current_head)
|
|
1316
|
+
all_state["tick_count"] = int(all_state.get("tick_count") or 0) + 1
|
|
1317
|
+
if not is_review_surfaced:
|
|
1318
|
+
return _after_copilot_not_surfaced(all_state, state_file)
|
|
1319
|
+
return _after_copilot_surfaced(
|
|
1320
|
+
all_state,
|
|
1321
|
+
state_file,
|
|
1322
|
+
classification=classification,
|
|
1323
|
+
current_head=current_head,
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
def run_after_ready_check(
|
|
1328
|
+
*,
|
|
1329
|
+
state_file: Path,
|
|
1330
|
+
check_exit: int,
|
|
1331
|
+
current_head: str,
|
|
1332
|
+
) -> tuple[dict[str, object], int]:
|
|
1333
|
+
"""Advance state from check_convergence exit code.
|
|
1334
|
+
|
|
1335
|
+
Args:
|
|
1336
|
+
state_file: Loop state path.
|
|
1337
|
+
check_exit: Process exit from check_convergence.py.
|
|
1338
|
+
current_head: Current PR head SHA.
|
|
1339
|
+
|
|
1340
|
+
Returns:
|
|
1341
|
+
Payload and process exit code.
|
|
1342
|
+
"""
|
|
1343
|
+
all_state = load_state(state_file)
|
|
1344
|
+
_maybe_reset_on_head_change(all_state, current_head)
|
|
1345
|
+
all_state["tick_count"] = int(all_state.get("tick_count") or 0) + 1
|
|
1346
|
+
if check_exit == 0:
|
|
1347
|
+
all_state["phase"] = PHASE_READY
|
|
1348
|
+
all_state["blocker"] = None
|
|
1349
|
+
return _finish_ok(
|
|
1350
|
+
all_state,
|
|
1351
|
+
state_file,
|
|
1352
|
+
next_action=NEXT_MARK_READY,
|
|
1353
|
+
all_commands=_mark_ready_commands(all_state),
|
|
1354
|
+
wait_seconds=None,
|
|
1355
|
+
blocker=None,
|
|
1356
|
+
all_extra_fields=None,
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
if check_exit == 1:
|
|
1360
|
+
_ensure_codex_required_matches_usage(all_state)
|
|
1361
|
+
if _needs_codex_step(all_state):
|
|
1362
|
+
all_state["phase"] = PHASE_BUGBOT
|
|
1363
|
+
return _finish_ok(
|
|
1364
|
+
all_state,
|
|
1365
|
+
state_file,
|
|
1366
|
+
next_action=NEXT_RUN_CODEX,
|
|
1367
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1368
|
+
wait_seconds=None,
|
|
1369
|
+
blocker=None,
|
|
1370
|
+
all_extra_fields=None,
|
|
1371
|
+
)
|
|
1372
|
+
all_state["phase"] = PHASE_CODE_REVIEW
|
|
1373
|
+
return _finish_ok(
|
|
1374
|
+
all_state,
|
|
1375
|
+
state_file,
|
|
1376
|
+
next_action=NEXT_APPLY_FIXES,
|
|
1377
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1378
|
+
wait_seconds=None,
|
|
1379
|
+
blocker=None,
|
|
1380
|
+
all_extra_fields=None,
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
all_state["phase"] = PHASE_BLOCKED
|
|
1384
|
+
all_state["blocker"] = f"check_convergence_exit_{check_exit}"
|
|
1385
|
+
return _finish_ok(
|
|
1386
|
+
all_state,
|
|
1387
|
+
state_file,
|
|
1388
|
+
next_action=NEXT_STOP_BLOCKED,
|
|
1389
|
+
all_commands=_EMPTY_COMMANDS,
|
|
1390
|
+
wait_seconds=None,
|
|
1391
|
+
blocker=str(all_state["blocker"]),
|
|
1392
|
+
all_extra_fields=None,
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def _register_open_run_arguments(
|
|
1397
|
+
open_run_parser: argparse.ArgumentParser,
|
|
1398
|
+
) -> None:
|
|
1399
|
+
open_run_parser.add_argument(CLI_SKILL_FLAG, required=True)
|
|
1400
|
+
open_run_parser.add_argument(CLI_HAS_WORKFLOW_FLAG, required=True)
|
|
1401
|
+
open_run_parser.add_argument(CLI_HAS_SCHEDULE_WAKEUP_FLAG, required=True)
|
|
1402
|
+
open_run_parser.add_argument(CLI_OWNER_FLAG, required=True)
|
|
1403
|
+
open_run_parser.add_argument(CLI_REPO_FLAG, required=True)
|
|
1404
|
+
open_run_parser.add_argument(CLI_PR_NUMBER_FLAG, required=True, type=int)
|
|
1405
|
+
open_run_parser.add_argument(CLI_CWD_FLAG, required=True)
|
|
1406
|
+
open_run_parser.add_argument(CLI_STATE_DIR_FLAG, required=True)
|
|
1407
|
+
open_run_parser.add_argument(
|
|
1408
|
+
CLI_SESSION_MODEL_FLAG, default=DEFAULT_SESSION_MODEL
|
|
1409
|
+
)
|
|
1410
|
+
open_run_parser.add_argument(CLI_COPILOT_DOWN_FLAG, default="0")
|
|
1411
|
+
open_run_parser.add_argument(CLI_BUGBOT_DOWN_FLAG, default="0")
|
|
1412
|
+
open_run_parser.add_argument(CLI_CODEX_DOWN_FLAG, default="0")
|
|
1413
|
+
open_run_parser.add_argument(CLI_CODEX_REQUIRED_FLAG, default="0")
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def build_argument_parser() -> argparse.ArgumentParser:
|
|
1417
|
+
"""Build the multi-command CLI parser.
|
|
1418
|
+
|
|
1419
|
+
Returns:
|
|
1420
|
+
Configured argument parser.
|
|
1421
|
+
"""
|
|
1422
|
+
parser = argparse.ArgumentParser(description=CLI_DESCRIPTION)
|
|
1423
|
+
all_subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1424
|
+
|
|
1425
|
+
open_run_parser = all_subparsers.add_parser(COMMAND_OPEN_RUN)
|
|
1426
|
+
_register_open_run_arguments(open_run_parser)
|
|
1427
|
+
|
|
1428
|
+
after_review_parser = all_subparsers.add_parser(COMMAND_AFTER_CODE_REVIEW)
|
|
1429
|
+
after_review_parser.add_argument(CLI_STATE_FILE_FLAG, required=True)
|
|
1430
|
+
after_review_parser.add_argument(
|
|
1431
|
+
CLI_RETURNCODE_FLAG, required=True, type=int
|
|
1432
|
+
)
|
|
1433
|
+
after_review_parser.add_argument(CLI_DIRTY_TREE_FLAG, required=True)
|
|
1434
|
+
after_review_parser.add_argument(CLI_SERVED_COMMAND_FLAG, default="")
|
|
1435
|
+
after_review_parser.add_argument(CLI_CURRENT_HEAD_FLAG, required=True)
|
|
1436
|
+
|
|
1437
|
+
after_bugteam_parser = all_subparsers.add_parser(COMMAND_AFTER_BUGTEAM)
|
|
1438
|
+
after_bugteam_parser.add_argument(CLI_STATE_FILE_FLAG, required=True)
|
|
1439
|
+
after_bugteam_parser.add_argument(CLI_PUSHED_FLAG, required=True)
|
|
1440
|
+
after_bugteam_parser.add_argument(CLI_CONVERGED_FLAG, required=True)
|
|
1441
|
+
after_bugteam_parser.add_argument(CLI_CURRENT_HEAD_FLAG, required=True)
|
|
1442
|
+
|
|
1443
|
+
after_bugbot_parser = all_subparsers.add_parser(COMMAND_AFTER_BUGBOT)
|
|
1444
|
+
after_bugbot_parser.add_argument(CLI_STATE_FILE_FLAG, required=True)
|
|
1445
|
+
after_bugbot_parser.add_argument(CLI_CLASSIFICATION_FLAG, required=True)
|
|
1446
|
+
after_bugbot_parser.add_argument(CLI_CURRENT_HEAD_FLAG, required=True)
|
|
1447
|
+
after_bugbot_parser.add_argument(CLI_INLINE_LAG_FLAG, default="0")
|
|
1448
|
+
|
|
1449
|
+
after_codex_parser = all_subparsers.add_parser(COMMAND_AFTER_CODEX)
|
|
1450
|
+
after_codex_parser.add_argument(CLI_STATE_FILE_FLAG, required=True)
|
|
1451
|
+
after_codex_parser.add_argument(CLI_CLASSIFICATION_FLAG, required=True)
|
|
1452
|
+
after_codex_parser.add_argument(CLI_CURRENT_HEAD_FLAG, required=True)
|
|
1453
|
+
|
|
1454
|
+
after_copilot_parser = all_subparsers.add_parser(COMMAND_AFTER_COPILOT_WAIT)
|
|
1455
|
+
after_copilot_parser.add_argument(CLI_STATE_FILE_FLAG, required=True)
|
|
1456
|
+
after_copilot_parser.add_argument(CLI_REVIEW_SURFACED_FLAG, required=True)
|
|
1457
|
+
after_copilot_parser.add_argument(
|
|
1458
|
+
CLI_CLASSIFICATION_FLAG, default="absent"
|
|
1459
|
+
)
|
|
1460
|
+
after_copilot_parser.add_argument(CLI_CURRENT_HEAD_FLAG, required=True)
|
|
1461
|
+
|
|
1462
|
+
after_ready_parser = all_subparsers.add_parser(COMMAND_AFTER_READY_CHECK)
|
|
1463
|
+
after_ready_parser.add_argument(CLI_STATE_FILE_FLAG, required=True)
|
|
1464
|
+
after_ready_parser.add_argument(
|
|
1465
|
+
CLI_CHECK_CONVERGENCE_EXIT_FLAG, required=True, type=int
|
|
1466
|
+
)
|
|
1467
|
+
after_ready_parser.add_argument(CLI_CURRENT_HEAD_FLAG, required=True)
|
|
1468
|
+
|
|
1469
|
+
show_parser = all_subparsers.add_parser(COMMAND_SHOW_STATE)
|
|
1470
|
+
show_parser.add_argument(CLI_STATE_FILE_FLAG, required=True)
|
|
1471
|
+
|
|
1472
|
+
return parser
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def _dispatch_open_run(
|
|
1476
|
+
parsed_arguments: argparse.Namespace,
|
|
1477
|
+
) -> tuple[dict[str, object], int]:
|
|
1478
|
+
return run_open_run(
|
|
1479
|
+
entry_skill=parsed_arguments.skill,
|
|
1480
|
+
has_workflow=parse_bool_flag(parsed_arguments.has_workflow),
|
|
1481
|
+
has_schedule_wakeup=parse_bool_flag(
|
|
1482
|
+
parsed_arguments.has_schedule_wakeup
|
|
1483
|
+
),
|
|
1484
|
+
owner=parsed_arguments.owner,
|
|
1485
|
+
repo=parsed_arguments.repo,
|
|
1486
|
+
pr_number=parsed_arguments.pr_number,
|
|
1487
|
+
cwd_path=Path(parsed_arguments.cwd),
|
|
1488
|
+
state_dir=Path(parsed_arguments.state_dir),
|
|
1489
|
+
session_model=parsed_arguments.session_model,
|
|
1490
|
+
is_copilot_down=parse_bool_flag(parsed_arguments.copilot_down),
|
|
1491
|
+
is_bugbot_down=parse_bool_flag(parsed_arguments.bugbot_down),
|
|
1492
|
+
is_codex_down=parse_bool_flag(parsed_arguments.codex_down),
|
|
1493
|
+
is_codex_required=parse_bool_flag(parsed_arguments.codex_required),
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def _dispatch_after_code_review(
|
|
1498
|
+
parsed_arguments: argparse.Namespace,
|
|
1499
|
+
) -> tuple[dict[str, object], int]:
|
|
1500
|
+
return run_after_code_review(
|
|
1501
|
+
state_file=Path(parsed_arguments.state_file),
|
|
1502
|
+
returncode=parsed_arguments.returncode,
|
|
1503
|
+
is_dirty_tree=parse_bool_flag(parsed_arguments.dirty_tree),
|
|
1504
|
+
served_command=parsed_arguments.served_command or "",
|
|
1505
|
+
current_head=parsed_arguments.current_head,
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
def _dispatch_after_bugteam(
|
|
1510
|
+
parsed_arguments: argparse.Namespace,
|
|
1511
|
+
) -> tuple[dict[str, object], int]:
|
|
1512
|
+
return run_after_bugteam(
|
|
1513
|
+
state_file=Path(parsed_arguments.state_file),
|
|
1514
|
+
is_pushed=parse_bool_flag(parsed_arguments.pushed),
|
|
1515
|
+
is_converged=parse_bool_flag(parsed_arguments.converged),
|
|
1516
|
+
current_head=parsed_arguments.current_head,
|
|
1517
|
+
)
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def _dispatch_after_bugbot(
|
|
1521
|
+
parsed_arguments: argparse.Namespace,
|
|
1522
|
+
) -> tuple[dict[str, object], int]:
|
|
1523
|
+
return run_after_bugbot(
|
|
1524
|
+
state_file=Path(parsed_arguments.state_file),
|
|
1525
|
+
classification=parsed_arguments.classification,
|
|
1526
|
+
current_head=parsed_arguments.current_head,
|
|
1527
|
+
is_inline_lag=parse_bool_flag(parsed_arguments.inline_lag),
|
|
1528
|
+
)
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def _dispatch_after_codex(
|
|
1532
|
+
parsed_arguments: argparse.Namespace,
|
|
1533
|
+
) -> tuple[dict[str, object], int]:
|
|
1534
|
+
return run_after_codex(
|
|
1535
|
+
state_file=Path(parsed_arguments.state_file),
|
|
1536
|
+
classification=parsed_arguments.classification,
|
|
1537
|
+
current_head=parsed_arguments.current_head,
|
|
1538
|
+
)
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
def _dispatch_after_copilot_wait(
|
|
1542
|
+
parsed_arguments: argparse.Namespace,
|
|
1543
|
+
) -> tuple[dict[str, object], int]:
|
|
1544
|
+
return run_after_copilot_wait(
|
|
1545
|
+
state_file=Path(parsed_arguments.state_file),
|
|
1546
|
+
is_review_surfaced=parse_bool_flag(parsed_arguments.review_surfaced),
|
|
1547
|
+
classification=parsed_arguments.classification,
|
|
1548
|
+
current_head=parsed_arguments.current_head,
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def _dispatch_after_ready_check(
|
|
1553
|
+
parsed_arguments: argparse.Namespace,
|
|
1554
|
+
) -> tuple[dict[str, object], int]:
|
|
1555
|
+
return run_after_ready_check(
|
|
1556
|
+
state_file=Path(parsed_arguments.state_file),
|
|
1557
|
+
check_exit=parsed_arguments.check_exit,
|
|
1558
|
+
current_head=parsed_arguments.current_head,
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
def _resolve_show_state_next(all_state: Mapping[str, object]) -> str:
|
|
1563
|
+
pending_next = all_state.get(STATE_KEY_PENDING_NEXT)
|
|
1564
|
+
if isinstance(pending_next, str) and pending_next:
|
|
1565
|
+
return pending_next
|
|
1566
|
+
if all_state.get("phase") == PHASE_READY:
|
|
1567
|
+
return NEXT_MARK_READY
|
|
1568
|
+
if all_state.get("phase") == PHASE_BLOCKED:
|
|
1569
|
+
return NEXT_STOP_BLOCKED
|
|
1570
|
+
return str(all_state.get("phase"))
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
def _dispatch_show_state(
|
|
1574
|
+
parsed_arguments: argparse.Namespace,
|
|
1575
|
+
) -> tuple[dict[str, object], int]:
|
|
1576
|
+
state_file = Path(parsed_arguments.state_file)
|
|
1577
|
+
all_state = load_state(state_file)
|
|
1578
|
+
all_payload = _base_ok_payload(all_state, state_file)
|
|
1579
|
+
next_action = _resolve_show_state_next(all_state)
|
|
1580
|
+
all_payload[RESULT_KEY_NEXT] = next_action
|
|
1581
|
+
if next_action == NEXT_RUN_CODE_REVIEW:
|
|
1582
|
+
all_payload[RESULT_KEY_COMMANDS] = _code_review_commands_from_state(
|
|
1583
|
+
all_state
|
|
1584
|
+
)
|
|
1585
|
+
elif next_action == NEXT_CHECK_READY:
|
|
1586
|
+
all_payload[RESULT_KEY_COMMANDS] = (
|
|
1587
|
+
_check_convergence_commands_from_state(all_state)
|
|
1588
|
+
)
|
|
1589
|
+
elif next_action == NEXT_MARK_READY:
|
|
1590
|
+
all_payload[RESULT_KEY_COMMANDS] = _mark_ready_commands(all_state)
|
|
1591
|
+
elif next_action == NEXT_POLL_WAIT:
|
|
1592
|
+
all_payload[RESULT_KEY_WAIT_SECONDS] = _resolve_poll_wait_seconds(
|
|
1593
|
+
all_state
|
|
1594
|
+
)
|
|
1595
|
+
return all_payload, EXIT_SUCCESS
|
|
1596
|
+
|
|
1597
|
+
|
|
1598
|
+
def main(all_argv: list[str]) -> int:
|
|
1599
|
+
"""CLI entry: print one JSON control payload on stdout.
|
|
1600
|
+
|
|
1601
|
+
Args:
|
|
1602
|
+
all_argv: Argument vector without program name.
|
|
1603
|
+
|
|
1604
|
+
Returns:
|
|
1605
|
+
Process exit code.
|
|
1606
|
+
"""
|
|
1607
|
+
parser = build_argument_parser()
|
|
1608
|
+
parsed_arguments = parser.parse_args(all_argv)
|
|
1609
|
+
dispatch_by_command = {
|
|
1610
|
+
COMMAND_OPEN_RUN: _dispatch_open_run,
|
|
1611
|
+
COMMAND_AFTER_CODE_REVIEW: _dispatch_after_code_review,
|
|
1612
|
+
COMMAND_AFTER_BUGTEAM: _dispatch_after_bugteam,
|
|
1613
|
+
COMMAND_AFTER_BUGBOT: _dispatch_after_bugbot,
|
|
1614
|
+
COMMAND_AFTER_CODEX: _dispatch_after_codex,
|
|
1615
|
+
COMMAND_AFTER_COPILOT_WAIT: _dispatch_after_copilot_wait,
|
|
1616
|
+
COMMAND_AFTER_READY_CHECK: _dispatch_after_ready_check,
|
|
1617
|
+
COMMAND_SHOW_STATE: _dispatch_show_state,
|
|
1618
|
+
}
|
|
1619
|
+
try:
|
|
1620
|
+
maybe_dispatch = dispatch_by_command.get(parsed_arguments.command)
|
|
1621
|
+
if maybe_dispatch is None:
|
|
1622
|
+
_emit(
|
|
1623
|
+
_error_payload(
|
|
1624
|
+
f"unknown command: {parsed_arguments.command!r}"
|
|
1625
|
+
)
|
|
1626
|
+
)
|
|
1627
|
+
return EXIT_USAGE_ERROR
|
|
1628
|
+
all_payload, exit_code = maybe_dispatch(parsed_arguments)
|
|
1629
|
+
_emit(all_payload)
|
|
1630
|
+
return exit_code
|
|
1631
|
+
except ValueError as validation_error:
|
|
1632
|
+
_emit(_error_payload(str(validation_error)))
|
|
1633
|
+
return EXIT_USAGE_ERROR
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
if __name__ == "__main__":
|
|
1637
|
+
raise SystemExit(main(sys.argv[1:]))
|