claude-dev-env 2.1.0 → 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/package.json +1 -1
- 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/autoconverge/CLAUDE.md +5 -3
- package/skills/autoconverge/SKILL.md +185 -74
- package/skills/autoconverge/reference/convergence.md +5 -5
- 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/copilot-finding-triage/SKILL.md +16 -4
- package/skills/pr-converge/CLAUDE.md +4 -3
- package/skills/pr-converge/SKILL.md +236 -44
- 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/test_portable_pacer_gate.py +67 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -3
package/package.json
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
# pr-loop
|
|
2
2
|
|
|
3
|
-
Shared infrastructure for the PR audit-fix loop used by `bugteam
|
|
3
|
+
Shared infrastructure for the PR audit-fix loop used by `bugteam`,
|
|
4
|
+
`pr-converge`, and `autoconverge`. Provides the XML prompt template, Python
|
|
5
|
+
runtime scripts, the portable converge driver protocol, and named constants
|
|
6
|
+
those skills invoke during each loop tick.
|
|
4
7
|
|
|
5
8
|
## Subdirectories
|
|
6
9
|
|
|
7
10
|
| Directory | Role |
|
|
8
11
|
|---|---|
|
|
9
12
|
| `prompts/` | XML agent prompt templates. |
|
|
10
|
-
| `scripts/` | Python scripts for loop state management, prompt building, outcome recording, path resolution, and preflight checks. |
|
|
13
|
+
| `scripts/` | Python scripts for loop state management, prompt building, outcome recording, path resolution, pacer selection, and preflight checks. |
|
|
11
14
|
|
|
12
15
|
## Key files
|
|
13
16
|
|
|
14
17
|
| File | Role |
|
|
15
18
|
|---|---|
|
|
19
|
+
| `portable-driver.md` | Continuous in-session pacer when Workflow / ScheduleWakeup are absent. |
|
|
16
20
|
| `prompts/pr-consistency-audit.xml` | Structured prompt artifact for the cross-file consistency audit agent. |
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
21
|
+
| `scripts/select_converge_pacer.py` | Maps entry skill + host tool flags to `workflow`, `schedule_wakeup`, or `portable`. |
|
|
22
|
+
| `scripts/build_audit_prompt.py` | Assembles the audit agent prompt from loop state and the constants module. |
|
|
23
|
+
| `scripts/build_fix_prompt.py` | Assembles the fix agent prompt from loop state and findings. |
|
|
24
|
+
| `scripts/init_loop_state.py` | Initializes the per-PR loop state JSON file. |
|
|
25
|
+
| `scripts/write_audit_outcomes.py` | Writes the per-loop audit outcome XML into the workspace. |
|
|
26
|
+
| `scripts/write_fix_outcomes.py` | Writes the per-loop fix outcome XML into the workspace. |
|
|
27
|
+
| `scripts/preflight_worktree.py` | Verifies the working directory is a healthy worktree for the target PR's repo. |
|
|
28
|
+
| `scripts/teardown_worktrees.py` | Removes loop worktrees on clean exit. |
|
|
29
|
+
| `scripts/write_handoff.py` | Writes durable resume-handoff files under `~/.claude/runtime/pr-loop/<run-name>/` at each converge checkpoint. |
|
|
30
|
+
| `scripts/_path_resolver.py` | Resolves workspace and worktree paths from PR metadata. |
|
|
31
|
+
| `scripts/_cli_utils.py` | Shared CLI argument parsing helpers. |
|
|
32
|
+
| `scripts/_xml_utils.py` | XML serialization helpers. |
|
|
28
33
|
| `scripts/skills_pr_loop_constants/` | Named constants package imported by the scripts above. |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Portable converge driver
|
|
2
|
+
|
|
3
|
+
**Rule: deterministic control is script-only.** Phase transitions, wait delays,
|
|
4
|
+
clean stamps, ready decisions, task lists, and “what next” never live as prose
|
|
5
|
+
for the agent to invent. The agent runs scripts, reads JSON, and only performs
|
|
6
|
+
judgment steps the JSON names.
|
|
7
|
+
|
|
8
|
+
## Step 1 — task list (every autoconverge / portable run)
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
python "$HOME/.claude/skills/_shared/pr-loop/scripts/build_converge_task_list.py" \
|
|
12
|
+
[--bugbot-down 0|1] [--copilot-down 0|1] \
|
|
13
|
+
[--codex-down 0|1] [--codex-required 0|1]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Register every `tasks[]` entry on the session task list. **Final task id is
|
|
17
|
+
always** `all_runnable_reviews_clean_same_head`. The run is complete only when
|
|
18
|
+
that final task is completed: every runnable code review is CLEAN on one
|
|
19
|
+
shared HEAD. Do not invent tasks in prose.
|
|
20
|
+
|
|
21
|
+
`open-run` embeds the same list (`tasks`, `runnable_review_ids`,
|
|
22
|
+
`final_task_id`, `done_when`).
|
|
23
|
+
|
|
24
|
+
## Pacer selection
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
python "$HOME/.claude/skills/_shared/pr-loop/scripts/select_converge_pacer.py" \
|
|
28
|
+
--skill <pr-converge|autoconverge> \
|
|
29
|
+
--has-workflow <0|1> \
|
|
30
|
+
--has-schedule-wakeup <0|1>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
When `pacer` is not `portable`, use the skill’s native Workflow or
|
|
34
|
+
ScheduleWakeup path. When `pacer=portable`, use the control script below.
|
|
35
|
+
|
|
36
|
+
## Isolation and worktree
|
|
37
|
+
|
|
38
|
+
1. When the tool list includes `EnterWorktree`, call it (same contract as the
|
|
39
|
+
Claude-host skills).
|
|
40
|
+
2. When `EnterWorktree` is absent, isolate with git worktree machinery:
|
|
41
|
+
- Prefer an existing worktree already on the PR head ref under
|
|
42
|
+
`.claude/worktrees/` (or another dedicated worktree path).
|
|
43
|
+
- Otherwise `git fetch origin <headRefName>` and
|
|
44
|
+
`git worktree add <path> <headRefName>` (or `gh pr checkout <N>` into a
|
|
45
|
+
dedicated directory), then `cd` into that checkout.
|
|
46
|
+
3. Confirm the working directory is the PR’s own repo on the PR head SHA:
|
|
47
|
+
`python "$HOME/.claude/skills/_shared/pr-loop/scripts/preflight_worktree.py" --owner <O> --repo <R> --mode strict`.
|
|
48
|
+
Non-zero exit → report the `ABORT` line and stop.
|
|
49
|
+
4. Cross-repo routing follows pr-converge Step 1.5: every local review and edit
|
|
50
|
+
runs with cwd set to the **PR worktree**.
|
|
51
|
+
|
|
52
|
+
`open-run` runs the same strict preflight before seeding state.
|
|
53
|
+
|
|
54
|
+
## Control script
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
python "$HOME/.claude/skills/_shared/pr-loop/scripts/portable_converge_driver.py" <command> ...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Stdout JSON: `status`, `next`, `phase`, `state_file`, optional `commands`,
|
|
61
|
+
`wait_seconds`, `blocker`, and on `open-run` the task-list fields. Exit `0` =
|
|
62
|
+
ok; `1` = contract failure; `2` = usage error.
|
|
63
|
+
|
|
64
|
+
| Command | Deterministic effect |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `open-run` | Require `portable`; preflight; seed state + task list; when `--codex-down` is off, set `codex_required` from CLI force-on or the weekly usage probe (same rule as `check_convergence`); next=`run_code_review` |
|
|
67
|
+
| `after-code-review` | From returncode / dirty_tree / served_command |
|
|
68
|
+
| `after-bugteam` | From pushed / converged |
|
|
69
|
+
| `after-bugbot` | From classification / inline lag |
|
|
70
|
+
| `after-codex` | From classification clean / dirty / down |
|
|
71
|
+
| `after-copilot-wait` | From review surfaced / wait cap |
|
|
72
|
+
| `after-ready-check` | From check_convergence exit |
|
|
73
|
+
| `show-state` | Echo state; rehydrate `commands` / `wait_seconds` for pending next |
|
|
74
|
+
|
|
75
|
+
## Continuous tick loop
|
|
76
|
+
|
|
77
|
+
After transport check, PR scope, isolation, permission grant, and the once-per-run
|
|
78
|
+
Copilot quota pre-check:
|
|
79
|
+
|
|
80
|
+
1. Seed or restore state via `open-run` or `show-state` (and handoff
|
|
81
|
+
`state-copy.json` when resuming).
|
|
82
|
+
2. Run the driver command for the current step; read JSON `next` / `commands` /
|
|
83
|
+
`wait_seconds`.
|
|
84
|
+
3. On non-terminal `next`:
|
|
85
|
+
- Write state and handoff when the after-* payload says so.
|
|
86
|
+
- If `next` is `poll_wait`, sleep `wait_seconds` only, then re-poll and call
|
|
87
|
+
the matching after-*.
|
|
88
|
+
- If `next` is immediate work, continue in the same turn without sleeping.
|
|
89
|
+
4. On `mark_ready` / `stop_blocked` / named stop: run lifecycle Close, print the
|
|
90
|
+
entry skill’s exit block, omit further pacing.
|
|
91
|
+
5. External reviewers remain skippable the same way as Claude-host runs when
|
|
92
|
+
opted out or down. Push and head-change reset push-invalidated markers
|
|
93
|
+
(`*_clean_at`, `merge_state_status`, `bugbot_down`, `codex_down`).
|
|
94
|
+
|
|
95
|
+
## Agent loop (judgment only)
|
|
96
|
+
|
|
97
|
+
1. Run the driver command for the current step.
|
|
98
|
+
2. If JSON `commands` is non-empty, run that argv (scripted helper).
|
|
99
|
+
3. Map JSON `next` to the single judgment action; report back via the matching
|
|
100
|
+
`after-*` command.
|
|
101
|
+
4. Mark session tasks complete only when the scripted stamps say so; complete
|
|
102
|
+
the final task only when every runnable review is clean on the same HEAD.
|
|
103
|
+
|
|
104
|
+
| `next` | Agent does | Report with |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `run_code_review` | Run `commands` | `after-code-review` |
|
|
107
|
+
| `apply_fixes_and_push` | Fix protocol; commit; push | re-review then `after-code-review` |
|
|
108
|
+
| `run_bugteam` | resolve_worker_spawn / bugteam body | `after-bugteam` |
|
|
109
|
+
| `run_bugbot_gate` | Bugbot helper scripts | `after-bugbot` |
|
|
110
|
+
| `run_codex_review` | Run Codex review step | `after-codex` |
|
|
111
|
+
| `request_copilot_review` | Request Copilot | `after-copilot-wait` |
|
|
112
|
+
| `poll_wait` | Sleep `wait_seconds` only | re-poll then after-* |
|
|
113
|
+
| `check_ready` | Run `commands` (check_convergence) | `after-ready-check` |
|
|
114
|
+
| `mark_ready` | Run `commands` (`gh pr ready`) | teardown |
|
|
115
|
+
| `stop_blocked` | Teardown; print blocker | stop |
|
|
116
|
+
|
|
117
|
+
## Autoconverge entry on portable pacer
|
|
118
|
+
|
|
119
|
+
When `/autoconverge` selects `pacer=portable`:
|
|
120
|
+
|
|
121
|
+
- Complete autoconverge pre-flight (scope, draft ownership, strict worktree,
|
|
122
|
+
grant, Copilot quota).
|
|
123
|
+
- Drive the continuous tick loop above (scripted phase machine) until ready
|
|
124
|
+
or a documented blocker.
|
|
125
|
+
- Skip `Workflow({ scriptPath: converge.mjs })` — that path requires the
|
|
126
|
+
Workflow tool.
|
|
127
|
+
- Teardown uses the lifecycle Close path; the Workflow-only closing HTML
|
|
128
|
+
journal report is optional and skipped when no workflow run id exists.
|
|
129
|
+
- Resume command on handoff: `/autoconverge <PR URL>`.
|
|
130
|
+
|
|
131
|
+
When `/autoconverge` selects `pacer=workflow`, follow the skill’s Workflow
|
|
132
|
+
sections unchanged.
|
|
133
|
+
|
|
134
|
+
## Helpers (scripted)
|
|
135
|
+
|
|
136
|
+
| Concern | Script |
|
|
137
|
+
|---|---|
|
|
138
|
+
| Task list | `build_converge_task_list.py` |
|
|
139
|
+
| Pacer | `select_converge_pacer.py` |
|
|
140
|
+
| Worktree | `preflight_worktree.py` |
|
|
141
|
+
| Code review | `$HOME/.claude/scripts/invoke_code_review.py` |
|
|
142
|
+
| Workers | `$HOME/.claude/scripts/resolve_worker_spawn.py` |
|
|
143
|
+
| Ready | `pr-converge/scripts/check_convergence.py` |
|
|
144
|
+
| Handoff | `write_handoff.py` |
|
|
145
|
+
|
|
146
|
+
## Fail closed
|
|
147
|
+
|
|
148
|
+
Never abort solely because Workflow or ScheduleWakeup is missing once
|
|
149
|
+
pacer=`portable`. Fail closed on contract failures only: auth, worktree,
|
|
150
|
+
unresolvable gates, and wait caps.
|
|
@@ -14,6 +14,9 @@ Python scripts that run the PR audit-fix loop at runtime. Both `bugteam` and `pr
|
|
|
14
14
|
| `preflight_worktree.py` | Verifies the working directory is a healthy git worktree for the target PR's repo. Supports `--mode strict` to abort when the repo does not match. |
|
|
15
15
|
| `teardown_worktrees.py` | Removes per-PR worktrees after a clean loop exit. |
|
|
16
16
|
| `write_handoff.py` | Writes durable resume-handoff files under the run's `~/.claude/runtime/pr-loop` directory at each converge checkpoint. |
|
|
17
|
+
| `select_converge_pacer.py` | Selects `workflow`, `schedule_wakeup`, or `portable` for pr-converge / autoconverge from host tool flags. |
|
|
18
|
+
| `build_converge_task_list.py` | Step-1 task list: runnable review gates + final all_runnable_reviews_clean_same_head. |
|
|
19
|
+
| `portable_converge_driver.py` | portable_converge_driver phase machine: open-run and post-step transitions emit JSON next/commands only. |
|
|
17
20
|
| `_path_resolver.py` | Resolves workspace and worktree paths from PR owner, repo, and number. |
|
|
18
21
|
| `_cli_utils.py` | Shared CLI argument parsing helpers (argparse wrappers). |
|
|
19
22
|
| `_xml_utils.py` | XML serialization helpers for outcome files. |
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""``build_converge_task_list.py`` — ordered review tasks for a run.
|
|
3
|
+
|
|
4
|
+
Step 1 of autoconverge / portable pr-converge: call this script. Do not invent
|
|
5
|
+
tasks in prose. The final task is always all runnable code reviews CLEAN on
|
|
6
|
+
the same HEAD.
|
|
7
|
+
|
|
8
|
+
::
|
|
9
|
+
|
|
10
|
+
python build_converge_task_list.py \\
|
|
11
|
+
[--bugbot-down 0|1] [--copilot-down 0|1] \\
|
|
12
|
+
[--codex-down 0|1] [--codex-required 0|1]
|
|
13
|
+
|
|
14
|
+
Stdout JSON::
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
"tasks": [...],
|
|
18
|
+
"runnable_review_ids": [...],
|
|
19
|
+
"skipped_review_ids": [...],
|
|
20
|
+
"final_task_id": "all_runnable_reviews_clean_same_head",
|
|
21
|
+
"done_when": "..."
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
_self_dir = Path(__file__).resolve().parent
|
|
33
|
+
if str(_self_dir) not in sys.path:
|
|
34
|
+
sys.path.insert(0, str(_self_dir))
|
|
35
|
+
|
|
36
|
+
from select_converge_pacer import parse_bool_flag # noqa: E402
|
|
37
|
+
from skills_pr_loop_constants.converge_task_list_constants import ( # noqa: E402
|
|
38
|
+
CLI_BUGBOT_DOWN_FLAG,
|
|
39
|
+
CLI_CODEX_DOWN_FLAG,
|
|
40
|
+
CLI_CODEX_REQUIRED_FLAG,
|
|
41
|
+
CLI_COPILOT_DOWN_FLAG,
|
|
42
|
+
DONE_WHEN_TEXT,
|
|
43
|
+
EXIT_SUCCESS,
|
|
44
|
+
EXIT_USAGE_ERROR,
|
|
45
|
+
RESULT_KEY_DONE_WHEN,
|
|
46
|
+
RESULT_KEY_FINAL_TASK_ID,
|
|
47
|
+
RESULT_KEY_RUNNABLE_REVIEW_IDS,
|
|
48
|
+
RESULT_KEY_SKIPPED_REVIEW_IDS,
|
|
49
|
+
RESULT_KEY_TASKS,
|
|
50
|
+
SKIP_REASON_BUGBOT_DOWN,
|
|
51
|
+
SKIP_REASON_CODEX_DOWN,
|
|
52
|
+
SKIP_REASON_CODEX_NOT_REQUIRED,
|
|
53
|
+
SKIP_REASON_COPILOT_DOWN,
|
|
54
|
+
TASK_FIELD_ID,
|
|
55
|
+
TASK_FIELD_IS_RUNNABLE,
|
|
56
|
+
TASK_FIELD_KIND,
|
|
57
|
+
TASK_FIELD_SKIP_REASON,
|
|
58
|
+
TASK_FIELD_TITLE,
|
|
59
|
+
TASK_ID_ALL_CLEAN_SAME_HEAD,
|
|
60
|
+
TASK_ID_BUGBOT,
|
|
61
|
+
TASK_ID_BUGTEAM,
|
|
62
|
+
TASK_ID_CODE_REVIEW,
|
|
63
|
+
TASK_ID_CODEX,
|
|
64
|
+
TASK_ID_COPILOT,
|
|
65
|
+
TASK_KIND_FINAL,
|
|
66
|
+
TASK_KIND_REVIEW,
|
|
67
|
+
TASK_TITLE_ALL_CLEAN_SAME_HEAD,
|
|
68
|
+
TASK_TITLE_BUGBOT,
|
|
69
|
+
TASK_TITLE_BUGTEAM,
|
|
70
|
+
TASK_TITLE_CODE_REVIEW,
|
|
71
|
+
TASK_TITLE_CODEX,
|
|
72
|
+
TASK_TITLE_COPILOT,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _review_task(
|
|
77
|
+
*,
|
|
78
|
+
task_id: str,
|
|
79
|
+
title: str,
|
|
80
|
+
is_runnable: bool,
|
|
81
|
+
skip_reason: str | None,
|
|
82
|
+
) -> dict[str, object]:
|
|
83
|
+
task: dict[str, object] = {
|
|
84
|
+
TASK_FIELD_ID: task_id,
|
|
85
|
+
TASK_FIELD_TITLE: title,
|
|
86
|
+
TASK_FIELD_KIND: TASK_KIND_REVIEW,
|
|
87
|
+
TASK_FIELD_IS_RUNNABLE: is_runnable,
|
|
88
|
+
}
|
|
89
|
+
if skip_reason is not None:
|
|
90
|
+
task[TASK_FIELD_SKIP_REASON] = skip_reason
|
|
91
|
+
return task
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _append_always_runnable_reviews(
|
|
95
|
+
all_tasks: list[dict[str, object]],
|
|
96
|
+
) -> None:
|
|
97
|
+
all_tasks.append(
|
|
98
|
+
_review_task(
|
|
99
|
+
task_id=TASK_ID_CODE_REVIEW,
|
|
100
|
+
title=TASK_TITLE_CODE_REVIEW,
|
|
101
|
+
is_runnable=True,
|
|
102
|
+
skip_reason=None,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
all_tasks.append(
|
|
106
|
+
_review_task(
|
|
107
|
+
task_id=TASK_ID_BUGTEAM,
|
|
108
|
+
title=TASK_TITLE_BUGTEAM,
|
|
109
|
+
is_runnable=True,
|
|
110
|
+
skip_reason=None,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _append_bugbot_task(
|
|
116
|
+
all_tasks: list[dict[str, object]],
|
|
117
|
+
*,
|
|
118
|
+
is_bugbot_down: bool,
|
|
119
|
+
) -> None:
|
|
120
|
+
if is_bugbot_down:
|
|
121
|
+
all_tasks.append(
|
|
122
|
+
_review_task(
|
|
123
|
+
task_id=TASK_ID_BUGBOT,
|
|
124
|
+
title=TASK_TITLE_BUGBOT,
|
|
125
|
+
is_runnable=False,
|
|
126
|
+
skip_reason=SKIP_REASON_BUGBOT_DOWN,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
all_tasks.append(
|
|
131
|
+
_review_task(
|
|
132
|
+
task_id=TASK_ID_BUGBOT,
|
|
133
|
+
title=TASK_TITLE_BUGBOT,
|
|
134
|
+
is_runnable=True,
|
|
135
|
+
skip_reason=None,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _append_copilot_task(
|
|
141
|
+
all_tasks: list[dict[str, object]],
|
|
142
|
+
*,
|
|
143
|
+
is_copilot_down: bool,
|
|
144
|
+
) -> None:
|
|
145
|
+
if is_copilot_down:
|
|
146
|
+
all_tasks.append(
|
|
147
|
+
_review_task(
|
|
148
|
+
task_id=TASK_ID_COPILOT,
|
|
149
|
+
title=TASK_TITLE_COPILOT,
|
|
150
|
+
is_runnable=False,
|
|
151
|
+
skip_reason=SKIP_REASON_COPILOT_DOWN,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
all_tasks.append(
|
|
156
|
+
_review_task(
|
|
157
|
+
task_id=TASK_ID_COPILOT,
|
|
158
|
+
title=TASK_TITLE_COPILOT,
|
|
159
|
+
is_runnable=True,
|
|
160
|
+
skip_reason=None,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _append_codex_task(
|
|
166
|
+
all_tasks: list[dict[str, object]],
|
|
167
|
+
*,
|
|
168
|
+
is_codex_down: bool,
|
|
169
|
+
is_codex_required: bool,
|
|
170
|
+
) -> None:
|
|
171
|
+
if is_codex_down:
|
|
172
|
+
all_tasks.append(
|
|
173
|
+
_review_task(
|
|
174
|
+
task_id=TASK_ID_CODEX,
|
|
175
|
+
title=TASK_TITLE_CODEX,
|
|
176
|
+
is_runnable=False,
|
|
177
|
+
skip_reason=SKIP_REASON_CODEX_DOWN,
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
return
|
|
181
|
+
if not is_codex_required:
|
|
182
|
+
all_tasks.append(
|
|
183
|
+
_review_task(
|
|
184
|
+
task_id=TASK_ID_CODEX,
|
|
185
|
+
title=TASK_TITLE_CODEX,
|
|
186
|
+
is_runnable=False,
|
|
187
|
+
skip_reason=SKIP_REASON_CODEX_NOT_REQUIRED,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
return
|
|
191
|
+
all_tasks.append(
|
|
192
|
+
_review_task(
|
|
193
|
+
task_id=TASK_ID_CODEX,
|
|
194
|
+
title=TASK_TITLE_CODEX,
|
|
195
|
+
is_runnable=True,
|
|
196
|
+
skip_reason=None,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _append_final_task(all_tasks: list[dict[str, object]]) -> None:
|
|
202
|
+
all_tasks.append(
|
|
203
|
+
{
|
|
204
|
+
TASK_FIELD_ID: TASK_ID_ALL_CLEAN_SAME_HEAD,
|
|
205
|
+
TASK_FIELD_TITLE: TASK_TITLE_ALL_CLEAN_SAME_HEAD,
|
|
206
|
+
TASK_FIELD_KIND: TASK_KIND_FINAL,
|
|
207
|
+
TASK_FIELD_IS_RUNNABLE: True,
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _partition_review_ids(
|
|
213
|
+
all_tasks: list[dict[str, object]],
|
|
214
|
+
) -> tuple[list[str], list[str]]:
|
|
215
|
+
all_runnable_review_ids = [
|
|
216
|
+
str(each_task[TASK_FIELD_ID])
|
|
217
|
+
for each_task in all_tasks
|
|
218
|
+
if each_task[TASK_FIELD_KIND] == TASK_KIND_REVIEW
|
|
219
|
+
and each_task[TASK_FIELD_IS_RUNNABLE] is True
|
|
220
|
+
]
|
|
221
|
+
all_skipped_review_ids = [
|
|
222
|
+
str(each_task[TASK_FIELD_ID])
|
|
223
|
+
for each_task in all_tasks
|
|
224
|
+
if each_task[TASK_FIELD_KIND] == TASK_KIND_REVIEW
|
|
225
|
+
and each_task[TASK_FIELD_IS_RUNNABLE] is False
|
|
226
|
+
]
|
|
227
|
+
return all_runnable_review_ids, all_skipped_review_ids
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def build_converge_task_list(
|
|
231
|
+
*,
|
|
232
|
+
is_bugbot_down: bool,
|
|
233
|
+
is_copilot_down: bool,
|
|
234
|
+
is_codex_down: bool,
|
|
235
|
+
is_codex_required: bool,
|
|
236
|
+
) -> dict[str, object]:
|
|
237
|
+
"""Return the ordered task list and done criterion for one converge run.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
is_bugbot_down: Skip Bugbot as a runnable review.
|
|
241
|
+
is_copilot_down: Skip Copilot as a runnable review.
|
|
242
|
+
is_codex_down: Skip Codex even when usage would require it.
|
|
243
|
+
is_codex_required: Weekly usage / policy requires a Codex review.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
JSON-serializable control object with ``tasks`` and ``done_when``.
|
|
247
|
+
"""
|
|
248
|
+
all_tasks: list[dict[str, object]] = []
|
|
249
|
+
_append_always_runnable_reviews(all_tasks)
|
|
250
|
+
_append_bugbot_task(all_tasks, is_bugbot_down=is_bugbot_down)
|
|
251
|
+
_append_copilot_task(all_tasks, is_copilot_down=is_copilot_down)
|
|
252
|
+
_append_codex_task(
|
|
253
|
+
all_tasks,
|
|
254
|
+
is_codex_down=is_codex_down,
|
|
255
|
+
is_codex_required=is_codex_required,
|
|
256
|
+
)
|
|
257
|
+
_append_final_task(all_tasks)
|
|
258
|
+
all_runnable_review_ids, all_skipped_review_ids = _partition_review_ids(
|
|
259
|
+
all_tasks
|
|
260
|
+
)
|
|
261
|
+
return {
|
|
262
|
+
RESULT_KEY_TASKS: all_tasks,
|
|
263
|
+
RESULT_KEY_RUNNABLE_REVIEW_IDS: all_runnable_review_ids,
|
|
264
|
+
RESULT_KEY_SKIPPED_REVIEW_IDS: all_skipped_review_ids,
|
|
265
|
+
RESULT_KEY_FINAL_TASK_ID: TASK_ID_ALL_CLEAN_SAME_HEAD,
|
|
266
|
+
RESULT_KEY_DONE_WHEN: DONE_WHEN_TEXT,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def build_argument_parser() -> argparse.ArgumentParser:
|
|
271
|
+
"""Build the CLI parser for ``build_converge_task_list``.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Configured argument parser.
|
|
275
|
+
"""
|
|
276
|
+
parser = argparse.ArgumentParser(description="build_converge_task_list")
|
|
277
|
+
parser.add_argument(CLI_BUGBOT_DOWN_FLAG, default="0")
|
|
278
|
+
parser.add_argument(CLI_COPILOT_DOWN_FLAG, default="0")
|
|
279
|
+
parser.add_argument(CLI_CODEX_DOWN_FLAG, default="0")
|
|
280
|
+
parser.add_argument(CLI_CODEX_REQUIRED_FLAG, default="0")
|
|
281
|
+
return parser
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def main(all_argv: list[str]) -> int:
|
|
285
|
+
"""CLI entry: print one task-list JSON object on stdout.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
all_argv: Argument vector without program name.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Process exit code.
|
|
292
|
+
"""
|
|
293
|
+
parser = build_argument_parser()
|
|
294
|
+
parsed_arguments = parser.parse_args(all_argv)
|
|
295
|
+
try:
|
|
296
|
+
task_list_payload = build_converge_task_list(
|
|
297
|
+
is_bugbot_down=parse_bool_flag(parsed_arguments.bugbot_down),
|
|
298
|
+
is_copilot_down=parse_bool_flag(parsed_arguments.copilot_down),
|
|
299
|
+
is_codex_down=parse_bool_flag(parsed_arguments.codex_down),
|
|
300
|
+
is_codex_required=parse_bool_flag(parsed_arguments.codex_required),
|
|
301
|
+
)
|
|
302
|
+
except ValueError as validation_error:
|
|
303
|
+
print(str(validation_error), file=sys.stderr)
|
|
304
|
+
return EXIT_USAGE_ERROR
|
|
305
|
+
print(json.dumps(task_list_payload, sort_keys=True))
|
|
306
|
+
return EXIT_SUCCESS
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
raise SystemExit(main(sys.argv[1:]))
|