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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/skills/_shared/pr-loop/CLAUDE.md +18 -13
  3. package/skills/_shared/pr-loop/portable-driver.md +150 -0
  4. package/skills/_shared/pr-loop/scripts/CLAUDE.md +3 -0
  5. package/skills/_shared/pr-loop/scripts/build_converge_task_list.py +310 -0
  6. package/skills/_shared/pr-loop/scripts/portable_converge_driver.py +1637 -0
  7. package/skills/_shared/pr-loop/scripts/select_converge_pacer.py +215 -0
  8. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +3 -0
  9. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +56 -0
  10. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +47 -0
  11. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +181 -0
  12. package/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +140 -0
  13. package/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +1119 -0
  14. package/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +207 -0
  15. package/skills/autoconverge/CLAUDE.md +5 -3
  16. package/skills/autoconverge/SKILL.md +185 -74
  17. package/skills/autoconverge/reference/convergence.md +5 -5
  18. package/skills/autoconverge/reference/multi-pr.md +33 -2
  19. package/skills/autoconverge/reference/stop-conditions.md +9 -6
  20. package/skills/autoconverge/test_portable_pacer_gate.py +54 -0
  21. package/skills/copilot-finding-triage/SKILL.md +16 -4
  22. package/skills/pr-converge/CLAUDE.md +4 -3
  23. package/skills/pr-converge/SKILL.md +236 -44
  24. package/skills/pr-converge/reference/multi-pr-orchestration.md +5 -1
  25. package/skills/pr-converge/reference/per-tick.md +51 -24
  26. package/skills/pr-converge/test_portable_pacer_gate.py +67 -0
  27. 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 the multi-PR workflow
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 (on workflow completion)
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 pacing signal: when a
10
- usage target is set, `converge.mjs` checks `budget.remaining()` before each
11
- round and stops at the round boundary when one full round (three parallel
12
- lenses + one fix commit + re-verify) does not fit. The run returns
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
- Arm `ScheduleWakeup` for a 45-minute deadline. Where `ScheduleWakeup` is absent,
119
- use `send_later`. The clock starts when the page reaches the user, which is the
120
- moment the script exits zero.
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 wakeup is armed for 45 minutes from a delivered page.
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
- - Each invocation runs one tick. The next tick is scheduled via `ScheduleWakeup` unless convergence or a stop condition has been reached.
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
- - The pre-flight gate requires `EnterWorktree` before any file read or API call, and confirms `ScheduleWakeup` is in the tool list.
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
- One tick per invocation. A code-review ↔ bugteam ↔ Bugbot ↔ Copilot loop on
13
- a draft PR: the internal code-review and bugteam passes drive the code to clean
14
- first, then Cursor Bugbot and Copilot run as terminal confirmation gates, until
15
- all are clean on the same `HEAD` and mergeable.
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
- Pause and physically scan the tool list at the top of this conversation
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
- Call `EnterWorktree` with no arguments before any API call, file read, or
33
- edit. Agent-view sessions start in the shared checkout; Bash (`gh`, `git`)
34
- does not auto-isolate. Do not proceed until the working directory contains
35
- `.claude/worktrees/`. If `EnterWorktree` fails, report the failure and stop.
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
- `EnterWorktree` isolates the session repo. When the PR lives in a different
38
- repo, Step 1.5 routes the working directory into a **PR worktree** of that
39
- repo on its head branch (routine, never a pause). See
40
- [`reference/per-tick.md` § Step 1.5](reference/per-tick.md).
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. Run the triage
75
- skill's user gate (ntfy + 45-minute hold across ticks; persist the deadline so
76
- each tick reads it on entry). Act on the user's direction inside the window;
77
- on timeout, teardown and report the findings un-reviewed.
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 calling ScheduleWakeup
110
- so the next tick resumes with accurate state.
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 ScheduleWakeup, write the durable handoff so a
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` in registry can call it.
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
- Run the eleven-step per-tick sequence in
223
- [`reference/progress-checklist.md`](reference/progress-checklist.md): Step 0
224
- (open the run) through Step 11 (final report), each step pointing to its spoke
225
- file for MCP calls, script invocations, and decision criteria. State variables
226
- live in [`reference/state-schema.md`](reference/state-schema.md); ground rules in
227
- [`reference/ground-rules.md`](reference/ground-rules.md).
228
-
229
- Two hard gates bind every step:
230
-
231
- - **No unresolved threads.** Do not advance from any step while any unresolved
232
- review thread exists on the PR (sweep semantics in the shared fix protocol,
233
- [`../../_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md)
234
- step 12).
235
- - **Full-PR-diff.** Every CODE-REVIEW (Step 5) and BUGTEAM (Step 6) round covers
236
- the full `origin/main...HEAD` diff — a "clean" verdict against a partial diff
237
- is not a valid clean.
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. Call `ScheduleWakeup` with appropriate delay.
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