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
|
@@ -42,7 +42,27 @@ each PR its own checkout with `git worktree add`. For each PR the user named:
|
|
|
42
42
|
non-zero exit sets `copilotDisabled: true` on every entry, so each child skips
|
|
43
43
|
the Copilot gate with no agent spawned.
|
|
44
44
|
|
|
45
|
-
## Launch
|
|
45
|
+
## Launch
|
|
46
|
+
|
|
47
|
+
Select the pacer once for the multi-PR run (same helper as single-PR):
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
python "$HOME/.claude/skills/_shared/pr-loop/scripts/select_converge_pacer.py" \
|
|
51
|
+
--skill autoconverge \
|
|
52
|
+
--has-workflow <0|1> \
|
|
53
|
+
--has-schedule-wakeup <0|1>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `pacer=portable`
|
|
57
|
+
|
|
58
|
+
Do **not** call `Workflow`. For each PR worktree from pre-flight, run the
|
|
59
|
+
continuous portable driver in
|
|
60
|
+
[`../../_shared/pr-loop/portable-driver.md`](../../_shared/pr-loop/portable-driver.md)
|
|
61
|
+
(serial, or host fan-out when the host can isolate workers). One teardown per
|
|
62
|
+
PR after that PR reaches ready or a named blocker. Shared Copilot quota and
|
|
63
|
+
permission grants from multi-PR pre-flight still apply once.
|
|
64
|
+
|
|
65
|
+
### `pacer=workflow`
|
|
46
66
|
|
|
47
67
|
Call the `Workflow` tool against the fan-out script, passing the absolute path of
|
|
48
68
|
`converge.mjs` and one entry per PR:
|
|
@@ -74,7 +94,18 @@ Each record's `deferredPrs` is that PR's own list of draft hardening PRs, and
|
|
|
74
94
|
`allDeferredPrs` is every record's `deferredPrs` flattened into one list. The
|
|
75
95
|
top-level `converged` is true only when every PR converged.
|
|
76
96
|
|
|
77
|
-
## Multi-PR teardown
|
|
97
|
+
## Multi-PR teardown
|
|
98
|
+
|
|
99
|
+
### `pacer=portable`
|
|
100
|
+
|
|
101
|
+
For each PR worktree, run the single-PR **Teardown → pacer=portable** path in
|
|
102
|
+
[`SKILL.md`](../SKILL.md) (`pr-loop-lifecycle` Close, no Workflow journal
|
|
103
|
+
report). Write one durable handoff per PR with that PR's `--pr-number` and
|
|
104
|
+
resume command `/autoconverge <PR URL>`. Revoke project permissions once per
|
|
105
|
+
repository after every PR's close. Print one summary line per PR as
|
|
106
|
+
`#<prNumber>: <converged | blocked> — rounds <N>, final <finalSha>[, blocker <blocker>]`.
|
|
107
|
+
|
|
108
|
+
### `pacer=workflow` (on workflow completion)
|
|
78
109
|
|
|
79
110
|
Run the single-PR Teardown in [`SKILL.md`](../SKILL.md) once per entry in
|
|
80
111
|
`results`, using that PR's `owner`, `repo`, `prNumber`, and `finalSha`, and its
|
|
@@ -6,14 +6,17 @@ skill still runs teardown (revoke permissions, final report).
|
|
|
6
6
|
|
|
7
7
|
## Blockers (end the run short of ready)
|
|
8
8
|
|
|
9
|
-
- **Budget stop** — the workflow's `budget` API is the
|
|
10
|
-
usage target is set, `converge.mjs` checks
|
|
11
|
-
round and stops at the round boundary when
|
|
12
|
-
lenses + one fix commit + re-verify) does not
|
|
13
|
-
`blocker: "budget"` with the run id; resume with
|
|
9
|
+
- **Budget stop** — on `pacer=workflow`, the workflow's `budget` API is the
|
|
10
|
+
pacing signal: when a usage target is set, `converge.mjs` checks
|
|
11
|
+
`budget.remaining()` before each round and stops at the round boundary when
|
|
12
|
+
one full round (three parallel lenses + one fix commit + re-verify) does not
|
|
13
|
+
fit. The run returns `blocker: "budget"` with the run id; resume with
|
|
14
14
|
`Workflow({scriptPath, resumeFromRunId})` — completed rounds replay from the
|
|
15
15
|
journal. The workflow never starts a round the budget cannot finish: a
|
|
16
|
-
half-run round records nothing resumable and replays dirty.
|
|
16
|
+
half-run round records nothing resumable and replays dirty. On
|
|
17
|
+
`pacer=portable`, stop at a tick boundary when the session cannot cover a
|
|
18
|
+
full clean tick; write handoff and resume with `/autoconverge <PR URL>`
|
|
19
|
+
([`../../_shared/pr-loop/portable-driver.md`](../../_shared/pr-loop/portable-driver.md)).
|
|
17
20
|
- **Iteration cap** — 20 loop iterations pass without a full convergence-check
|
|
18
21
|
pass. The iteration counter increments on every pass through any phase, so a
|
|
19
22
|
convergence-check gate that no round can clear (for example a `mergeable_state`
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Contract checks: autoconverge selects a portable pacer when Workflow is absent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SKILL_ROOT = Path(__file__).resolve().parent
|
|
9
|
+
SKILL_MARKDOWN_PATH = SKILL_ROOT / "SKILL.md"
|
|
10
|
+
MULTI_PR_MARKDOWN_PATH = SKILL_ROOT / "reference" / "multi-pr.md"
|
|
11
|
+
PORTABLE_DRIVER_PATH = (
|
|
12
|
+
SKILL_ROOT.parent / "_shared" / "pr-loop" / "portable-driver.md"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
SELECT_CONVERGE_PACER_SCRIPT = "select_converge_pacer.py"
|
|
16
|
+
PACER_PORTABLE_TOKEN = "pacer=portable"
|
|
17
|
+
PACER_WORKFLOW_TOKEN = "pacer=workflow"
|
|
18
|
+
ABORT_ONLY_PHRASE = "autoconverge requires the Workflow tool"
|
|
19
|
+
PORTABLE_DRIVER_DOC = "portable-driver.md"
|
|
20
|
+
PORTABLE_DRIVER_LINK_PATTERN = re.compile(
|
|
21
|
+
r"\((?P<relative>(?:\.\./)+_shared/pr-loop/portable-driver\.md)\)"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_skill_selects_pacer_and_forbids_abort_only_workflow() -> None:
|
|
26
|
+
skill_text = SKILL_MARKDOWN_PATH.read_text(encoding="utf-8")
|
|
27
|
+
assert SELECT_CONVERGE_PACER_SCRIPT in skill_text
|
|
28
|
+
assert PACER_PORTABLE_TOKEN in skill_text
|
|
29
|
+
assert PACER_WORKFLOW_TOKEN in skill_text
|
|
30
|
+
assert PORTABLE_DRIVER_DOC in skill_text
|
|
31
|
+
assert ABORT_ONLY_PHRASE not in skill_text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_skill_portable_driver_links_resolve_on_disk() -> None:
|
|
35
|
+
skill_text = SKILL_MARKDOWN_PATH.read_text(encoding="utf-8")
|
|
36
|
+
all_relative_links = PORTABLE_DRIVER_LINK_PATTERN.findall(skill_text)
|
|
37
|
+
assert all_relative_links, "SKILL.md must link portable-driver.md"
|
|
38
|
+
for each_relative_link in all_relative_links:
|
|
39
|
+
resolved_path = (SKILL_MARKDOWN_PATH.parent / each_relative_link).resolve()
|
|
40
|
+
assert resolved_path.is_file(), (
|
|
41
|
+
f"portable-driver link must resolve on disk: "
|
|
42
|
+
f"{each_relative_link} -> {resolved_path}"
|
|
43
|
+
)
|
|
44
|
+
assert resolved_path == PORTABLE_DRIVER_PATH.resolve()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_multi_pr_documents_portable_launch_and_teardown() -> None:
|
|
48
|
+
multi_pr_text = MULTI_PR_MARKDOWN_PATH.read_text(encoding="utf-8")
|
|
49
|
+
assert PACER_PORTABLE_TOKEN in multi_pr_text
|
|
50
|
+
assert "Do **not** call `Workflow`" in multi_pr_text or (
|
|
51
|
+
"Do not call `Workflow`" in multi_pr_text
|
|
52
|
+
)
|
|
53
|
+
assert "pacer=portable" in multi_pr_text
|
|
54
|
+
assert PORTABLE_DRIVER_DOC in multi_pr_text
|
|
@@ -115,9 +115,21 @@ counts as approval.
|
|
|
115
115
|
|
|
116
116
|
### Step 2 — Hold and wait
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
Branch the hold on the caller's pacer (or on whether a durable wake surface is
|
|
119
|
+
present when the caller has not selected a pacer):
|
|
120
|
+
|
|
121
|
+
- **Native pacer** (`pacer=schedule_wakeup`, `pacer=workflow`, or
|
|
122
|
+
`ScheduleWakeup` present): arm `ScheduleWakeup` for a 45-minute deadline.
|
|
123
|
+
Where `ScheduleWakeup` is absent on that native path, use `send_later`.
|
|
124
|
+
- **Portable pacer** (`pacer=portable`, or no durable wake): do not call
|
|
125
|
+
`ScheduleWakeup` or `send_later`. Hold with an in-session deadline poll, or
|
|
126
|
+
write handoff and stop, per
|
|
127
|
+
[`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md).
|
|
128
|
+
Callers may override this step after the ntfy page and apply their own
|
|
129
|
+
portable hold.
|
|
130
|
+
|
|
131
|
+
The clock starts when the page reaches the user, which is the moment the script
|
|
132
|
+
exits zero.
|
|
121
133
|
|
|
122
134
|
- If the user answers, follow their direction.
|
|
123
135
|
- If the deadline passes with no answer, run the caller's normal teardown and
|
|
@@ -141,7 +153,7 @@ its fix re-runs that same check and posts the before/after output on the thread.
|
|
|
141
153
|
evidence note.
|
|
142
154
|
- [ ] The page carries the PR name, the per-finding summary, and the review URL.
|
|
143
155
|
- [ ] `scripts/notify_ntfy.py` exited zero before the 45-minute clock started.
|
|
144
|
-
- [ ] The
|
|
156
|
+
- [ ] The hold is armed from a delivered page (45-minute `ScheduleWakeup` / `send_later` on native pacer; portable in-session poll or handoff on `pacer=portable`).
|
|
145
157
|
- [ ] A failed page kept the gate open rather than approving the round.
|
|
146
158
|
|
|
147
159
|
## Files
|
|
@@ -18,13 +18,14 @@ Drives a draft PR to convergence by driving the internal passes to clean first
|
|
|
18
18
|
| `pr_converge_skill_constants/` | Importable constants module shared by skill scripts |
|
|
19
19
|
| `reference/` | Per-tick steps, convergence gates, fix protocol, obstacle runbooks, state schema, stop conditions, examples |
|
|
20
20
|
| `scripts/` | Python helpers (bugbot check, convergence check, Copilot review fetcher, fix-reply poster, reflow tool) and their tests |
|
|
21
|
-
| `workflows/` | ScheduleWakeup loop pacing specification |
|
|
21
|
+
| `workflows/` | ScheduleWakeup loop pacing specification (`pacer=schedule_wakeup`) |
|
|
22
22
|
|
|
23
23
|
## Conventions
|
|
24
24
|
|
|
25
|
-
-
|
|
25
|
+
- Pre-flight selects a pacer via `select_converge_pacer.py`: `schedule_wakeup` or `portable`. Missing `ScheduleWakeup` selects portable — it does not abort the skill.
|
|
26
|
+
- On `pacer=schedule_wakeup`, each invocation runs one tick and the next advances via `ScheduleWakeup`. On `pacer=portable`, the session runs ticks continuously (continue or in-session poll) until convergence, a stop condition, or a budget handoff.
|
|
26
27
|
- Loop state persists to `$CLAUDE_JOB_DIR/pr-converge-state.json` between ticks.
|
|
27
|
-
-
|
|
28
|
+
- Isolation uses `EnterWorktree` when present; otherwise the portable driver git worktree path, then `preflight_worktree.py`.
|
|
28
29
|
- All findings and PR reports state verified facts only — no hedging language.
|
|
29
30
|
- The GitHub MCP (`pull_request_read`, `pull_request_review_write`) is the primary path for PR inspection; `gh api` is the fallback.
|
|
30
31
|
- Three step-scoped agents (`fix_executor`, `thread_sweep`, `copilot_watch`) persist across ticks via the `persistent_agents` map in loop state; each tick resumes them with `SendMessage` and spawns a fresh named agent when a stored id is dead.
|
|
@@ -9,10 +9,13 @@ description: >-
|
|
|
9
9
|
|
|
10
10
|
# PR Converge
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
A code-review ↔ bugteam ↔ Bugbot ↔ Copilot loop on a draft PR: the internal
|
|
13
|
+
code-review and bugteam passes drive the code to clean first, then Cursor
|
|
14
|
+
Bugbot and Copilot run as terminal confirmation gates, until all are clean on
|
|
15
|
+
the same `HEAD` and mergeable. On `pacer=schedule_wakeup`, one tick runs per
|
|
16
|
+
invocation and the next tick is scheduled. On `pacer=portable`, ticks run
|
|
17
|
+
continuously in-session (see
|
|
18
|
+
[`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md)).
|
|
16
19
|
|
|
17
20
|
## Transport check (before any GitHub step)
|
|
18
21
|
|
|
@@ -24,20 +27,50 @@ skill through its substitution matrix.
|
|
|
24
27
|
|
|
25
28
|
## Pre-flight
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
for the literal string `ScheduleWakeup`. If you see it, proceed. If the
|
|
29
|
-
string is absent after scanning, report `pr-converge requires
|
|
30
|
-
ScheduleWakeup; aborting` and stop.
|
|
30
|
+
### Task list (step 1)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
After down/opt-out flags are known, build the work list with
|
|
33
|
+
`build_converge_task_list.py` only (same script as autoconverge). Register every
|
|
34
|
+
task; **final task** is always `all_runnable_reviews_clean_same_head`. Complete
|
|
35
|
+
that final task only when every runnable review is CLEAN on the same HEAD.
|
|
36
|
+
On `pacer=portable`, `open-run` returns the same list.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
### Pacer selection
|
|
39
|
+
|
|
40
|
+
Scan the tool list for `ScheduleWakeup`. Record whether it is present, then
|
|
41
|
+
select the pacer:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
python "$HOME/.claude/skills/_shared/pr-loop/scripts/select_converge_pacer.py" \
|
|
45
|
+
--skill pr-converge \
|
|
46
|
+
--has-workflow <0|1> \
|
|
47
|
+
--has-schedule-wakeup <0|1>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `pacer=schedule_wakeup` — native tick pacing via `ScheduleWakeup` (see
|
|
51
|
+
[`workflows/schedule-wakeup-loop.md`](workflows/schedule-wakeup-loop.md)).
|
|
52
|
+
- `pacer=portable` — continuous in-session driver on
|
|
53
|
+
[`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md).
|
|
54
|
+
**Do not abort** because `ScheduleWakeup` is missing.
|
|
55
|
+
|
|
56
|
+
### Worktree isolation
|
|
57
|
+
|
|
58
|
+
When the tool list includes `EnterWorktree`, call it with no arguments before
|
|
59
|
+
any API call, file read, or edit. Agent-view sessions start in the shared
|
|
60
|
+
checkout; Bash (`gh`, `git`) does not auto-isolate. Do not proceed until the
|
|
61
|
+
working directory contains `.claude/worktrees/`. If `EnterWorktree` fails,
|
|
62
|
+
report the failure and stop.
|
|
63
|
+
|
|
64
|
+
When `EnterWorktree` is absent, isolate with git worktree machinery per
|
|
65
|
+
[`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md)
|
|
66
|
+
§ Isolation and worktree, then run strict
|
|
67
|
+
`preflight_worktree.py` for the PR's owner/repo. Fail closed only when the
|
|
68
|
+
checkout is not the PR's repo on the PR head ref.
|
|
69
|
+
|
|
70
|
+
`EnterWorktree` (or the portable worktree path) isolates the session repo.
|
|
71
|
+
When the PR lives in a different repo, Step 1.5 routes the working directory
|
|
72
|
+
into a **PR worktree** of that repo on its head branch (routine, never a
|
|
73
|
+
pause). See [`reference/per-tick.md` § Step 1.5](reference/per-tick.md).
|
|
41
74
|
|
|
42
75
|
## Resume from a prior run
|
|
43
76
|
|
|
@@ -71,10 +104,16 @@ auto-fix; code-concern verify then confirmed / refuted / inconclusive).
|
|
|
71
104
|
- Self-healing and **confirmed** findings join the fix tick on `current_head`;
|
|
72
105
|
after push, reset clean-at SHAs, set `phase = CODE_REVIEW`, return to Step 5.
|
|
73
106
|
- **Refuted** findings resolve clean on the thread; no phase change.
|
|
74
|
-
- One or more **inconclusive** findings: do not mark ready.
|
|
75
|
-
skill's
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
- One or more **inconclusive** findings: do not mark ready. Page via the
|
|
108
|
+
triage skill's ntfy step, then hold by pacer:
|
|
109
|
+
- **`pacer=schedule_wakeup`** — triage user gate (`ScheduleWakeup` /
|
|
110
|
+
`send_later` 45-minute hold across ticks; persist the deadline so each tick
|
|
111
|
+
reads it on entry).
|
|
112
|
+
- **`pacer=portable`** — in-session deadline poll or handoff per
|
|
113
|
+
[`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md);
|
|
114
|
+
never `ScheduleWakeup` or `send_later`.
|
|
115
|
+
Act on the user's direction inside the window; on timeout, teardown and report
|
|
116
|
+
the findings un-reviewed.
|
|
78
117
|
- Enter `COPILOT_WAIT` only from gate (d) after requesting a Copilot review
|
|
79
118
|
(Step 7 → 7a). Stay on `COPILOT_WAIT` until a review surfaces at
|
|
80
119
|
`current_head`, `copilot_wait_count >= 3` hard-blocks, or `copilot_down`
|
|
@@ -106,11 +145,13 @@ softened.
|
|
|
106
145
|
|
|
107
146
|
Single-PR mode persists loop state to `$CLAUDE_JOB_DIR/pr-converge-state.json`.
|
|
108
147
|
On tick entry, read this file if it exists to restore phase, tick_count, and
|
|
109
|
-
clean-at SHAs. On tick exit, write updated state before
|
|
110
|
-
|
|
148
|
+
clean-at SHAs. On tick exit, write updated state before the next pacer step
|
|
149
|
+
(`ScheduleWakeup` when `pacer=schedule_wakeup`; continuous continue or
|
|
150
|
+
in-session poll when `pacer=portable`) so the next tick resumes with accurate
|
|
151
|
+
state.
|
|
111
152
|
|
|
112
|
-
After the state write and before
|
|
113
|
-
fresh session in a new job can resume this run:
|
|
153
|
+
After the state write and before the next pacer step, write the durable handoff
|
|
154
|
+
so a fresh session in a new job can resume this run:
|
|
114
155
|
|
|
115
156
|
```
|
|
116
157
|
python "$HOME/.claude/skills/_shared/pr-loop/scripts/write_handoff.py" \
|
|
@@ -171,8 +212,12 @@ grows to 5 or more items, suggest spinning up a subagent to investigate, fix,
|
|
|
171
212
|
post a fresh PR in a fresh branch based on origin main to the user.
|
|
172
213
|
|
|
173
214
|
- **`ScheduleWakeup` not in subagent tool registries** — background
|
|
174
|
-
`general-purpose` tick cannot schedule re-entry; only parent session
|
|
175
|
-
with `ScheduleWakeup`
|
|
215
|
+
`general-purpose` tick cannot schedule re-entry; only the parent session
|
|
216
|
+
with `ScheduleWakeup` (or the portable continuous driver on the parent)
|
|
217
|
+
may pace the next tick.
|
|
218
|
+
- **Portable host has no durable wake outside the session** — when
|
|
219
|
+
`pacer=portable`, budget and context bound how many ticks complete; write
|
|
220
|
+
handoff at a tick boundary and resume with `/pr-converge <PR URL>`.
|
|
176
221
|
- **`state.json` without §Concurrency lock loses merges** when teammates
|
|
177
222
|
finish in same wall-clock window.
|
|
178
223
|
- **`tick_count` must not double-increment** — conversation state line
|
|
@@ -219,22 +264,168 @@ post a fresh PR in a fresh branch based on origin main to the user.
|
|
|
219
264
|
|
|
220
265
|
## Progress checklist
|
|
221
266
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
267
|
+
State variables (`phase`, `bugbot_clean_at`, `code_review_clean_at`,
|
|
268
|
+
`bugteam_clean_at`, `copilot_clean_at`, `merge_state_status`, counters) are
|
|
269
|
+
defined in [`reference/state-schema.md`](reference/state-schema.md). Ground rules
|
|
270
|
+
in [`reference/ground-rules.md`](reference/ground-rules.md).
|
|
271
|
+
|
|
272
|
+
Each step references its spoke file for full procedural detail — MCP calls,
|
|
273
|
+
script invocations, decision criteria. Every "return to Step N" means the next
|
|
274
|
+
tick starts fresh from that step.
|
|
275
|
+
|
|
276
|
+
**Hard gate: do not advance from any step while ANY unresolved review
|
|
277
|
+
thread exists on the PR.** Sweep semantics and per-thread handling live in
|
|
278
|
+
the shared fix protocol
|
|
279
|
+
([`../../_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md)
|
|
280
|
+
step 12; skill deltas in [`reference/fix-protocol.md`](reference/fix-protocol.md)).
|
|
281
|
+
|
|
282
|
+
**Full-PR-diff rule: every CODE-REVIEW round (Step 5) and every BUGTEAM
|
|
283
|
+
round (Step 6) covers the FULL `origin/main...HEAD` diff — every file
|
|
284
|
+
the PR touches.** A round that scopes to a subset — only the last commit,
|
|
285
|
+
only files touched since the prior clean SHA, only bugbot-flagged paths,
|
|
286
|
+
or any other delta cut — does not satisfy the gate, and a "clean" verdict
|
|
287
|
+
against a partial diff is not a valid clean. Re-run the round against the
|
|
288
|
+
full diff before recording `code_review_clean_at` or treating the bugteam
|
|
289
|
+
round as converged. This rule holds every tick, every loop, every PR.
|
|
290
|
+
|
|
291
|
+
- [ ] **Step 0: Open the run**
|
|
292
|
+
Apply the `pr-loop-lifecycle` skill's Open section
|
|
293
|
+
(`../pr-loop-lifecycle/SKILL.md`): permission grant + worktree preflight.
|
|
294
|
+
|
|
295
|
+
- [ ] **Step 1: Resolve PR scope + PR worktree**
|
|
296
|
+
Apply Step 1.5 scope resolution in
|
|
297
|
+
[`reference/per-tick.md`](reference/per-tick.md). Resolve the **PR
|
|
298
|
+
worktree** — the local checkout every local step this tick targets:
|
|
299
|
+
the `EnterWorktree` checkout when the PR shares the session's repo,
|
|
300
|
+
else a checkout of the PR's repo that the working directory routes
|
|
301
|
+
into via `cd`. Cross-repo routing is automatic, not a fork.
|
|
302
|
+
See: [`reference/per-tick.md` § Step 1](reference/per-tick.md)
|
|
303
|
+
and [§ Step 1.5](reference/per-tick.md)
|
|
304
|
+
|
|
305
|
+
- [ ] **Step 2: Initialize loop state**
|
|
306
|
+
`phase = CODE_REVIEW`; all counters at zero; `run_name` resolved.
|
|
307
|
+
|
|
308
|
+
- [ ] **Step 3: Mergeability check**
|
|
309
|
+
See: [`reference/convergence-gates.md` § (c)](reference/convergence-gates.md)
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
pull_request_read(method="get") → .mergeable_state, .mergeable
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- [ ] mergeable → advance to Step 4
|
|
316
|
+
- [ ] not mergeable → rebase → force-push → return to Step 1
|
|
317
|
+
|
|
318
|
+
- [ ] **Step 4: BUGBOT — terminal Bugbot confirmation gate**
|
|
319
|
+
Step 6 routes here after BUGTEAM converges. Bugbot confirms code the
|
|
320
|
+
internal passes already drove to clean.
|
|
321
|
+
See: [`reference/per-tick.md` § BUGBOT terminal gate + Step 3](reference/per-tick.md);
|
|
322
|
+
availability and trigger via `reviewer-gates`
|
|
323
|
+
(`../reviewer-gates/SKILL.md` § Gate 1 / Gate 3).
|
|
324
|
+
|
|
325
|
+
- [ ] **disabled / down** → `bugbot_down = true` → Step 7
|
|
326
|
+
- [ ] **dirty on `current_head`** → apply shared fix protocol
|
|
327
|
+
([`../../_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md);
|
|
328
|
+
skill deltas in [`reference/fix-protocol.md`](reference/fix-protocol.md))
|
|
329
|
+
→ push → reset push-invalidated markers → `phase = CODE_REVIEW` → Step 5
|
|
330
|
+
- [ ] **clean on `current_head`** → zero unresolved threads (else fix + resolve first)
|
|
331
|
+
→ `bugbot_clean_at = current_head` → Step 7
|
|
332
|
+
- [ ] **no review / commit_id mismatch** → `reviewer-gates` Bugbot flow (Gate 3):
|
|
333
|
+
silent pass → stamp + Step 7; queued/triggered → ScheduleWakeup 360s or portable in-session poll → Step 4;
|
|
334
|
+
down → `bugbot_down = true` → Step 7
|
|
335
|
+
|
|
336
|
+
- [ ] **Step 5: CODE-REVIEW — static sweep, review, fix, advance**
|
|
337
|
+
Entry phase every tick; re-entered after any fix push.
|
|
338
|
+
See: [`reference/per-tick.md` § CODE_REVIEW entry](reference/per-tick.md).
|
|
339
|
+
Pre-condition: cwd is the Step 1.5 PR worktree on `current_head`.
|
|
340
|
+
Scope: FULL `origin/main...HEAD` diff every tick (no path args, no delta cut).
|
|
341
|
+
Review always runs at effort high on model opus through
|
|
342
|
+
`invoke_code_review.py`. Mode decision inputs: host profile + session
|
|
343
|
+
model. Call:
|
|
344
|
+
`python "$HOME/.claude/scripts/invoke_code_review.py" --cwd <PR-worktree>
|
|
345
|
+
--session-model <alias>`. Chain mode uses that cwd and empty stdin; the
|
|
346
|
+
chain process never commits and never pushes. JSON stdout carries
|
|
347
|
+
`mode` (`in_session` | `chain`), `served_command`, `returncode`, and
|
|
348
|
+
`dirty_tree`. Config/host errors still emit that JSON with non-zero
|
|
349
|
+
`returncode` (no traceback-only failure).
|
|
350
|
+
|
|
351
|
+
- [ ] **Static sweep fails** → apply shared fix protocol → push → reset markers
|
|
352
|
+
→ stay CODE_REVIEW → Step 5
|
|
353
|
+
- [ ] **`mode == in_session`** (Claude host, session model opus) → run
|
|
354
|
+
`/code-review high --fix` in-session (no path args)
|
|
355
|
+
- [ ] **`mode == chain`** (any other host or non-opus session) → helper
|
|
356
|
+
already ran the headless review; read `returncode`,
|
|
357
|
+
`served_command`, and `dirty_tree` from JSON
|
|
358
|
+
- [ ] **failed review** (`returncode != 0`, or chain with null
|
|
359
|
+
`served_command`) → do not set `code_review_clean_at` → stay
|
|
360
|
+
CODE_REVIEW → Step 5
|
|
361
|
+
- [ ] **fixes applied** (`dirty_tree` true / working tree dirty) →
|
|
362
|
+
commit + push via shared fix protocol → reset markers → stay
|
|
363
|
+
CODE_REVIEW → Step 5
|
|
364
|
+
- [ ] **clean** (successful serve: `returncode == 0`, chain
|
|
365
|
+
`served_command` non-null when chain, and `dirty_tree` false) →
|
|
366
|
+
zero unresolved threads (else fix + resolve) →
|
|
367
|
+
`code_review_clean_at = current_head` → `phase = BUGTEAM` → Step 6
|
|
368
|
+
|
|
369
|
+
- [ ] **Step 6: BUGTEAM — run, decide, fix, reply, resolve**
|
|
370
|
+
See: [`reference/per-tick.md` § Step 2 BUGTEAM](reference/per-tick.md);
|
|
371
|
+
[`../bugteam/SKILL.md`](../bugteam/SKILL.md).
|
|
372
|
+
Pre-condition: `code_review_clean_at == current_head`.
|
|
373
|
+
Mandatory: `Skill({skill: "bugteam", args: "<PR URL>"})` this tick
|
|
374
|
+
(enforcer-blocked otherwise; `qbug` is not a substitute). Scope: FULL
|
|
375
|
+
`origin/main...HEAD` diff. Re-resolve HEAD after bugteam.
|
|
376
|
+
|
|
377
|
+
- [ ] **bugteam pushed** → verify threads replied + resolved → reset markers
|
|
378
|
+
→ `phase = CODE_REVIEW` → ScheduleWakeup 360s or portable in-session poll → Step 5
|
|
379
|
+
- [ ] **converged, no push** → zero unresolved threads →
|
|
380
|
+
`bugteam_clean_at = current_head` → `phase = BUGBOT` → Step 4
|
|
381
|
+
- [ ] **findings without committed fixes** → apply shared fix protocol → push →
|
|
382
|
+
reset markers → `phase = CODE_REVIEW` → Step 5
|
|
383
|
+
|
|
384
|
+
- [ ] **Step 7: Convergence gates**
|
|
385
|
+
Full procedure: [`reference/convergence-gates.md`](reference/convergence-gates.md).
|
|
386
|
+
|
|
387
|
+
Pre-condition: Step 6 converged AND (`bugbot_clean_at == current_head` OR
|
|
388
|
+
`bugbot_down`). The terminal Bugbot gate (Step 4) sets that state just
|
|
389
|
+
before these gates run. Count unresolved threads before each gate.
|
|
390
|
+
Every gate records evidence; gate (f) cites evidence from (a)–(e).
|
|
391
|
+
|
|
392
|
+
- [ ] **(a) Copilot findings** — fetch Copilot on `current_head`; dirty → fix + return to Step 5; clean → stamp `copilot_clean_at`; absent → continue; when `copilot_down`, skip
|
|
393
|
+
- [ ] **(b) Claude reviewer** — fetch Claude on `current_head`; dirty → fix + return to Step 5; clean or absent → continue
|
|
394
|
+
- [ ] **(c) Mergeability** — `mergeable_state == "clean"` and `mergeable == true`; dirty → rebase + return to Step 1; blocked/behind/unknown/unstable → hard blocker
|
|
395
|
+
- [ ] **(d) Post-convergence Copilot request** — request Copilot when not pending and not `copilot_down`; enter `COPILOT_WAIT` (Step 7a); when `copilot_down`, skip to (e)
|
|
396
|
+
- [ ] **(e) Thread-resolution** — zero unresolved threads across the PR; else sweep + fix/resolve
|
|
397
|
+
- [ ] **(f) Mark ready** — run `check_convergence.py`; exit 0 → `update_pull_request(draft=false)` → Step 8; exit 1 → fix path; exit 2 → retry/stop
|
|
398
|
+
|
|
399
|
+
- [ ] **Step 7a: COPILOT_WAIT — fetch Copilot, decide**
|
|
400
|
+
See: [`reference/per-tick.md` § Step 2 COPILOT_WAIT](reference/per-tick.md).
|
|
401
|
+
Skipped entirely when `copilot_down` (gate (d) never enters this phase).
|
|
402
|
+
|
|
403
|
+
- [ ] **clean** → `copilot_clean_at = current_head` → Step 7 (re-validate (b), (c), (e), (f))
|
|
404
|
+
- [ ] **dirty** → apply shared fix protocol
|
|
405
|
+
([`../../_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md);
|
|
406
|
+
skill deltas in [`reference/fix-protocol.md`](reference/fix-protocol.md))
|
|
407
|
+
→ push → reset markers → `phase = CODE_REVIEW` → Step 5
|
|
408
|
+
- [ ] **no review yet** → increment `copilot_wait_count` → ≥ 3 hard-blocks;
|
|
409
|
+
else ScheduleWakeup 360s or portable in-session poll → Step 7a next tick
|
|
410
|
+
|
|
411
|
+
- [ ] **Step 8: Clean working tree**
|
|
412
|
+
`pr-loop-lifecycle` Close (`../pr-loop-lifecycle/SKILL.md`).
|
|
413
|
+
See: [`pr-loop-lifecycle/reference/teardown-publish-permissions.md` § Clean working tree](../pr-loop-lifecycle/reference/teardown-publish-permissions.md)
|
|
414
|
+
|
|
415
|
+
- [ ] **Step 9: Rewrite PR description**
|
|
416
|
+
`pr-loop-lifecycle` Close.
|
|
417
|
+
See: [`pr-loop-lifecycle/reference/teardown-publish-permissions.md` § Publish the final PR description](../pr-loop-lifecycle/reference/teardown-publish-permissions.md)
|
|
418
|
+
|
|
419
|
+
- [ ] **Step 10: Revoke project permissions (always)**
|
|
420
|
+
`pr-loop-lifecycle` Close (`../pr-loop-lifecycle/SKILL.md` § Close).
|
|
421
|
+
|
|
422
|
+
- [ ] **Step 11: Print final report**
|
|
423
|
+
Print this block verbatim — no paraphrase, no extra commentary:
|
|
424
|
+
```
|
|
425
|
+
/pr-converge exit: converged
|
|
426
|
+
Loops: <N>
|
|
427
|
+
Final commit: <SHA>
|
|
428
|
+
```
|
|
238
429
|
|
|
239
430
|
## Edge cases
|
|
240
431
|
|
|
@@ -253,6 +444,7 @@ Two hard gates bind every step:
|
|
|
253
444
|
| `copilot-finding-triage` | Tier, verify, and route each Copilot finding |
|
|
254
445
|
| `bugteam` | Full-diff bug audit in Step 6 (Skill invocation mandatory) |
|
|
255
446
|
| `pr-loop-cloud-transport` | When `gh` is unavailable or unauthenticated |
|
|
447
|
+
| [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md) | Continuous in-session pacer when `ScheduleWakeup` is absent |
|
|
256
448
|
| [`../../_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md) | Shared 13-step fix sequence (TDD, commit, push, reply, resolve) |
|
|
257
449
|
| [`reference/fix-protocol.md`](reference/fix-protocol.md) | pr-converge deltas on the shared fix protocol |
|
|
258
450
|
|
|
@@ -263,7 +455,6 @@ Two hard gates bind every step:
|
|
|
263
455
|
| `SKILL.md` | Hub: pre-flight, state, progress checklist, gotchas |
|
|
264
456
|
| `CLAUDE.md` | Package map for agents working in this skill |
|
|
265
457
|
| `pr_converge_skill_constants/` | Importable constants for skill scripts |
|
|
266
|
-
| `reference/progress-checklist.md` | Per-tick eleven-step checklist (Step 0 open → Step 11 report) |
|
|
267
458
|
| `reference/per-tick.md` | Full per-tick procedure (phases, cwd routing, pacing) |
|
|
268
459
|
| `reference/convergence-gates.md` | Six gates before mark-ready |
|
|
269
460
|
| `reference/fix-protocol.md` | pr-converge fix-protocol deltas |
|
|
@@ -274,4 +465,5 @@ Two hard gates bind every step:
|
|
|
274
465
|
| `reference/examples.md` | Worked tick sequences |
|
|
275
466
|
| `reference/obstacles/` | Per-obstacle fix runbooks |
|
|
276
467
|
| `scripts/` | Convergence helpers, Copilot fetch, fix-reply poster, tests |
|
|
277
|
-
| `workflows/schedule-wakeup-loop.md` | ScheduleWakeup pacing |
|
|
468
|
+
| `workflows/schedule-wakeup-loop.md` | ScheduleWakeup pacing (`pacer=schedule_wakeup`) |
|
|
469
|
+
| [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md) | Portable continuous pacer (`pacer=portable`) |
|
|
@@ -210,7 +210,11 @@ When bugfix (clean-coder) subagent completes after push:
|
|
|
210
210
|
3. Each PR with new subagent results → spawn next agent per rules, all
|
|
211
211
|
in one parallel message.
|
|
212
212
|
4. Re-read `state.json` if needed for scheduling.
|
|
213
|
-
5.
|
|
213
|
+
5. Advance the pacer: on `pacer=schedule_wakeup`, call `ScheduleWakeup`
|
|
214
|
+
with the appropriate delay; on `pacer=portable`, continue or poll
|
|
215
|
+
in-session per
|
|
216
|
+
[`../../_shared/pr-loop/portable-driver.md`](../../_shared/pr-loop/portable-driver.md)
|
|
217
|
+
(no `ScheduleWakeup`).
|
|
214
218
|
6. Nothing else.
|
|
215
219
|
|
|
216
220
|
## Memory
|