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.
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/audit-contract.md +1 -1
- package/_shared/pr-loop/gh-payloads.md +3 -3
- package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
- package/_shared/pr-loop/scripts/README.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +127 -13
- package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -69
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
- package/_shared/pr-loop/state-schema.md +3 -22
- package/bin/CLAUDE.md +1 -1
- package/bin/install.mjs +2 -7
- package/bin/install.test.mjs +10 -4
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/pii_commit_command.py +398 -0
- package/hooks/blocking/pii_payload_scan.py +133 -0
- package/hooks/blocking/pii_prevention_blocker.py +151 -449
- package/hooks/blocking/pii_scanner.py +26 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +2 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
- package/hooks/blocking/test_pii_scanner.py +27 -2
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +20 -8
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/package.json +1 -1
- package/scripts/CLAUDE.md +10 -1
- package/scripts/Get-SessionAccount.ps1 +210 -0
- package/scripts/tests/CLAUDE.md +15 -1
- package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
- package/skills/CLAUDE.md +3 -25
- package/skills/anthropic-plan/SKILL.md +1 -13
- package/skills/autoconverge/CLAUDE.md +5 -2
- package/skills/autoconverge/SKILL.md +112 -387
- package/skills/autoconverge/reference/CLAUDE.md +4 -1
- package/skills/autoconverge/reference/closing-report.md +9 -2
- package/skills/autoconverge/reference/convergence.md +26 -22
- package/skills/autoconverge/reference/headless-safety.md +44 -0
- package/skills/autoconverge/reference/multi-pr.md +88 -0
- package/skills/autoconverge/reference/self-closing-loop.md +84 -0
- package/skills/autoconverge/reference/stop-conditions.md +21 -13
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
- package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +284 -94
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/CONSTRAINTS.md +3 -3
- package/skills/bugteam/PROMPTS.md +8 -19
- package/skills/bugteam/SKILL.md +13 -14
- package/skills/bugteam/reference/CLAUDE.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
- package/skills/bugteam/reference/github-pr-reviews.md +56 -38
- package/skills/bugteam/reference/team-setup.md +11 -11
- package/skills/bugteam/scripts/CLAUDE.md +6 -6
- package/skills/bugteam/scripts/README.md +20 -8
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
- package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
- package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
- package/skills/copilot-finding-triage/SKILL.md +80 -28
- package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
- package/skills/copilot-finding-triage/templates/notification.md +8 -3
- package/skills/everything-search/SKILL.md +1 -2
- package/skills/grokify/SKILL.md +58 -0
- package/skills/grokify/templates/handoff-template.md +92 -0
- package/skills/orchestrator/SKILL.md +12 -8
- package/skills/orchestrator-refresh/SKILL.md +3 -3
- package/skills/pr-converge/SKILL.md +132 -233
- package/skills/pr-converge/reference/convergence-gates.md +102 -66
- package/skills/pr-converge/reference/examples.md +37 -25
- package/skills/pr-converge/reference/fix-protocol.md +7 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
- package/skills/pr-converge/reference/per-tick.md +64 -41
- package/skills/pr-converge/reference/state-schema.md +27 -2
- package/skills/pr-converge/scripts/CLAUDE.md +8 -0
- package/skills/pr-converge/scripts/README.md +4 -3
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
- package/skills/pr-converge/scripts/check_convergence.py +189 -645
- package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
- package/skills/pr-converge/scripts/conftest.py +8 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
- package/skills/privacy-hygiene/SKILL.md +8 -7
- package/skills/reviewer-gates/SKILL.md +26 -8
- package/skills/session-log/SKILL.md +11 -12
- package/skills/session-tidy/SKILL.md +40 -30
- package/skills/skill-builder/CLAUDE.md +13 -10
- package/skills/skill-builder/SKILL.md +31 -31
- package/skills/skill-builder/references/CLAUDE.md +10 -6
- package/skills/skill-builder/references/delegation-map.md +13 -4
- package/skills/skill-builder/references/description-field.md +113 -0
- package/skills/skill-builder/references/self-audit-checklist.md +21 -4
- package/skills/skill-builder/references/skill-modularity.md +113 -0
- package/skills/skill-builder/references/skill-types.md +4 -0
- package/skills/skill-builder/templates/CLAUDE.md +4 -4
- package/skills/skill-builder/templates/gap-analysis.md +25 -1
- package/skills/skill-builder/workflows/CLAUDE.md +6 -4
- package/skills/skill-builder/workflows/improve-skill.md +17 -16
- package/skills/skill-builder/workflows/new-skill.md +34 -10
- package/skills/skill-builder/workflows/polish-skill.md +59 -43
- package/skills/usage-pause/SKILL.md +10 -14
- package/skills/bdd-protocol/CLAUDE.md +0 -26
- package/skills/bdd-protocol/SKILL.md +0 -30
- package/skills/bdd-protocol/references/CLAUDE.md +0 -10
- package/skills/bdd-protocol/references/anti-patterns.md +0 -26
- package/skills/bdd-protocol/references/example-mapping.md +0 -23
- package/skills/bg-agent/CLAUDE.md +0 -17
- package/skills/bg-agent/SKILL.md +0 -69
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
- package/skills/caveman/CLAUDE.md +0 -15
- package/skills/caveman/SKILL.md +0 -38
- package/skills/code/CLAUDE.md +0 -17
- package/skills/code/SKILL.md +0 -178
- package/skills/deep-research/CLAUDE.md +0 -17
- package/skills/deep-research/SKILL.md +0 -80
- package/skills/findbugs/CLAUDE.md +0 -20
- package/skills/findbugs/SKILL.md +0 -241
- package/skills/fixbugs/CLAUDE.md +0 -19
- package/skills/fixbugs/SKILL.md +0 -142
- package/skills/gh-paginate/CLAUDE.md +0 -18
- package/skills/gh-paginate/SKILL.md +0 -84
- package/skills/gotcha/CLAUDE.md +0 -33
- package/skills/gotcha/SKILL.md +0 -73
- package/skills/implement/CLAUDE.md +0 -27
- package/skills/implement/SKILL.md +0 -66
- package/skills/implement/scripts/CLAUDE.md +0 -22
- package/skills/implement/scripts/append_note.py +0 -133
- package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
- package/skills/implement/scripts/test_append_note.py +0 -191
- package/skills/log-audit/CLAUDE.md +0 -20
- package/skills/log-audit/SKILL.md +0 -66
- package/skills/log-audit/reference/CLAUDE.md +0 -9
- package/skills/log-audit/reference/charter.md +0 -52
- package/skills/log-audit/scripts/CLAUDE.md +0 -27
- package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
- package/skills/log-audit/scripts/collect_log_window.py +0 -199
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
- package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
- package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
- package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
- package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
- package/skills/logifix/CLAUDE.md +0 -36
- package/skills/logifix/SKILL.md +0 -69
- package/skills/logifix/scripts/CLAUDE.md +0 -16
- package/skills/logifix/scripts/logifix.ps1 +0 -205
- package/skills/monitor-open-prs/CLAUDE.md +0 -34
- package/skills/monitor-open-prs/SKILL.md +0 -88
- package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
- package/skills/monitor-open-prs/test_skill_contract.py +0 -27
- package/skills/post-audit-findings/SKILL.md +0 -80
- package/skills/pr-consistency-audit/CLAUDE.md +0 -34
- package/skills/pr-consistency-audit/SKILL.md +0 -116
- package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
- package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
- package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
- package/skills/pr-fix-protocol/SKILL.md +0 -65
- package/skills/pr-review-responder/CLAUDE.md +0 -35
- package/skills/pr-review-responder/EXAMPLES.md +0 -590
- package/skills/pr-review-responder/PRINCIPLES.md +0 -533
- package/skills/pr-review-responder/README.md +0 -168
- package/skills/pr-review-responder/SKILL.md +0 -203
- package/skills/pr-review-responder/TESTING.md +0 -350
- package/skills/pr-scope-resolve/SKILL.md +0 -45
- package/skills/pre-compact/CLAUDE.md +0 -24
- package/skills/pre-compact/SKILL.md +0 -134
- package/skills/qbug/CLAUDE.md +0 -40
- package/skills/qbug/SKILL.md +0 -387
- package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
- package/skills/refine/CLAUDE.md +0 -44
- package/skills/refine/SKILL.md +0 -262
- package/skills/refine/templates/CLAUDE.md +0 -17
- package/skills/refine/templates/implementation-notes-template.html +0 -56
- package/skills/refine/templates/plan-template.md +0 -60
- package/skills/research-mode/CLAUDE.md +0 -35
- package/skills/research-mode/SKILL.md +0 -53
- package/skills/structure-prompt/CLAUDE.md +0 -42
- package/skills/structure-prompt/SKILL.md +0 -44
- package/skills/structure-prompt/reference/CLAUDE.md +0 -28
- package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
- package/skills/structure-prompt/reference/block-classification.md +0 -27
- package/skills/structure-prompt/reference/canonical-case.md +0 -48
- package/skills/structure-prompt/reference/citation-depth.md +0 -70
- package/skills/structure-prompt/reference/cleanup.md +0 -33
- package/skills/structure-prompt/reference/constraints.md +0 -33
- package/skills/structure-prompt/reference/directives.md +0 -37
- package/skills/structure-prompt/reference/examples.md +0 -72
- package/skills/structure-prompt/reference/instantiation.md +0 -51
- package/skills/structure-prompt/reference/output-contract.md +0 -72
- package/skills/structure-prompt/reference/per-category.md +0 -23
- package/skills/structure-prompt/reference/persona.md +0 -38
- package/skills/structure-prompt/reference/research.md +0 -33
- package/skills/structure-prompt/reference/structure.md +0 -28
- package/skills/verified-build/CLAUDE.md +0 -33
- 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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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('
|
|
25
|
+
`${functionBody('cleanAuditPostReason')}\n` +
|
|
26
|
+
`${functionBody('cleanAuditBypassNote')}\n` +
|
|
27
|
+
'return { cleanAuditPostReason, cleanAuditBypassNote };',
|
|
26
28
|
)();
|
|
27
29
|
|
|
28
|
-
const {
|
|
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('
|
|
83
|
-
const
|
|
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(
|
|
89
|
-
assert.match(
|
|
90
|
-
assert.match(
|
|
91
|
-
assert.match(
|
|
92
|
-
assert.
|
|
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('
|
|
102
|
-
|
|
103
|
-
assert.
|
|
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
|
|
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, /
|
|
534
|
-
assert.match(branch, /
|
|
535
|
-
assert.
|
|
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\(
|
|
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
|
|
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 \(
|
|
588
|
-
assert.match(branch, /
|
|
589
|
-
assert.match(branch,
|
|
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('${
|
|
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('
|
|
823
|
-
const
|
|
824
|
-
/[
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
832
|
-
/declares
|
|
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,
|