claude-dev-env 2.1.0 → 2.2.1

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 (44) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/scripts/pyproject.toml +22 -0
  3. package/hooks/blocking/code_rules_shared.py +4 -2
  4. package/hooks/blocking/pii_payload_scan.py +78 -20
  5. package/hooks/blocking/pii_prevention_blocker.py +3 -1
  6. package/hooks/blocking/test_pii_write_surface_ephemeral.py +221 -0
  7. package/hooks/validators/ruff_integration.py +2 -1
  8. package/hooks/validators/run_all_validators.py +460 -24
  9. package/hooks/validators/test_ruff_integration.py +21 -0
  10. package/hooks/validators/test_run_all_validators_pretooluse.py +223 -3
  11. package/package.json +1 -1
  12. package/rules/CLAUDE.md +1 -0
  13. package/rules/cleanup-command-forms.md +23 -0
  14. package/scripts/check.ps1 +32 -6
  15. package/skills/_shared/pr-loop/CLAUDE.md +18 -13
  16. package/skills/_shared/pr-loop/portable-driver.md +150 -0
  17. package/skills/_shared/pr-loop/scripts/CLAUDE.md +3 -0
  18. package/skills/_shared/pr-loop/scripts/build_converge_task_list.py +310 -0
  19. package/skills/_shared/pr-loop/scripts/portable_converge_driver.py +1637 -0
  20. package/skills/_shared/pr-loop/scripts/select_converge_pacer.py +215 -0
  21. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +3 -0
  22. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +56 -0
  23. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +47 -0
  24. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +181 -0
  25. package/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +140 -0
  26. package/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +1119 -0
  27. package/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +207 -0
  28. package/skills/autoconverge/CLAUDE.md +5 -3
  29. package/skills/autoconverge/SKILL.md +185 -74
  30. package/skills/autoconverge/reference/convergence.md +5 -5
  31. package/skills/autoconverge/reference/multi-pr.md +33 -2
  32. package/skills/autoconverge/reference/stop-conditions.md +9 -6
  33. package/skills/autoconverge/test_portable_pacer_gate.py +54 -0
  34. package/skills/copilot-finding-triage/SKILL.md +16 -4
  35. package/skills/pr-converge/CLAUDE.md +4 -3
  36. package/skills/pr-converge/SKILL.md +236 -44
  37. package/skills/pr-converge/reference/multi-pr-orchestration.md +5 -1
  38. package/skills/pr-converge/reference/per-tick.md +51 -24
  39. package/skills/pr-converge/test_portable_pacer_gate.py +67 -0
  40. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -3
  41. package/skills/usage-pause/SKILL.md +5 -4
  42. package/skills/usage-pause/scripts/resolve_usage_window.py +68 -13
  43. package/skills/usage-pause/scripts/test_resolve_usage_window.py +121 -9
  44. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +7 -3
@@ -0,0 +1,207 @@
1
+ """Tests for select_converge_pacer — host tool surface → pacer mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
13
+ if str(_SCRIPTS_DIR) not in sys.path:
14
+ sys.path.insert(0, str(_SCRIPTS_DIR))
15
+
16
+ import select_converge_pacer as pacer_module # noqa: E402
17
+ from skills_pr_loop_constants.pacer_constants import ( # noqa: E402
18
+ CLI_HAS_SCHEDULE_WAKEUP_FLAG,
19
+ CLI_HAS_WORKFLOW_FLAG,
20
+ CLI_SKILL_FLAG,
21
+ ENTRY_SKILL_AUTOCONVERGE,
22
+ ENTRY_SKILL_PR_CONVERGE,
23
+ EXIT_SUCCESS,
24
+ EXIT_USAGE_ERROR,
25
+ PACER_PORTABLE,
26
+ PACER_SCHEDULE_WAKEUP,
27
+ PACER_WORKFLOW,
28
+ RESULT_KEY_ENTRY_SKILL,
29
+ RESULT_KEY_HAS_SCHEDULE_WAKEUP,
30
+ RESULT_KEY_HAS_WORKFLOW,
31
+ RESULT_KEY_PACER,
32
+ )
33
+
34
+ SCRIPT_PATH = _SCRIPTS_DIR / "select_converge_pacer.py"
35
+
36
+
37
+ def test_autoconverge_with_workflow_selects_workflow() -> None:
38
+ selection = pacer_module.select_converge_pacer(
39
+ entry_skill=ENTRY_SKILL_AUTOCONVERGE,
40
+ has_workflow=True,
41
+ has_schedule_wakeup=False,
42
+ )
43
+ assert selection.pacer == PACER_WORKFLOW
44
+ assert selection.entry_skill == ENTRY_SKILL_AUTOCONVERGE
45
+
46
+
47
+ def test_autoconverge_without_workflow_selects_portable() -> None:
48
+ selection = pacer_module.select_converge_pacer(
49
+ entry_skill=ENTRY_SKILL_AUTOCONVERGE,
50
+ has_workflow=False,
51
+ has_schedule_wakeup=True,
52
+ )
53
+ assert selection.pacer == PACER_PORTABLE
54
+
55
+
56
+ def test_pr_converge_with_schedule_wakeup_selects_schedule_wakeup() -> None:
57
+ selection = pacer_module.select_converge_pacer(
58
+ entry_skill=ENTRY_SKILL_PR_CONVERGE,
59
+ has_workflow=False,
60
+ has_schedule_wakeup=True,
61
+ )
62
+ assert selection.pacer == PACER_SCHEDULE_WAKEUP
63
+ assert selection.entry_skill == ENTRY_SKILL_PR_CONVERGE
64
+
65
+
66
+ def test_pr_converge_without_schedule_wakeup_selects_portable() -> None:
67
+ selection = pacer_module.select_converge_pacer(
68
+ entry_skill=ENTRY_SKILL_PR_CONVERGE,
69
+ has_workflow=True,
70
+ has_schedule_wakeup=False,
71
+ )
72
+ assert selection.pacer == PACER_PORTABLE
73
+
74
+
75
+ def test_third_party_surface_selects_portable_for_both_entries() -> None:
76
+ for each_skill in (ENTRY_SKILL_AUTOCONVERGE, ENTRY_SKILL_PR_CONVERGE):
77
+ selection = pacer_module.select_converge_pacer(
78
+ entry_skill=each_skill,
79
+ has_workflow=False,
80
+ has_schedule_wakeup=False,
81
+ )
82
+ assert selection.pacer == PACER_PORTABLE, each_skill
83
+
84
+
85
+ def test_unknown_entry_skill_raises() -> None:
86
+ with pytest.raises(ValueError, match="entry skill must be one of"):
87
+ pacer_module.select_converge_pacer(
88
+ entry_skill="bugteam",
89
+ has_workflow=False,
90
+ has_schedule_wakeup=False,
91
+ )
92
+
93
+
94
+ def test_parse_bool_flag_accepts_common_tokens() -> None:
95
+ assert pacer_module.parse_bool_flag("1") is True
96
+ assert pacer_module.parse_bool_flag("true") is True
97
+ assert pacer_module.parse_bool_flag("YES") is True
98
+ assert pacer_module.parse_bool_flag("0") is False
99
+ assert pacer_module.parse_bool_flag("false") is False
100
+ assert pacer_module.parse_bool_flag("OFF") is False
101
+
102
+
103
+ def test_parse_bool_flag_rejects_unknown_token() -> None:
104
+ with pytest.raises(ValueError, match="boolean token"):
105
+ pacer_module.parse_bool_flag("maybe")
106
+
107
+
108
+ def test_selection_as_json_dict_uses_stable_keys() -> None:
109
+ selection = pacer_module.select_converge_pacer(
110
+ entry_skill=ENTRY_SKILL_PR_CONVERGE,
111
+ has_workflow=False,
112
+ has_schedule_wakeup=False,
113
+ )
114
+ payload = pacer_module.selection_as_json_dict(selection)
115
+ assert payload[RESULT_KEY_PACER] == PACER_PORTABLE
116
+ assert payload[RESULT_KEY_ENTRY_SKILL] == ENTRY_SKILL_PR_CONVERGE
117
+ assert payload[RESULT_KEY_HAS_WORKFLOW] is False
118
+ assert payload[RESULT_KEY_HAS_SCHEDULE_WAKEUP] is False
119
+
120
+
121
+ def test_cli_prints_json_and_exits_zero() -> None:
122
+ completed = subprocess.run(
123
+ [
124
+ sys.executable,
125
+ str(SCRIPT_PATH),
126
+ "--skill",
127
+ ENTRY_SKILL_AUTOCONVERGE,
128
+ "--has-workflow",
129
+ "0",
130
+ "--has-schedule-wakeup",
131
+ "0",
132
+ ],
133
+ check=False,
134
+ capture_output=True,
135
+ text=True,
136
+ )
137
+ assert completed.returncode == EXIT_SUCCESS, completed.stderr
138
+ payload = json.loads(completed.stdout)
139
+ assert payload[RESULT_KEY_PACER] == PACER_PORTABLE
140
+ assert payload[RESULT_KEY_ENTRY_SKILL] == ENTRY_SKILL_AUTOCONVERGE
141
+
142
+
143
+ def test_cli_rejects_bad_bool_with_usage_exit() -> None:
144
+ completed = subprocess.run(
145
+ [
146
+ sys.executable,
147
+ str(SCRIPT_PATH),
148
+ "--skill",
149
+ ENTRY_SKILL_PR_CONVERGE,
150
+ "--has-workflow",
151
+ "nope",
152
+ "--has-schedule-wakeup",
153
+ "0",
154
+ ],
155
+ check=False,
156
+ capture_output=True,
157
+ text=True,
158
+ )
159
+ assert completed.returncode == EXIT_USAGE_ERROR
160
+ assert "boolean token" in completed.stderr
161
+
162
+
163
+ def test_main_returns_usage_error_on_value_error(
164
+ monkeypatch: pytest.MonkeyPatch,
165
+ ) -> None:
166
+ def raise_value_error(**_kwargs: object) -> object:
167
+ raise ValueError("entry skill must be one of")
168
+
169
+ monkeypatch.setattr(
170
+ pacer_module, "select_converge_pacer", raise_value_error
171
+ )
172
+ exit_code = pacer_module.main(
173
+ [
174
+ "--skill",
175
+ ENTRY_SKILL_PR_CONVERGE,
176
+ "--has-workflow",
177
+ "0",
178
+ "--has-schedule-wakeup",
179
+ "0",
180
+ ]
181
+ )
182
+ assert exit_code == EXIT_USAGE_ERROR
183
+
184
+
185
+ def test_build_argument_parser_requires_skill_and_host_flags() -> None:
186
+ parser = pacer_module.build_argument_parser()
187
+ all_option_strings = {
188
+ each_option
189
+ for each_action in parser._actions
190
+ for each_option in each_action.option_strings
191
+ }
192
+ assert CLI_SKILL_FLAG in all_option_strings
193
+ assert CLI_HAS_WORKFLOW_FLAG in all_option_strings
194
+ assert CLI_HAS_SCHEDULE_WAKEUP_FLAG in all_option_strings
195
+ parsed_namespace = parser.parse_args(
196
+ [
197
+ CLI_SKILL_FLAG,
198
+ ENTRY_SKILL_AUTOCONVERGE,
199
+ CLI_HAS_WORKFLOW_FLAG,
200
+ "1",
201
+ CLI_HAS_SCHEDULE_WAKEUP_FLAG,
202
+ "0",
203
+ ]
204
+ )
205
+ assert parsed_namespace.skill == ENTRY_SKILL_AUTOCONVERGE
206
+ assert parsed_namespace.has_workflow == "1"
207
+ assert parsed_namespace.has_schedule_wakeup == "0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Trigger:** `/autoconverge`, "autoconverge this PR", "converge this PR in one run", "run the converge workflow", "drive the PR to ready autonomously".
4
4
 
5
- Drives one draft PR to convergence in a single autonomous workflow run. Each round runs a deterministic static sweep first, then a code-review pass, a bug-audit (with its adversarial second pass), and a self-review parity pass in parallel on the same HEAD, deduplicates findings, applies every fix in one commit, and re-verifies. Once the internal lenses are clean, Cursor Bugbot, Copilot, and Codex run as terminal confirmation gates, and the run marks the PR ready on convergence. State lives in the workflow journal; no `ScheduleWakeup` ticks.
5
+ Drives one draft PR to convergence in one autonomous run. On `pacer=workflow`, each round runs a deterministic static sweep first, then a code-review pass, a bug-audit (with its adversarial second pass), and a self-review parity pass in parallel on the same HEAD, deduplicates findings, applies every fix in one commit, and re-verifies; state lives in the workflow journal. On `pacer=portable`, the continuous driver in `_shared/pr-loop/portable-driver.md` runs the shared pr-converge phase machine with the same helpers and `check_convergence.py` ready definition. Once the internal passes are clean, Cursor Bugbot, Copilot, and Codex run as terminal confirmation gates, and the run marks the PR ready on convergence.
6
6
 
7
7
  ## Subdirectories
8
8
 
@@ -15,7 +15,7 @@ Drives one draft PR to convergence in a single autonomous workflow run. Each rou
15
15
 
16
16
  | File | Role |
17
17
  |---|---|
18
- | `SKILL.md` | Full entry-point protocol: pre-flight steps, worktree setup, `Workflow` call, budget-aware round boundaries, teardown, and the per-round convergence loop summary. |
18
+ | `SKILL.md` | Full entry-point protocol: pacer selection, pre-flight steps, worktree setup, Workflow or portable path, budget-aware boundaries, teardown, and the convergence loop summary. |
19
19
  | `workflow/converge.mjs` | Main convergence workflow. Runs a static sweep then the internal lenses each round, applies fixes, runs Bugbot, Copilot, and Codex as terminal gates, checks convergence, and marks the PR ready. |
20
20
  | `workflow/aggregate_runs.py` | Merges every autoconverge journal for a PR into one deduped journal. |
21
21
  | `workflow/convergence_summary.py` | Builds the convergence-summary agent prompt over the merged findings. |
@@ -31,4 +31,6 @@ Drives one draft PR to convergence in a single autonomous workflow run. Each rou
31
31
 
32
32
  ## Entry point
33
33
 
34
- Requires the `Workflow` tool. The `SKILL.md` body specifies the exact pre-flight sequence and `Workflow` call.
34
+ Selects `workflow` or `portable` via `select_converge_pacer.py`. Missing
35
+ `Workflow` selects portable and continues — it does not abort. The `SKILL.md`
36
+ body specifies the pre-flight sequence and each pacer's run path.
@@ -1,48 +1,66 @@
1
1
  ---
2
2
  name: autoconverge
3
3
  description: >-
4
- Drives one draft PR to convergence in one autonomous run: static sweep, then
5
- code review, bug audit, and self-review on one HEAD, fixes in one commit, then
6
- Bugbot, Copilot, and Codex as terminal gates before ready. Use when the user
7
- says '/autoconverge', 'autoconverge this PR', 'converge this PR in one run',
8
- 'run the converge workflow', or 'drive the PR to ready autonomously'.
4
+ Drives one draft PR to convergence in one autonomous run with a host-selected
5
+ pacer. On workflow: three parallel lenses (code-review, bug-audit, self-review)
6
+ and one-commit fixes via converge.mjs. On portable: pr-converge continuous ticks
7
+ via portable-driver.md. Bugbot, Copilot, and Codex as terminal confirmation
8
+ gates before ready. Use when the user says '/autoconverge', 'autoconverge this
9
+ PR', 'converge this PR in one run', 'run the converge workflow', or 'drive the
10
+ PR to ready autonomously'.
9
11
  ---
10
12
 
11
13
  # Autoconverge
12
14
 
13
15
  One launch drives the whole loop to convergence. The `/autoconverge` skill
14
- resolves PR scope, enters a worktree, grants project permissions, then hands
15
- the loop to the **`converge.mjs` workflow**, which runs every round and every
16
- reviewer wait inside one background pass no ticks, no `ScheduleWakeup`, no
17
- state file. State lives in the workflow's own variables; resume is handled by
18
- the workflow journal.
16
+ scans the tool list, selects a **pacer** (Workflow or portable), then resolves
17
+ PR scope, enters a worktree, grants project permissions, and drives the shared
18
+ converge product to ready (or a named blocker).
19
19
 
20
- `pr-converge` paces the same four-reviewer loop across `ScheduleWakeup` ticks;
21
- autoconverge runs it as a deterministic workflow. The two skills share the same
22
- helper scripts and the same convergence gate.
20
+ | Pacer | Host surface | How the loop runs |
21
+ |---|---|---|
22
+ | `workflow` | Tool list includes `Workflow` | `converge.mjs` background pass; state in the workflow journal |
23
+ | `portable` | `Workflow` absent | Continuous in-session ticks per [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md); same helpers and `check_convergence.py` ready definition |
24
+
25
+ `pr-converge` selects `schedule_wakeup` or `portable` the same way. Both
26
+ entry skills share the helper scripts and the convergence gate.
23
27
 
24
28
  ## Run scope: one PR or several
25
29
 
26
30
  Decide the scope from how many PRs the user named, then follow that path:
27
31
 
28
- 1. **One PR** → the single-PR run described below (`workflow/converge.mjs`): one
29
- worktree, one workflow launch, one teardown.
30
- 2. **Several PRs** → the [Multiple PRs](reference/multi-pr.md) run
31
- (`workflow/converge_multi.mjs`): one worktree per PR and a single workflow
32
- launch that drives every PR's converge run in parallel, then one teardown per
33
- PR.
32
+ 1. **One PR** → the single-PR run described below: one worktree, one pacer
33
+ launch (`converge.mjs` on `pacer=workflow`, portable continuous ticks on
34
+ `pacer=portable`), one teardown.
35
+ 2. **Several PRs** the [Multiple PRs](reference/multi-pr.md) run: on
36
+ `pacer=workflow`, `workflow/converge_multi.mjs` drives every PR in parallel;
37
+ on `pacer=portable`, run the portable driver once per PR (serial or host
38
+ fan-out), then one teardown per PR.
34
39
 
35
- The single-PR sections (Requirements, Pre-flight, Run the workflow, Teardown)
36
- each describe one converge run. The multi-PR reference reuses them once per PR
37
- and adds only what fanning out needs: a per-PR worktree and a per-PR teardown
38
- loop.
40
+ The single-PR sections (Requirements, Pre-flight, Run the workflow or portable
41
+ driver, Teardown) each describe one converge run. The multi-PR reference reuses
42
+ them once per PR and adds only what fanning out needs: a per-PR worktree and a
43
+ per-PR teardown loop.
39
44
 
40
45
  ## Requirements
41
46
 
42
- Scan the tool list at the top of this conversation for the literal string
43
- `Workflow`. If it is absent, report `autoconverge requires the Workflow tool;
44
- aborting` and stop. The workflow also needs the `gh` CLI authenticated for the
45
- PR's owner.
47
+ Scan the tool list for `Workflow` and `ScheduleWakeup`, then select the pacer:
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=workflow` — continue with Pre-flight and **Run the workflow** below.
57
+ - `pacer=portable` — continue with Pre-flight (portable worktree rules when
58
+ `EnterWorktree` is absent), then the continuous driver in
59
+ [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md).
60
+ **Do not abort** because the Workflow tool is missing.
61
+
62
+ Transport still needs authenticated GitHub access for the PR's owner (`gh` or
63
+ `pr-loop-cloud-transport`).
46
64
 
47
65
  ## Review-lens boundary
48
66
 
@@ -65,11 +83,30 @@ orchestrating session's own steps, and the script's agent-prompt text carries
65
83
 
66
84
  ## Pre-flight (main session)
67
85
 
68
- 1. **Enter a worktree.** Call `EnterWorktree` with no arguments before any
69
- `gh`, `git`, file read, or edit. `gh`/`git` Bash calls do not auto-isolate,
70
- so this is mandatory. If it fails, report and stop. A bare `EnterWorktree`
71
- branches from `origin/main`; step 2 positions the worktree on the PR's head
72
- ref, which the workflow needs.
86
+ 0. **Build the task list (step 1 of every run).** After Copilot quota / down
87
+ flags and Codex required/down are known, run only:
88
+
89
+ ```
90
+ python "$HOME/.claude/skills/_shared/pr-loop/scripts/build_converge_task_list.py" \
91
+ --bugbot-down <0|1> --copilot-down <0|1> \
92
+ --codex-down <0|1> --codex-required <0|1>
93
+ ```
94
+
95
+ Register every `tasks[]` entry on the session task list. **Final task** is
96
+ always `all_runnable_reviews_clean_same_head` (`done_when` in the JSON).
97
+ The run is complete only when that final task is completed — all runnable
98
+ code reviews CLEAN on the same HEAD. Do not invent tasks in prose. On
99
+ `pacer=portable`, `open-run` returns the same list; still register it as
100
+ step 1 before driving reviews.
101
+
102
+ 1. **Enter a worktree.** When `EnterWorktree` is in the tool list, call it with
103
+ no arguments before any `gh`, `git`, file read, or edit. `gh`/`git` Bash
104
+ calls do not auto-isolate, so isolation is mandatory. If it fails, report
105
+ and stop. A bare `EnterWorktree` branches from `origin/main`; step 2
106
+ positions the worktree on the PR's head ref. When `EnterWorktree` is
107
+ absent, isolate with git worktree machinery per
108
+ [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md)
109
+ § Isolation and worktree, then continue step 2.
73
110
 
74
111
  2. **Resolve PR scope.** When the user passed a PR URL or number, parse owner,
75
112
  repo, and number from it. Otherwise read the current branch's PR:
@@ -78,14 +115,15 @@ orchestrating session's own steps, and the script's agent-prompt text carries
78
115
  ready, mark it draft first (`gh pr ready <n> --repo <o>/<r> --undo`) so the
79
116
  loop owns the ready transition.
80
117
 
81
- **Position the worktree on the PR branch.** The workflow reviews
118
+ **Position the worktree on the PR branch.** The run reviews
82
119
  `git diff origin/main...HEAD` against this worktree's local `HEAD` and pushes
83
120
  each fix to the PR branch, so the worktree sits on the PR's head ref at the PR
84
- HEAD before the workflow launches. A worktree fresh off `origin/main` has
121
+ HEAD before the loop starts. A worktree fresh off `origin/main` has
85
122
  `HEAD == origin/main`, shows an empty diff, and reports a false convergence
86
123
  with zero findings. When a local worktree already tracks the PR branch, enter
87
- that one by passing its path to `EnterWorktree`; otherwise put the entered
88
- worktree on the branch with `gh pr checkout <number> --repo <owner>/<repo>`
124
+ that one (pass its path to `EnterWorktree` when that tool exists; otherwise
125
+ `cd` into it). Otherwise put the checkout on the branch with
126
+ `gh pr checkout <number> --repo <owner>/<repo>`
89
127
  (or `git fetch origin <headRefName>` then `git switch <headRefName>`). Confirm
90
128
  before launching: `git rev-parse --abbrev-ref HEAD` equals the PR's head ref
91
129
  and local `HEAD` equals the PR head SHA.
@@ -93,27 +131,32 @@ orchestrating session's own steps, and the script's agent-prompt text carries
93
131
  3. **Verify the worktree is the PR's repo (strict pre-flight).** Run
94
132
  `python "$HOME/.claude/skills/_shared/pr-loop/scripts/preflight_worktree.py" --owner <owner> --repo <repo> --mode strict`.
95
133
  It confirms the working directory is a checkout of the PR's own repo and
96
- that `git worktree` machinery is healthy, so `EnterWorktree` can create and
97
- enter the branch worktree. A non-zero exit prints a `PREFLIGHT_OUTCOME` line
98
- and an `ABORT` line: report that line and stop. Autoconverge runs inside the
99
- PR's own repo, so a working directory rooted in a different repo (for
100
- example, `claude-dev-env` while the PR lives in `llm-settings`) or in no
101
- git checkout at all cannot continue.
134
+ that `git worktree` machinery is healthy. A non-zero exit prints a
135
+ `PREFLIGHT_OUTCOME` line and an `ABORT` line: report that line and stop.
136
+ Autoconverge runs inside the PR's own repo, so a working directory rooted in
137
+ a different repo (for example, `claude-dev-env` while the PR lives in
138
+ `llm-settings`) or in no git checkout at all cannot continue.
102
139
 
103
140
  4. **Grant project permissions.** Apply the `pr-loop-lifecycle` skill's Open
104
141
  section (`../pr-loop-lifecycle/SKILL.md`) — the grant command
105
142
  (`grant_project_claude_permissions.py`) and the auto-mode `AskUserQuestion`
106
143
  escalation for a blocked grant both live there.
107
144
 
108
- 5. **Copilot quota pre-check.** Before the `Workflow` call, apply the
145
+ 5. **Copilot quota pre-check.** Before the pacer starts the loop, apply the
109
146
  `reviewer-gates` skill's Copilot quota gate (`../reviewer-gates/SKILL.md`)
110
- once. Exit 0 maps to `copilotDisabled: false` in the Workflow call; any
111
- non-zero exit maps to `copilotDisabled: true`, and the workflow then skips
112
- the Copilot gate with no agent spawned.
147
+ once. Exit 0 maps to `copilotDisabled: false` / `copilot_down=false`; any
148
+ non-zero exit maps to `copilotDisabled: true` / `copilot_down=true`, and the
149
+ run skips the Copilot gate with no agent spawned.
150
+
151
+ 6. **Branch on pacer.** When `pacer=portable`, follow
152
+ [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md)
153
+ § Continuous tick loop and § Autoconverge entry on portable pacer, then
154
+ skip **Run the workflow** (no Workflow tool). When `pacer=workflow`,
155
+ continue with **Run the workflow**.
113
156
 
114
157
  ## Run the workflow
115
158
 
116
- Call the `Workflow` tool against the colocated script:
159
+ (`pacer=workflow` only.) Call the `Workflow` tool against the colocated script:
117
160
 
118
161
  ```
119
162
  Workflow({
@@ -160,26 +203,89 @@ skips a reviewer that is down or out of quota without re-probing. This list is t
160
203
 
161
204
  ## Copilot findings — tier, verify, then route
162
205
 
163
- The Copilot gate tiers each finding (self-healing vs code-concern), verifies
164
- each code concern with an executed check against the flagged HEAD, and returns a
165
- confirmed / refuted / inconclusive verdict full semantics and the `userReview`
166
- return contract in
167
- [`reference/copilot-findings.md`](reference/copilot-findings.md). Only
168
- inconclusive findings return `blocker: "user-review"`; the orchestrating session
169
- handles that hold in Teardown via the
170
- [`copilot-finding-triage`](../copilot-finding-triage/SKILL.md) skill.
206
+ The Copilot gate tiers each finding: a **self-healing** finding (style, type
207
+ hints, imports, formatting, magic-value extraction, test-only or doc-vs-code
208
+ fixes nothing that changes observable runtime behavior) flows into the fix
209
+ round with no user notification. A **code-concern** finding (logic, security,
210
+ data handling, error-handling semantics, concurrency — the tier whenever in
211
+ doubt) goes to a verification stage before any routing.
212
+
213
+ Each code-concern finding gets its own verifier agent, all in parallel, inside
214
+ the workflow. A verdict is conclusive only when an actual check ran: the verifier
215
+ executes a command against the flagged HEAD — running the code path with crafted
216
+ inputs, forcing the claimed error condition, or running a purpose-built test —
217
+ and captures its output. The verdict carries
218
+ `{ verdict, checkCommand, checkOutput, evidence }`; a conclusive verdict with an
219
+ empty `checkCommand` or `checkOutput` downgrades to inconclusive.
220
+
221
+ - **confirmed** — the check reproduces the defect. The finding becomes
222
+ self-healing: it joins the fix round carrying its repro, and the fix re-runs
223
+ that same check, adds a regression test where the suite covers the surface,
224
+ lands in one commit, pushes, and replies on the thread with the fix SHA and the
225
+ before/after output. No page.
226
+ - **refuted** — the check shows the code already behaves correctly in the exact
227
+ scenario the finding claims is broken. The workflow replies on the thread with
228
+ the command and output, resolves it, and counts it clean. No page.
229
+ - **inconclusive** — everything else, and the verifier's default: no runnable
230
+ check exists, the check is infeasible here, the results are ambiguous, or the
231
+ fix needs a product decision. Any doubt sorts here. Only inconclusive findings
232
+ page the user.
233
+
234
+ A round whose code concerns all confirm or refute never returns
235
+ `blocker: "user-review"`. On one or more inconclusive findings, the workflow
236
+ stops with `converged: false`, `blocker: "user-review"`, and a `userReview`
237
+ field carrying
238
+ `{ reviewUrl, findings: [{ file, line, severity, tier, title, evidence }] }` —
239
+ `evidence` is the verifier's one-line note stating what check was attempted and
240
+ why it was not decisive.
241
+
242
+ The wait for a human belongs to the orchestrating session. On a
243
+ `blocker: "user-review"` return, run the
244
+ [`copilot-finding-triage`](../copilot-finding-triage/SKILL.md) skill for the ntfy
245
+ page only (the per-finding summary and evidence note plus the `reviewUrl`
246
+ Copilot review link), then hold for the user's response by pacer. On
247
+ `pacer=portable`, override triage's hold step — ntfy plus the portable hold
248
+ below; do not follow triage's `ScheduleWakeup` / `send_later` path.
249
+
250
+ - **`pacer=workflow`** — triage hold: 45-minute `ScheduleWakeup` (or
251
+ `send_later` when that is the host arm).
252
+ - **`pacer=portable`** — portable hold only: in-session poll or handoff per
253
+ [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md);
254
+ never `ScheduleWakeup` or `send_later`.
255
+
256
+ When the user answers within the window, follow their direction. When the
257
+ window closes with no response, run normal teardown and report the
258
+ inconclusive findings un-reviewed.
171
259
 
172
260
  ## Budget stop
173
261
 
174
- `converge.mjs` paces itself against the workflow `budget` API and stops at a
175
- round boundary when a full round does not fit —
176
- [`reference/stop-conditions.md`](reference/stop-conditions.md) § Budget stop
177
- gives the rule. On a `blocker: "budget"` return, write the durable handoff with
178
- the run id and the `Workflow({scriptPath, resumeFromRunId})` resume command
179
- before stopping, so a fresh session resumes the paced run without the stopped
180
- session's transcript.
262
+ Branch the stop and resume path on the selected pacer. Full rule:
263
+ [`reference/stop-conditions.md`](reference/stop-conditions.md) § Budget stop.
264
+
265
+ - **`pacer=workflow`** `converge.mjs` paces against the workflow `budget` API
266
+ and stops at a round boundary when a full round does not fit. On a
267
+ `blocker: "budget"` return, write the durable handoff with the run id and the
268
+ `Workflow({scriptPath, resumeFromRunId})` resume command before stopping, so a
269
+ fresh session resumes the paced run without the stopped session's transcript.
270
+ - **`pacer=portable`** — stop at a tick boundary when the session cannot cover a
271
+ full clean tick; write the durable handoff and resume with
272
+ `/autoconverge <PR URL>` (see
273
+ [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md)).
274
+ Do not instruct a Workflow resume on the portable path.
275
+
276
+ ## Teardown
277
+
278
+ ### `pacer=portable`
279
+
280
+ Skip Workflow-only report steps that need a workflow run id (journal merge,
281
+ HTML closing report, Artifact publish). Run `pr-loop-lifecycle` Close
282
+ (description rewrite when converged, working-tree clean, permission revoke)
283
+ and print the final report block using tick/`check_convergence` outcomes.
284
+ User-review holds use the portable in-session poll (or handoff) from
285
+ [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md)
286
+ — not `ScheduleWakeup`. Resume command: `/autoconverge <PR URL>`.
181
287
 
182
- ## Teardown (on workflow completion)
288
+ ### `pacer=workflow` (on workflow completion)
183
289
 
184
290
  Teardown runs as an ordered checkpoint list. After each checkpoint finishes,
185
291
  re-write the durable handoff with `--phase teardown`, the run id, and the
@@ -201,15 +307,18 @@ On teardown entry, when `~/.claude/runtime/pr-loop/bugteam-pr-<N>/handoff.json`
201
307
  exists, read its `completed_steps` and skip any checkpoint the list already
202
308
  names, so a resumed run performs only the checkpoints left.
203
309
 
204
- On a `blocker: "user-review"` return, the workflow held one or more code-concern
205
- findings that stayed inconclusive after the executed-check verification stage.
206
- Run the [`copilot-finding-triage`](../copilot-finding-triage/SKILL.md) gate before
310
+ On a `blocker: "user-review"` return under `pacer=workflow`, the workflow held
311
+ one or more code-concern findings that stayed inconclusive after the
312
+ executed-check verification stage. Run the
313
+ [`copilot-finding-triage`](../copilot-finding-triage/SKILL.md) gate before
207
314
  teardown: send the ntfy alert with the per-finding summary and evidence note and
208
- the `userReview.reviewUrl` link, then hold with a 45-minute `ScheduleWakeup`. Act
209
- on the user's direction when it arrives inside the window; when the window closes
210
- with no response, fall through to normal teardown and report the
211
- `userReview.findings` un-reviewed. The PR stays a draft in this path — the
212
- workflow marked nothing ready.
315
+ the `userReview.reviewUrl` link, then hold with a 45-minute `ScheduleWakeup` (or
316
+ `send_later` when that is the host arm). Act on the user's direction when it
317
+ arrives inside the window; when the window closes with no response, fall through
318
+ to normal teardown and report the `userReview.findings` un-reviewed. The PR
319
+ stays a draft in this path — the workflow marked nothing ready. Under
320
+ `pacer=portable`, use the portable teardown hold above (ntfy plus in-session
321
+ poll or handoff; never `ScheduleWakeup`).
213
322
 
214
323
  Before the checkpoints, when the workflow returned a non-null `copilotNote`
215
324
  (the Copilot gate was bypassed), query the PR's reviews once more for a
@@ -277,8 +386,10 @@ When the user names several PRs, run the multi-PR path in
277
386
  [`reference/multi-pr.md`](reference/multi-pr.md): one worktree per PR
278
387
  (`git worktree add` on each head ref, strict pre-flight per worktree, one
279
388
  permission grant per repository, one Copilot quota check for the whole run),
280
- then a single `workflow/converge_multi.mjs` launch with one entry per PR, then
281
- the single-PR teardown once per `results` entry and a one-line-per-PR summary.
389
+ then launch by pacer — `pacer=workflow`: one `workflow/converge_multi.mjs`
390
+ call with one entry per PR; `pacer=portable`: portable driver once per PR
391
+ (serial or host fan-out) — then single-PR teardown once per PR and a
392
+ one-line-per-PR summary.
282
393
 
283
394
  ## Self-closing loop: converge the deferred PRs
284
395
 
@@ -294,6 +405,7 @@ also carries the Conventional-Commit title rule each hardening PR must meet.
294
405
  ## Folder map
295
406
 
296
407
  - `SKILL.md` — this hub.
408
+ - [`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md) — portable pacer when `Workflow` is absent.
297
409
  - `workflow/converge.mjs` — the convergence workflow script.
298
410
  - `workflow/converge_multi.mjs` — the multi-PR fan-out driver: one `converge.mjs` child run per PR in parallel, each pinned to its PR worktree via `repoPath`.
299
411
  - `workflow/aggregate_runs.py` — merges every autoconverge journal for a PR into one journal and returns its deduped findings, fix summaries, round count, and final SHA.
@@ -301,7 +413,6 @@ also carries the Conventional-Commit title rule each hardening PR must meet.
301
413
  - `workflow/render_report.py` — builds the closing convergence insights HTML report, taking the summary from `--summary-file`.
302
414
  - `workflow/autoconverge_report_constants/` — named constants for the report builder and the summary prompt.
303
415
  - `reference/convergence.md` — the whole loop: reuse pass, round shape, terminal gates, model tiers, ready definition.
304
- - `reference/copilot-findings.md` — the Copilot gate tiering, per-finding verification, and the `userReview` return contract.
305
416
  - `reference/stop-conditions.md` — every way the run ends short of ready, including the budget stop.
306
417
  - `reference/gotchas.md` — hard-won failure lessons.
307
418
  - `reference/closing-report.md` — the closing HTML report: data source, build steps, publishing.
@@ -66,11 +66,11 @@ confirmation gates that are expected to return zero.
66
66
  - **Code-review lens** — a correctness-focused review pass (`code-quality-agent`),
67
67
  report-only workflow agent — see runCodeReviewLens in workflow/converge.mjs for its configuration.
68
68
  The built-in `/code-review` command is a separate surface outside this
69
- workflow path. Autoconverge runs only where the Workflow tool exists (the
70
- skill hub Requirements sentence `autoconverge requires the Workflow tool`).
71
- Harness portability of that command is a pr-converge concern: pr-converge's
72
- CODE_REVIEW phase runs Claude Code's built-in `/code-review high --fix`
73
- ([`../../pr-converge/reference/per-tick.md`](../../pr-converge/reference/per-tick.md)).
69
+ workflow path. On `pacer=workflow`, this lens runs inside `converge.mjs`.
70
+ On `pacer=portable`, the continuous driver uses the pr-converge CODE_REVIEW
71
+ phase through `invoke_code_review.py` instead of `runCodeReviewLens`
72
+ ([`../../_shared/pr-loop/portable-driver.md`](../../_shared/pr-loop/portable-driver.md);
73
+ [`../../pr-converge/reference/per-tick.md`](../../pr-converge/reference/per-tick.md)).
74
74
  - **Bug-audit lens** — the bug-audit (`code-quality-agent`) applying the
75
75
  shared A–P rubric from `_shared/pr-loop/audit-contract.md`, then its
76
76
  adversarial second pass, and the doc-parity, test-assertion, and