claude-dev-env 1.93.0 → 1.94.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 (220) hide show
  1. package/_shared/pr-loop/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/audit-contract.md +1 -1
  3. package/_shared/pr-loop/gh-payloads.md +3 -3
  4. package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
  5. package/_shared/pr-loop/scripts/README.md +2 -2
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +127 -13
  7. package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
  8. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
  9. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -69
  10. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
  11. package/_shared/pr-loop/state-schema.md +3 -22
  12. package/bin/CLAUDE.md +1 -1
  13. package/bin/install.mjs +2 -7
  14. package/bin/install.test.mjs +10 -4
  15. package/hooks/blocking/CLAUDE.md +2 -0
  16. package/hooks/blocking/pii_commit_command.py +398 -0
  17. package/hooks/blocking/pii_payload_scan.py +133 -0
  18. package/hooks/blocking/pii_prevention_blocker.py +151 -449
  19. package/hooks/blocking/pii_scanner.py +26 -1
  20. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +2 -2
  21. package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
  22. package/hooks/blocking/test_pii_scanner.py +27 -2
  23. package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
  24. package/hooks/hooks_constants/CLAUDE.md +2 -2
  25. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
  26. package/hooks/hooks_constants/local_identity.py +65 -15
  27. package/hooks/hooks_constants/pii_prevention_constants.py +20 -8
  28. package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
  29. package/hooks/hooks_constants/test_local_identity.py +68 -0
  30. package/package.json +1 -1
  31. package/scripts/CLAUDE.md +10 -1
  32. package/scripts/Get-SessionAccount.ps1 +210 -0
  33. package/scripts/tests/CLAUDE.md +15 -1
  34. package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
  35. package/skills/CLAUDE.md +3 -25
  36. package/skills/anthropic-plan/SKILL.md +1 -13
  37. package/skills/autoconverge/CLAUDE.md +5 -2
  38. package/skills/autoconverge/SKILL.md +112 -387
  39. package/skills/autoconverge/reference/CLAUDE.md +4 -1
  40. package/skills/autoconverge/reference/closing-report.md +9 -2
  41. package/skills/autoconverge/reference/convergence.md +26 -22
  42. package/skills/autoconverge/reference/headless-safety.md +44 -0
  43. package/skills/autoconverge/reference/multi-pr.md +88 -0
  44. package/skills/autoconverge/reference/self-closing-loop.md +84 -0
  45. package/skills/autoconverge/reference/stop-conditions.md +21 -13
  46. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
  47. package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
  48. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
  49. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  50. package/skills/autoconverge/workflow/converge.mjs +284 -94
  51. package/skills/bugteam/CLAUDE.md +1 -1
  52. package/skills/bugteam/CONSTRAINTS.md +3 -3
  53. package/skills/bugteam/PROMPTS.md +8 -19
  54. package/skills/bugteam/SKILL.md +13 -14
  55. package/skills/bugteam/reference/CLAUDE.md +1 -1
  56. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  57. package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
  58. package/skills/bugteam/reference/github-pr-reviews.md +56 -38
  59. package/skills/bugteam/reference/team-setup.md +11 -11
  60. package/skills/bugteam/scripts/CLAUDE.md +6 -6
  61. package/skills/bugteam/scripts/README.md +20 -8
  62. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
  63. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
  64. package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
  65. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
  66. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
  67. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
  68. package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
  69. package/skills/copilot-finding-triage/SKILL.md +80 -28
  70. package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
  71. package/skills/copilot-finding-triage/templates/notification.md +8 -3
  72. package/skills/everything-search/SKILL.md +1 -2
  73. package/skills/grokify/SKILL.md +58 -0
  74. package/skills/grokify/templates/handoff-template.md +92 -0
  75. package/skills/orchestrator/SKILL.md +12 -8
  76. package/skills/orchestrator-refresh/SKILL.md +3 -3
  77. package/skills/pr-converge/SKILL.md +132 -233
  78. package/skills/pr-converge/reference/convergence-gates.md +102 -66
  79. package/skills/pr-converge/reference/examples.md +37 -25
  80. package/skills/pr-converge/reference/fix-protocol.md +7 -7
  81. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
  82. package/skills/pr-converge/reference/per-tick.md +64 -41
  83. package/skills/pr-converge/reference/state-schema.md +27 -2
  84. package/skills/pr-converge/scripts/CLAUDE.md +8 -0
  85. package/skills/pr-converge/scripts/README.md +4 -3
  86. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
  87. package/skills/pr-converge/scripts/check_convergence.py +189 -645
  88. package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
  89. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
  90. package/skills/pr-converge/scripts/conftest.py +8 -0
  91. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
  92. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
  93. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
  94. package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
  95. package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
  96. package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
  97. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
  98. package/skills/privacy-hygiene/SKILL.md +8 -7
  99. package/skills/reviewer-gates/SKILL.md +26 -8
  100. package/skills/session-log/SKILL.md +11 -12
  101. package/skills/session-tidy/SKILL.md +40 -30
  102. package/skills/skill-builder/CLAUDE.md +13 -10
  103. package/skills/skill-builder/SKILL.md +31 -31
  104. package/skills/skill-builder/references/CLAUDE.md +10 -6
  105. package/skills/skill-builder/references/delegation-map.md +13 -4
  106. package/skills/skill-builder/references/description-field.md +113 -0
  107. package/skills/skill-builder/references/self-audit-checklist.md +21 -4
  108. package/skills/skill-builder/references/skill-modularity.md +113 -0
  109. package/skills/skill-builder/references/skill-types.md +4 -0
  110. package/skills/skill-builder/templates/CLAUDE.md +4 -4
  111. package/skills/skill-builder/templates/gap-analysis.md +25 -1
  112. package/skills/skill-builder/workflows/CLAUDE.md +6 -4
  113. package/skills/skill-builder/workflows/improve-skill.md +17 -16
  114. package/skills/skill-builder/workflows/new-skill.md +34 -10
  115. package/skills/skill-builder/workflows/polish-skill.md +59 -43
  116. package/skills/usage-pause/SKILL.md +10 -14
  117. package/skills/bdd-protocol/CLAUDE.md +0 -26
  118. package/skills/bdd-protocol/SKILL.md +0 -30
  119. package/skills/bdd-protocol/references/CLAUDE.md +0 -10
  120. package/skills/bdd-protocol/references/anti-patterns.md +0 -26
  121. package/skills/bdd-protocol/references/example-mapping.md +0 -23
  122. package/skills/bg-agent/CLAUDE.md +0 -17
  123. package/skills/bg-agent/SKILL.md +0 -69
  124. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
  125. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
  126. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
  127. package/skills/caveman/CLAUDE.md +0 -15
  128. package/skills/caveman/SKILL.md +0 -38
  129. package/skills/code/CLAUDE.md +0 -17
  130. package/skills/code/SKILL.md +0 -178
  131. package/skills/deep-research/CLAUDE.md +0 -17
  132. package/skills/deep-research/SKILL.md +0 -80
  133. package/skills/findbugs/CLAUDE.md +0 -20
  134. package/skills/findbugs/SKILL.md +0 -241
  135. package/skills/fixbugs/CLAUDE.md +0 -19
  136. package/skills/fixbugs/SKILL.md +0 -142
  137. package/skills/gh-paginate/CLAUDE.md +0 -18
  138. package/skills/gh-paginate/SKILL.md +0 -84
  139. package/skills/gotcha/CLAUDE.md +0 -33
  140. package/skills/gotcha/SKILL.md +0 -73
  141. package/skills/implement/CLAUDE.md +0 -27
  142. package/skills/implement/SKILL.md +0 -66
  143. package/skills/implement/scripts/CLAUDE.md +0 -22
  144. package/skills/implement/scripts/append_note.py +0 -133
  145. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
  146. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  147. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
  148. package/skills/implement/scripts/test_append_note.py +0 -191
  149. package/skills/log-audit/CLAUDE.md +0 -20
  150. package/skills/log-audit/SKILL.md +0 -66
  151. package/skills/log-audit/reference/CLAUDE.md +0 -9
  152. package/skills/log-audit/reference/charter.md +0 -52
  153. package/skills/log-audit/scripts/CLAUDE.md +0 -27
  154. package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
  155. package/skills/log-audit/scripts/collect_log_window.py +0 -199
  156. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
  157. package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
  158. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
  159. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
  160. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
  161. package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
  162. package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
  163. package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
  164. package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
  165. package/skills/logifix/CLAUDE.md +0 -36
  166. package/skills/logifix/SKILL.md +0 -69
  167. package/skills/logifix/scripts/CLAUDE.md +0 -16
  168. package/skills/logifix/scripts/logifix.ps1 +0 -205
  169. package/skills/monitor-open-prs/CLAUDE.md +0 -34
  170. package/skills/monitor-open-prs/SKILL.md +0 -88
  171. package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
  172. package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
  173. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
  174. package/skills/monitor-open-prs/test_skill_contract.py +0 -27
  175. package/skills/post-audit-findings/SKILL.md +0 -80
  176. package/skills/pr-consistency-audit/CLAUDE.md +0 -34
  177. package/skills/pr-consistency-audit/SKILL.md +0 -116
  178. package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
  179. package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
  180. package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
  181. package/skills/pr-fix-protocol/SKILL.md +0 -65
  182. package/skills/pr-review-responder/CLAUDE.md +0 -35
  183. package/skills/pr-review-responder/EXAMPLES.md +0 -590
  184. package/skills/pr-review-responder/PRINCIPLES.md +0 -533
  185. package/skills/pr-review-responder/README.md +0 -168
  186. package/skills/pr-review-responder/SKILL.md +0 -203
  187. package/skills/pr-review-responder/TESTING.md +0 -350
  188. package/skills/pr-scope-resolve/SKILL.md +0 -45
  189. package/skills/pre-compact/CLAUDE.md +0 -24
  190. package/skills/pre-compact/SKILL.md +0 -134
  191. package/skills/qbug/CLAUDE.md +0 -40
  192. package/skills/qbug/SKILL.md +0 -387
  193. package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
  194. package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
  195. package/skills/refine/CLAUDE.md +0 -44
  196. package/skills/refine/SKILL.md +0 -262
  197. package/skills/refine/templates/CLAUDE.md +0 -17
  198. package/skills/refine/templates/implementation-notes-template.html +0 -56
  199. package/skills/refine/templates/plan-template.md +0 -60
  200. package/skills/research-mode/CLAUDE.md +0 -35
  201. package/skills/research-mode/SKILL.md +0 -53
  202. package/skills/structure-prompt/CLAUDE.md +0 -42
  203. package/skills/structure-prompt/SKILL.md +0 -44
  204. package/skills/structure-prompt/reference/CLAUDE.md +0 -28
  205. package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
  206. package/skills/structure-prompt/reference/block-classification.md +0 -27
  207. package/skills/structure-prompt/reference/canonical-case.md +0 -48
  208. package/skills/structure-prompt/reference/citation-depth.md +0 -70
  209. package/skills/structure-prompt/reference/cleanup.md +0 -33
  210. package/skills/structure-prompt/reference/constraints.md +0 -33
  211. package/skills/structure-prompt/reference/directives.md +0 -37
  212. package/skills/structure-prompt/reference/examples.md +0 -72
  213. package/skills/structure-prompt/reference/instantiation.md +0 -51
  214. package/skills/structure-prompt/reference/output-contract.md +0 -72
  215. package/skills/structure-prompt/reference/per-category.md +0 -23
  216. package/skills/structure-prompt/reference/persona.md +0 -38
  217. package/skills/structure-prompt/reference/research.md +0 -33
  218. package/skills/structure-prompt/reference/structure.md +0 -28
  219. package/skills/verified-build/CLAUDE.md +0 -33
  220. package/skills/verified-build/SKILL.md +0 -33
@@ -35,7 +35,8 @@ same edit → verify → commit fix flow the rounds use, landing in one verified
35
35
  commit before convergence begins. The pass is best-effort: when no case clears
36
36
  all three criteria the run proceeds straight to convergence. Whatever the reuse
37
37
  pass surfaces also joins the round findings, so the code-review lens re-checks
38
- any improvement that did not land.
38
+ any improvement that did not land. The run result's `reuseNote` records what
39
+ landed.
39
40
 
40
41
  ## The round loop
41
42
 
@@ -93,6 +94,13 @@ confirmation gates that are expected to return zero.
93
94
  6. **Zero findings on a stable HEAD** → post the CLEAN bugteam audit artifact
94
95
  for that HEAD, then move to the terminal Bugbot gate.
95
96
 
97
+ A round whose findings are ALL code-standard violations (pure CODE_RULES/style,
98
+ no behavioral impact) passes for convergence purposes: the workflow files a
99
+ follow-up issue listing the findings, opens a draft environment-hardening PR
100
+ (hooks/rules that block those violation classes at Write/Edit time), resolves
101
+ any bot threads with a deferral note, and reports the deferral in
102
+ `standardsNote`. The hardening PRs land in the run result's `deferredPrs` list.
103
+
96
104
  **BUGBOT** gate (terminal external confirmation):
97
105
 
98
106
  - Runs once the internal lenses are clean. When Bugbot is off for the run — the
@@ -132,6 +140,8 @@ the strongest model only where judgment is dense:
132
140
  - **opus** — the reading lenses (code-review, bug-audit, self-review,
133
141
  reuse), the terminal Bugbot gate, and the code-editing steps that fix findings
134
142
  (fix-edit, conflict-edit, repair-edit, standards-edit).
143
+ - **haiku** — the mechanical probes: preflight, the Copilot gate, the
144
+ CLEAN-audit post, and the convergence check.
135
145
 
136
146
  ## Full-diff rule
137
147
 
@@ -141,27 +151,21 @@ bugbot-flagged path does not satisfy the round; its clean verdict is not a clean
141
151
 
142
152
  ## The ready definition
143
153
 
144
- `check_convergence.py` is the single source of truth for readiness. It re-derives
145
- every condition from GitHub and marks the PR ready only when all of these hold on
146
- the current HEAD:
147
-
148
- 1. Bugbot CI check run is completed with a success or neutral conclusion
149
- (bypassed when Bugbot is off for the run — the default unless
150
- `CLAUDE_REVIEWS_ENABLED` lists `bugbot` — opted out via
151
- `CLAUDE_REVIEWS_DISABLED`, or proved unreachable this run).
152
- 2. The Bugbot review body on HEAD reports no findings (checked when a Bugbot
153
- review is present).
154
- 3. A CLEAN bugteam audit review sits on HEAD.
155
- 4. The Copilot review on HEAD is clean or approved (bypassed when Copilot is down
156
- or out of quota this run).
157
- 5. Zero unresolved bot review threads anywhere on the PR — counting Cursor,
158
- Claude, and Copilot authored threads. The filter is purely
159
- `isResolved == false`; `isOutdated` is informational, and the fix lens
160
- verifies each outdated thread against current HEAD like any other (the
161
- `pr-fix-protocol` skill's unresolved-thread sweep).
162
- 6. The PR is mergeable (`mergeable` true and `mergeable_state` clean).
163
- 7. No requested reviewers are still pending (bypassed when Copilot is down or out
164
- of quota this run).
154
+ `check_convergence.py` (`pr-converge/scripts/check_convergence.py`) is the single
155
+ source of truth for readiness. Ready means the script exits `0` and prints:
156
+
157
+ ```
158
+ All pre-conditions met PR is ready to mark ready.
159
+ ```
160
+
161
+ The script re-derives every condition from GitHub and prints one PASS/FAIL line
162
+ per label. The exact printed labels (script order) live in
163
+ [`pr-converge/reference/convergence-gates.md`](../../pr-converge/reference/convergence-gates.md)
164
+ § (f) Mark ready and report the seven-label block under "Exact printed labels".
165
+
166
+ The script has no Claude APPROVED review gate. Agent-side checks (Claude
167
+ reviewer presence, broader unresolved-thread sweeps) sit outside this machine
168
+ checklist.
165
169
 
166
170
  ## Audit-trail design
167
171
 
@@ -0,0 +1,44 @@
1
+ # Headless-safety preamble
2
+
3
+ Every agent prompt the workflow authors carries a headless-safety preamble: the
4
+ run is unattended, so agents never inline a destructive-command literal
5
+ (`rm -rf`, `git reset --hard`, `dd`) into a Bash command — the
6
+ `destructive_command_blocker` hook matches those patterns as raw text, and a
7
+ confirmation prompt no human can answer would stall the run.
8
+
9
+ ## Two forms
10
+
11
+ The preamble has two forms. Read-only agents — the review, verify, and utility
12
+ spawns that edit nothing — receive a trimmed form that drops the rm-shape rules,
13
+ since a read-only agent never runs `rm`. Edit agents — the fix and commit spawns
14
+ — receive the full form that carries the rm-shape rules below.
15
+
16
+ Agents verify destructive-blocker behavior through the committed test suite
17
+ (`python -m pytest`) and keep scratch work in the OS temp dir.
18
+
19
+ ## The rm auto-allow paths (full form)
20
+
21
+ The full preamble describes three auto-allow paths:
22
+
23
+ 1. **Standalone path** — a standalone Bash call whose target resolves inside the
24
+ ephemeral namespace (`/tmp`, `/temp`, the OS temp root, or the run worktree).
25
+ It fails closed on `$(...)` substitution and backtick subshells. It declines a
26
+ `$`-bearing target only when the literal path is not already under an
27
+ ephemeral root, so it does not by itself stop a `$VAR` that expands inside an
28
+ ephemeral root.
29
+ 2. **Compound path** — accepts an rm joined with benign reporting segments when
30
+ every rm target is an absolute ephemeral path. It fails closed on `$(...)`
31
+ substitution, backtick subshells, and any `$` in the target — including
32
+ `$CLAUDE_JOB_DIR`.
33
+ 3. **Cwd-scoped path** — matches only when the command itself declares an
34
+ ephemeral working directory (it `cd`s into one, or runs under one). It
35
+ resolves the target against the declared cwd, fails closed on `$(...)`,
36
+ backticks, and unknown variables, and resolves the known temporary variables
37
+ `TEMP`, `TMP`, `TMPDIR`, and `CLAUDE_JOB_DIR` to the OS temp root. Under a
38
+ declared ephemeral cwd, a bare `$CLAUDE_JOB_DIR/tmp/<name>` target and a
39
+ relative target after a `cd` are auto-allowed.
40
+
41
+ Even so, for any cleanup whose path is variable-built or whose teardown spans
42
+ multiple steps, agents author a Python helper file and run it as
43
+ `python <file>.py` — keeping every destructive literal out of a Bash command
44
+ string entirely and independent of which auto-allow path matches.
@@ -0,0 +1,88 @@
1
+ # Multiple PRs
2
+
3
+ The multi-PR run drives several draft PRs to ready in one launch:
4
+ `workflow/converge_multi.mjs` fans out one `converge.mjs` child run per PR with
5
+ `parallel()`, and every child is pinned to its own PR's worktree through the
6
+ `repoPath` it receives, so the children never share a checkout. Each child run is
7
+ the exact single-PR convergence loop — same rounds, same reuse pass, same Copilot
8
+ gate, same convergence check — one per PR at once. The children share the run's
9
+ concurrency cap, so the fan-out self-throttles rather than spawning every PR's
10
+ lenses at the same instant.
11
+
12
+ ## Multi-PR pre-flight (main session)
13
+
14
+ `EnterWorktree` puts the session on one branch only, so the multi-PR path gives
15
+ each PR its own checkout with `git worktree add`. For each PR the user named:
16
+
17
+ 1. **Resolve PR scope** as the single-PR pre-flight step 2 in
18
+ [`SKILL.md`](../SKILL.md) does: capture `owner`, `repo`, `prNumber`, and
19
+ `headRefName`; confirm the PR is a draft, and mark it draft
20
+ (`gh pr ready <n> --repo <o>/<r> --undo`) when it is already ready so the
21
+ loop owns the ready transition.
22
+ 2. **Create a worktree on the PR's head ref** and capture its absolute path. From
23
+ inside the PR's repository checkout:
24
+ `git worktree add <abs worktree path> <headRefName>` (run `git fetch origin
25
+ <headRefName>` first when the ref is not local). Put each PR's worktree under a
26
+ path carrying its PR number so the fan-out keeps them distinct. Confirm
27
+ `git -C <abs worktree path> rev-parse --abbrev-ref HEAD` equals the head ref
28
+ and its `HEAD` equals the PR head SHA.
29
+ 3. **Verify each worktree is the PR's repo (strict pre-flight):**
30
+ `python "$HOME/.claude/skills/_shared/pr-loop/scripts/preflight_worktree.py" --owner <owner> --repo <repo> --mode strict`,
31
+ run with that worktree as the working directory. A non-zero exit prints a
32
+ `PREFLIGHT_OUTCOME` line and an `ABORT` line: report it and drop that PR from
33
+ the run rather than aborting every PR.
34
+ 4. **Grant project permissions once per repository** — the single-PR pre-flight
35
+ step 4 grant covers every worktree of the same repo, so run it one time for
36
+ the repo the PRs live in.
37
+ 5. **Copilot quota pre-check once for the whole run** — run the single-PR
38
+ pre-flight step 5 check one time:
39
+ `python "$HOME/.claude/_shared/pr-loop/scripts/copilot_quota.py"`. Every PR in
40
+ the run shares one account's Copilot premium-request quota, so one check covers
41
+ them all. Exit 0 sets `copilotDisabled: false` on every PR entry below; any
42
+ non-zero exit sets `copilotDisabled: true` on every entry, so each child skips
43
+ the Copilot gate with no agent spawned.
44
+
45
+ ## Launch the multi-PR workflow
46
+
47
+ Call the `Workflow` tool against the fan-out script, passing the absolute path of
48
+ `converge.mjs` and one entry per PR:
49
+
50
+ ```
51
+ Workflow({
52
+ scriptPath: "<this skill dir>/workflow/converge_multi.mjs",
53
+ args: {
54
+ convergeScriptPath: "<this skill dir>/workflow/converge.mjs",
55
+ prs: [
56
+ { owner: "<O>", repo: "<R>", prNumber: <N1>, repoPath: "<abs worktree 1>", bugbotDisabled: false, copilotDisabled: false },
57
+ { owner: "<O>", repo: "<R>", prNumber: <N2>, repoPath: "<abs worktree 2>", bugbotDisabled: false, copilotDisabled: false }
58
+ ]
59
+ }
60
+ })
61
+ ```
62
+
63
+ `convergeScriptPath` is the absolute path to `workflow/converge.mjs` in this same
64
+ skill directory; each `repoPath` is the absolute path of the worktree that PR is
65
+ checked out in. The workflow runs in the background and notifies the session on
66
+ completion; watch live progress with `/workflows`, where each PR's child run
67
+ appears under its own group.
68
+
69
+ The workflow returns
70
+ `{ converged, prCount, convergedCount, results, allDeferredPrs, blocker }`, where
71
+ `results` is one record per PR carrying
72
+ `{ owner, repo, prNumber, converged, rounds, finalSha, blocker, deferredPrs }`.
73
+ Each record's `deferredPrs` is that PR's own list of draft hardening PRs, and
74
+ `allDeferredPrs` is every record's `deferredPrs` flattened into one list. The
75
+ top-level `converged` is true only when every PR converged.
76
+
77
+ ## Multi-PR teardown (on workflow completion)
78
+
79
+ Run the single-PR Teardown in [`SKILL.md`](../SKILL.md) once per entry in
80
+ `results`, using that PR's `owner`, `repo`, `prNumber`, and `finalSha`, and its
81
+ own worktree as the working directory. Write one durable handoff per PR entry —
82
+ each with that PR's own `--pr-number` — so a fresh session can resume any PR on
83
+ its own. Build and publish a PR's closing report
84
+ only for a PR whose `converged` is true; for a PR that returned a blocker, skip
85
+ its report and carry the blocker into the final summary. Revoke project
86
+ permissions once per repository after every PR's teardown. Then print one summary
87
+ report — a line per PR as
88
+ `#<prNumber>: <converged | blocked> — rounds <N>, final <finalSha>[, blocker <blocker>]`.
@@ -0,0 +1,84 @@
1
+ # Self-closing loop: converge the deferred PRs
2
+
3
+ Every run leaves work behind. When a round holds only code-standard findings, the
4
+ standards-deferral path opens a draft environment-hardening PR and reports it in
5
+ `deferredPrs`. That PR is itself a draft that needs to reach ready, and its own
6
+ convergence can defer more standard findings, opening more hardening PRs. The
7
+ self-closing loop drives that chain to the ground: after teardown, the
8
+ orchestrator (the session that launched the workflow) converges the deferred
9
+ PRs, then the PRs their runs defer, and so on until a generation opens none.
10
+
11
+ This loop runs by default at the end of every autoconverge run — single-PR and
12
+ multi-PR alike. It stops only when a generation's deferred-PR list comes back
13
+ empty.
14
+
15
+ ## Seed the loop
16
+
17
+ Collect the first generation of deferred PRs from the run that just finished:
18
+
19
+ - A single-PR run seeds from its `deferredPrs`.
20
+ - A multi-PR run seeds from its `allDeferredPrs`.
21
+
22
+ When the seed list is empty, the loop is already done — the run deferred nothing,
23
+ so there is nothing to converge. Report that and stop.
24
+
25
+ ## Each generation
26
+
27
+ Given a non-empty list of deferred PRs `{ owner, repo, prNumber, copilotDisabled, bugbotDisabled }`
28
+ (a generation may span more than one repository — a hardening PR lands in whichever repo owns
29
+ the surface that blocks the deferred class, so `JonEcho/llm-settings` for hooks
30
+ and `jl-cmd/claude-dev-env` for rules and skills both appear):
31
+
32
+ 1. **Check out each deferred PR.** Run the
33
+ [multi-PR pre-flight](multi-pr.md#multi-pr-pre-flight-main-session) once per
34
+ deferred PR. Because a deferred PR can live in a repo the first run never
35
+ touched, first find a local checkout of that PR's repo; when none exists,
36
+ clone it under the session temp dir, then add the per-PR worktree on the PR's
37
+ head ref. Drop any PR whose strict pre-flight fails rather than stopping the
38
+ whole generation. Grant project permissions once per repository the
39
+ generation spans.
40
+ 2. **Converge the generation.** Launch `workflow/converge_multi.mjs` with one
41
+ entry per checked-out deferred PR, exactly as the
42
+ [multi-PR launch](multi-pr.md#launch-the-multi-pr-workflow) describes. Each
43
+ child run checks Copilot and Bugbot availability through the workflow's own
44
+ preflight-git probe, carried across rounds, so a reviewer that is down or out
45
+ of quota is never spawned in any generation; the
46
+ `copilotDisabled`/`bugbotDisabled` flags each deferred PR carries seed that
47
+ check for the first round.
48
+ 3. **Tear down.** Run the [multi-PR teardown](multi-pr.md#multi-pr-teardown-on-workflow-completion)
49
+ over the generation's `results`, and revoke project permissions once per
50
+ repository.
51
+ 4. **Take the next seed.** The generation's `allDeferredPrs` is the next
52
+ generation's seed list. When it is empty, the loop is done.
53
+
54
+ Repeat from step 1 with each new seed. The depth is unbounded: the loop keeps
55
+ opening generations until one converges every deferred PR without deferring
56
+ anything new.
57
+
58
+ ## Report each generation
59
+
60
+ Log one line as each generation finishes, so a watcher sees the chain close:
61
+
62
+ ```
63
+ Self-closing generation <k>: <converged>/<total> deferred PR(s) converged, <new> new deferred PR(s) opened
64
+ ```
65
+
66
+ When the loop ends, print a final line naming the generation count and the total
67
+ deferred PRs converged across every generation.
68
+
69
+ ## Conventional titles on deferred PRs
70
+
71
+ Each hardening PR the loop opens targets a repo whose CI validates the PR title
72
+ as a Conventional Commit. The commit step's prompt directs the agent to title
73
+ the hardening PR as a Conventional Commit — a type prefix, an optional scope,
74
+ then a colon and a short summary — so a deferred PR carries a conforming title
75
+ (`feat(hooks): …`, `chore(rules): …`) before it exists. That prompt is where the
76
+ conforming title is enforced.
77
+
78
+ The `conventional_pr_title_gate` hook is a best-effort backstop on that title,
79
+ not the guarantee. It blocks a `gh pr create` with a non-conforming `--title`
80
+ only for a repo whose semantic-pull-request workflow leaves the action's
81
+ `types:` input at the default Conventional Commits list. For a repo that pins
82
+ its own explicit `types:` list — which the main target repo does in
83
+ `.github/workflows/pr-check.yml` — the hook fails open and lets the title
84
+ through, and the CI title check on GitHub has the final say.
@@ -6,6 +6,14 @@ 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
14
+ `Workflow({scriptPath, resumeFromRunId})` — completed rounds replay from the
15
+ journal. The workflow never starts a round the budget cannot finish: a
16
+ half-run round records nothing resumable and replays dirty.
9
17
  - **Iteration cap** — 20 loop iterations pass without a full convergence-check
10
18
  pass. The iteration counter increments on every pass through any phase, so a
11
19
  convergence-check gate that no round can clear (for example a `mergeable_state`
@@ -37,19 +45,6 @@ skill still runs teardown (revoke permissions, final report).
37
45
  cannot confirm the PR left draft state (`gh pr ready` errored, or the draft
38
46
  re-query still reports true). The workflow does not report `converged: true`;
39
47
  the run ends with a `blocker` naming the failed ready transition.
40
- - **Clean-audit post blocked** — every review lens is clean on HEAD, but the
41
- CLEAN bugteam review cannot be posted (the `post_audit_thread.py` post is
42
- denied, errors, or its agent dies). The convergence gate's bugteam-review
43
- check can never pass without that CLEAN review, so the run stops rather than
44
- re-converge to the iteration cap. The `blocker` names the post failure and the
45
- HEAD. Unblock a permission denial by allowing `post_audit_thread.py` with a
46
- Bash permission rule. Unblock any other failure by creating a fresh temporary
47
- file whose exact content is an empty JSON array (`[]`) and re-running the run's
48
- own `post_audit_thread.py --skill bugteam --state CLEAN` command for the
49
- lens-verified HEAD with that file as `--findings-json`; then re-run the
50
- workflow. A CLEAN post carries an empty findings array by construction, so this
51
- re-issues the run's own command for the outcome the lenses already established
52
- for that HEAD.
53
48
  - **No review lens reviewed HEAD** — a round can end with no lens having reviewed
54
49
  the HEAD three ways: the preflight resolves no SHA, every lens agent dies, or
55
50
  every lens is down or disabled. A single such round retries on the next round.
@@ -61,6 +56,19 @@ skill still runs teardown (revoke permissions, final report).
61
56
 
62
57
  ## Not a blocker (the run continues)
63
58
 
59
+ Each reviewer-down condition below skips its own convergence-check gate. The flag and the matching `CLAUDE_REVIEWS_DISABLED` token for each condition are the single flag-per-condition table in [`../../reviewer-gates/SKILL.md`](../../reviewer-gates/SKILL.md) § "Convergence-check bypass flags"; the token carries the bypass into the mark-ready blocker hook's no-flag re-check.
60
+
61
+ - **Clean-audit post bypassed** — every review lens is clean on HEAD, but the
62
+ environment refuses the CLEAN bugteam review post (the `post_audit_thread.py`
63
+ post is denied, errors, or its agent never runs). The lens results already show
64
+ this HEAD is clean, so the CLEAN post is a record-keeping artifact, not the
65
+ safety-bearing gate. The run records a `cleanAuditNote` naming the HEAD and the
66
+ reason, logs the bypass, and proceeds to the terminal Bugbot gate — the same
67
+ shape as the Bugbot-down and Copilot-down paths. The convergence check runs with
68
+ `--bugteam-post-blocked` so its bugteam CLEAN-review gate is skipped and the run
69
+ closes on the remaining signals. A round where no lens actually reviewed HEAD is
70
+ separate: that re-converges under the no-review-lens rule above, since no lens
71
+ grounded a post.
64
72
  - **Terminal Bugbot gate down or disabled** — the terminal Bugbot gate runs once
65
73
  the internal lenses are clean. When Cursor Bugbot is off for the run (the
66
74
  default unless `CLAUDE_REVIEWS_ENABLED` lists `bugbot`) or opted out via
@@ -22,10 +22,22 @@ function moduleConstantSource(constantName) {
22
22
  }
23
23
 
24
24
  const productionModule = new Function(
25
- `${functionBody('cleanAuditBlocker')}\n` + 'return { cleanAuditBlocker };',
25
+ `${functionBody('cleanAuditPostReason')}\n` +
26
+ `${functionBody('cleanAuditBypassNote')}\n` +
27
+ 'return { cleanAuditPostReason, cleanAuditBypassNote };',
26
28
  )();
27
29
 
28
- const { cleanAuditBlocker } = productionModule;
30
+ const { cleanAuditPostReason, cleanAuditBypassNote } = productionModule;
31
+
32
+ function buildResolveCleanAuditNote(logStub) {
33
+ const factory = new Function(
34
+ 'log',
35
+ 'cleanAuditPostReason',
36
+ 'cleanAuditBypassNote',
37
+ `${functionBody('resolveCleanAuditNote')}\n return resolveCleanAuditNote;`,
38
+ );
39
+ return factory(logStub, cleanAuditPostReason, cleanAuditBypassNote);
40
+ }
29
41
 
30
42
  const provenanceModule = new Function(
31
43
  `const LENS_NAMES = ${moduleConstantSource('LENS_NAMES')};\n` +
@@ -79,28 +91,22 @@ function buildRunGeneralUtilityTask(convergeAgentStub) {
79
91
 
80
92
  const CONVERGED_HEAD = 'abcdef0123456789abcdef0123456789abcdef01';
81
93
 
82
- test('cleanAuditBlocker names the reason, the HEAD, and grounded recovery for both permission and non-permission failures without any compose-by-hand language', () => {
83
- const message = cleanAuditBlocker(CONVERGED_HEAD, {
94
+ test('cleanAuditBypassNote names the HEAD and the reason and frames a recorded bypass rather than a stop', () => {
95
+ const note = cleanAuditBypassNote(CONVERGED_HEAD, {
84
96
  posted: false,
85
97
  reviewUrl: '',
86
98
  reason: 'denied by the auto mode classifier',
87
99
  });
88
- assert.match(message, /denied by the auto mode classifier/);
89
- assert.match(message, /post_audit_thread\.py/);
90
- assert.match(message, new RegExp(CONVERGED_HEAD));
91
- assert.match(message, /can never pass without it/);
92
- assert.match(message, /permission rule/);
93
- assert.match(message, /gh auth|network|script error/);
94
- assert.match(message, /re-run/);
95
- assert.match(message, /empty JSON array/, 'expected a self-contained recovery that creates a fresh empty findings file');
96
- assert.doesNotMatch(message, /the run's own empty findings file/, 'expected no reference to a findings file that may never have been created');
97
- assert.doesNotMatch(message, /by hand/i);
98
- assert.doesNotMatch(message, /compose/i);
100
+ assert.match(note, /denied by the auto mode classifier/);
101
+ assert.match(note, new RegExp(CONVERGED_HEAD));
102
+ assert.match(note, /could not be posted/);
103
+ assert.match(note, /bugteam-review gate skipped/);
104
+ assert.doesNotMatch(note, /the run stops/);
99
105
  });
100
106
 
101
- test('cleanAuditBlocker falls back to a no-result reason when the post agent died', () => {
102
- const message = cleanAuditBlocker(CONVERGED_HEAD, null);
103
- assert.match(message, /the post agent returned no result/);
107
+ test('cleanAuditPostReason reads the result reason and falls back to a no-result reason when the post agent died', () => {
108
+ assert.equal(cleanAuditPostReason(null), 'the post agent returned no result');
109
+ assert.equal(cleanAuditPostReason({ posted: false, reason: 'denied by the classifier' }), 'denied by the classifier');
104
110
  });
105
111
 
106
112
  test('the post-clean-audit task in runGeneralUtilityTask returns the CLEAN_AUDIT_SCHEMA result rather than an unused transcript', () => {
@@ -524,15 +530,16 @@ test('CLEAN_AUDIT_SCHEMA requires posted, reviewUrl, and reason', () => {
524
530
  );
525
531
  });
526
532
 
527
- test('the standards-only call site breaks with a clean-audit blocker when the post does not land', () => {
533
+ test('the standards-only call site records a clean-audit bypass and continues to BUGBOT when the post does not land', () => {
528
534
  const branch = convergeSource.slice(
529
535
  convergeSource.indexOf('if (isStandardsOnlyRound(findings)) {'),
530
536
  convergeSource.indexOf('if (findings.length > 0) {'),
531
537
  );
532
538
  assert.match(branch, /runGeneralUtilityTask\(.*'post-clean-audit'/);
533
- assert.match(branch, /if \(!auditResult\?\.posted\)/);
534
- assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
535
- assert.match(branch, /\bbreak\b/);
539
+ assert.match(branch, /cleanAuditNote = resolveCleanAuditNote\(auditResult, head, rounds\)/);
540
+ assert.match(branch, /phase = 'BUGBOT'/);
541
+ assert.doesNotMatch(branch, /blocker = cleanAuditBlocker/);
542
+ assert.doesNotMatch(branch, /\bbreak\b/);
536
543
  });
537
544
 
538
545
  test('the standards-only call site relays lens provenance, the deferred standards findings, and the deferral filing state', () => {
@@ -568,7 +575,7 @@ test('every standardsDeferralNote call site passes the shared deferral state wit
568
575
  'expected the terminal-Bugbot call site to pass buildStandardsDeferral() inline',
569
576
  );
570
577
  assert.equal(
571
- noteCalls.filter((eachCall) => /standardsDeferralNote\(copilotOutcome\.findings\.length, buildStandardsDeferral\(\)\)/.test(eachCall)).length,
578
+ noteCalls.filter((eachCall) => /standardsDeferralNote\(roundFindings\.length, buildStandardsDeferral\(\)\)/.test(eachCall)).length,
572
579
  1,
573
580
  'expected the copilot call site to pass buildStandardsDeferral() inline',
574
581
  );
@@ -578,15 +585,17 @@ test('every standardsDeferralNote call site passes the shared deferral state wit
578
585
  assert.doesNotMatch(convergeSource, /buildStandardsDeferral\(standardsOutcome\)/);
579
586
  });
580
587
 
581
- test('the all-clean call site breaks with a clean-audit blocker when the post does not land', () => {
588
+ test('the all-clean call site records a clean-audit bypass and continues to BUGBOT when the post does not land, keeping the noLensRan re-converge', () => {
582
589
  const branch = convergeSource.slice(
583
590
  convergeSource.indexOf('all lenses clean on'),
584
591
  convergeSource.indexOf("if (phase === 'BUGBOT') {"),
585
592
  );
586
593
  assert.match(branch, /runGeneralUtilityTask\(.*'post-clean-audit'/);
587
- assert.match(branch, /if \(!auditResult\?\.posted\)/);
588
- assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
589
- assert.match(branch, /\bbreak\b/);
594
+ assert.match(branch, /if \(auditResult\?\.noLensRan\)/);
595
+ assert.match(branch, /cleanAuditNote = resolveCleanAuditNote\(auditResult, head, rounds\)/);
596
+ assert.match(branch, /resetNoLensRounds\(\)/);
597
+ assert.match(branch, /phase = 'BUGBOT'/);
598
+ assert.doesNotMatch(branch, /blocker = cleanAuditBlocker/);
590
599
  });
591
600
 
592
601
  test('the all-clean call site relays lens provenance into the post-clean-audit context without a postFindings field', () => {
@@ -598,3 +607,54 @@ test('the all-clean call site relays lens provenance into the post-clean-audit c
598
607
  assert.match(branch, /lensResults: allCleanNamedLenses/);
599
608
  assert.doesNotMatch(branch, /postFindings/);
600
609
  });
610
+
611
+ test('runConvergenceCheck threads a --bugteam-post-blocked flag and an explanatory note when the bugteam post was bypassed', () => {
612
+ const body = functionBody('runConvergenceCheck');
613
+ assert.match(body, /const bugteamPostBlockedFlag = context\.bugteamPostBlocked \? ' --bugteam-post-blocked' : ''/);
614
+ assert.match(body, /context\.bugteamPostBlocked/);
615
+ assert.match(body, /\$\{bugteamPostBlockedFlag\}/);
616
+ assert.match(body, /bugteamPostBlockedNote/);
617
+ assert.match(body, /skips the bugteam CLEAN-review gate/);
618
+ });
619
+
620
+ test('resolveCleanAuditNote records a bypass note on a refused post and resets to null when a later round lands the post', () => {
621
+ const loggedLines = [];
622
+ const resolveCleanAuditNote = buildResolveCleanAuditNote((message) => loggedLines.push(message));
623
+
624
+ const bypassNote = resolveCleanAuditNote(
625
+ { posted: false, reviewUrl: '', reason: 'denied by the classifier' },
626
+ 'abc1234def5678',
627
+ 2,
628
+ );
629
+ assert.notEqual(bypassNote, null, 'a refused post records the bypass note');
630
+ assert.match(bypassNote, /denied by the classifier/);
631
+ assert.equal(loggedLines.length, 1, 'a refused post logs the bypass once');
632
+
633
+ const clearedNote = resolveCleanAuditNote(
634
+ { posted: true, reviewUrl: 'https://github.com/o/r/pull/1#pullrequestreview-9', reason: '' },
635
+ 'abc1234def5678',
636
+ 3,
637
+ );
638
+ assert.equal(clearedNote, null, 'a landed post in a later round resets the bypass note so FINALIZE runs the bugteam gate');
639
+ assert.equal(loggedLines.length, 1, 'a landed post logs no bypass line');
640
+ });
641
+
642
+ test('the finalize call derives bugteamPostBlocked from a set cleanAuditNote', () => {
643
+ assert.match(
644
+ convergeSource,
645
+ /runConvergenceCheck\(\{ head, bugbotDown, copilotDown, bugteamPostBlocked: cleanAuditNote !== null \}\)/,
646
+ );
647
+ });
648
+
649
+ test('the workflow declares cleanAuditNote and assembles it into every result via a single note-fields builder', () => {
650
+ assert.match(convergeSource, /let cleanAuditNote = null/);
651
+ assert.match(
652
+ convergeSource,
653
+ /const assembleResult = \(outcomeFields\) => \(\{\n\s*\.\.\.outcomeFields,\n\s*standardsNote,\n\s*copilotNote,\n\s*cleanAuditNote,\n\s*reuseNote,\n\s*deferredPrs,\n\s*\}\)/,
654
+ );
655
+ const assembleCalls = convergeSource.match(/return assembleResult\(/g) || [];
656
+ assert.ok(
657
+ assembleCalls.length >= 3,
658
+ 'expected the user-review, converged, and iteration-cap returns to build through assembleResult',
659
+ );
660
+ });
@@ -14,6 +14,10 @@ const skillSource = readFileSync(
14
14
  join(workflowDirectory, '..', 'SKILL.md'),
15
15
  'utf8',
16
16
  );
17
+ const headlessSafetySource = readFileSync(
18
+ join(workflowDirectory, '..', 'reference', 'headless-safety.md'),
19
+ 'utf8',
20
+ );
17
21
 
18
22
  function lensPromptBody(builderName) {
19
23
  let builderStart = convergeSource.indexOf(`function ${builderName}(`);
@@ -272,7 +276,7 @@ test('the CONVERGE fix branch does not re-assign head from the fix before re-con
272
276
 
273
277
  test('the COPILOT fix branch does not re-assign head from the fix before re-converging', () => {
274
278
  assert.doesNotMatch(
275
- fixBranchAfter('${copilotOutcome.findings.length} finding(s) — fixing and re-converging'),
279
+ fixBranchAfter('${roundFindings.length} finding(s) — fixing and re-converging'),
276
280
  /head\s*=\s*fixProgress/,
277
281
  'the CONVERGE pass it transitions to re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
278
282
  );
@@ -819,17 +823,43 @@ test('SKILL.md does not attribute the known-temp-var resolution to the standalon
819
823
  );
820
824
  });
821
825
 
822
- test('SKILL.md attributes the known-temp-var resolution to the cwd-scoped auto-allow path', () => {
823
- const tempVarSentenceMatch =
824
- /[^.]*\bTMPDIR\b[^.]*CLAUDE_JOB_DIR[^.]*\./i.exec(skillSource.replace(/\s+/g, ' '));
825
- assert.notEqual(
826
- tempVarSentenceMatch,
827
- null,
826
+ test('headless-safety.md attributes the known-temp-var resolution to the cwd-scoped auto-allow path', () => {
827
+ const standaloneSectionMatch =
828
+ /1\.\s*\*\*Standalone path\*\*[\s\S]*?(?=2\.\s*\*\*Compound path\*\*)/i.exec(
829
+ headlessSafetySource,
830
+ );
831
+ assert.notEqual(standaloneSectionMatch, null, 'expected numbered standalone path section');
832
+ assert.doesNotMatch(
833
+ standaloneSectionMatch[0],
834
+ /resolves the known temporary variables/i,
835
+ 'standalone path must not claim the known-temp-var resolution',
836
+ );
837
+
838
+ const compoundSectionMatch =
839
+ /2\.\s*\*\*Compound path\*\*[\s\S]*?(?=3\.\s*\*\*Cwd-scoped path\*\*)/i.exec(
840
+ headlessSafetySource,
841
+ );
842
+ assert.notEqual(compoundSectionMatch, null, 'expected numbered compound path section');
843
+ assert.doesNotMatch(
844
+ compoundSectionMatch[0],
845
+ /resolves the known temporary variables/i,
846
+ 'compound path must not claim the known-temp-var resolution',
847
+ );
848
+
849
+ const cwdScopedSectionMatch =
850
+ /3\.\s*\*\*Cwd-scoped path\*\*[\s\S]*?(?=\n\nEven so|\n## |$)/i.exec(
851
+ headlessSafetySource,
852
+ );
853
+ assert.notEqual(cwdScopedSectionMatch, null, 'expected numbered cwd-scoped path section');
854
+ const cwdScopedSection = cwdScopedSectionMatch[0].replace(/\s+/g, ' ');
855
+ assert.match(
856
+ cwdScopedSection,
857
+ /resolves the known temporary variables[\s\S]{0,80}\bTEMP\b[\s\S]{0,40}\bTMP\b[\s\S]{0,40}\bTMPDIR\b[\s\S]{0,40}\bCLAUDE_JOB_DIR\b/i,
828
858
  'expected a sentence describing the TEMP/TMP/TMPDIR/CLAUDE_JOB_DIR resolution',
829
859
  );
830
860
  assert.match(
831
- tempVarSentenceMatch[0],
832
- /declares? an ephemeral cwd|declared ephemeral cwd|ephemeral-cwd path|third (?:auto-allow )?path|cwd-scoped path/i,
861
+ cwdScopedSection,
862
+ /declares an ephemeral working directory|declared ephemeral cwd|cwd-scoped path/i,
833
863
  'expected the temp-var resolution to be tied to the cwd-scoped path that declares an ephemeral working directory, not the standalone or compound paths',
834
864
  );
835
865
  });
@@ -1204,7 +1234,7 @@ test('the FINALIZE phase drives the merged check and reads ready from its result
1204
1234
  const finalizeStart = convergeSource.indexOf("if (phase === 'FINALIZE') {");
1205
1235
  assert.notEqual(finalizeStart, -1, 'expected a FINALIZE phase block');
1206
1236
  const finalizeBody = convergeSource.slice(finalizeStart, finalizeStart + 1000);
1207
- assert.match(finalizeBody, /runConvergenceCheck\(\{ head, bugbotDown, copilotDown \}\)/);
1237
+ assert.match(finalizeBody, /runConvergenceCheck\(\{ head, bugbotDown, copilotDown, bugteamPostBlocked: cleanAuditNote !== null \}\)/);
1208
1238
  assert.match(finalizeBody, /classifyReadyOutcome\(finalizeResult\)/);
1209
1239
  assert.doesNotMatch(
1210
1240
  finalizeBody,