claude-dev-env 1.59.0 → 1.60.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/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +152 -0
- package/hooks/blocking/code_rules_enforcer.py +30 -15
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +43 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/package.json +1 -1
- package/rules/file-global-constants.md +7 -1
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +54 -17
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.mjs +128 -6
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/rebase/SKILL.md +2 -4
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Tests for preflight_worktree against real git working trees."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
14
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_preflight() -> ModuleType:
|
|
19
|
+
module_path = _SCRIPTS_DIR / "preflight_worktree.py"
|
|
20
|
+
spec = importlib.util.spec_from_file_location("preflight_worktree", module_path)
|
|
21
|
+
assert spec is not None
|
|
22
|
+
assert spec.loader is not None
|
|
23
|
+
module = importlib.util.module_from_spec(spec)
|
|
24
|
+
sys.modules["preflight_worktree"] = module
|
|
25
|
+
spec.loader.exec_module(module)
|
|
26
|
+
return module
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
preflight = _load_preflight()
|
|
30
|
+
|
|
31
|
+
GIT_TIMEOUT = 30
|
|
32
|
+
CLAUDE_REMOTE = "https://github.com/jl-cmd/claude-code-config.git"
|
|
33
|
+
GIT_CEILING_DIRECTORIES = "GIT_CEILING_DIRECTORIES"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture(autouse=True)
|
|
37
|
+
def _isolate_git_from_ancestor_repos(
|
|
38
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
39
|
+
) -> None:
|
|
40
|
+
monkeypatch.setenv(GIT_CEILING_DIRECTORIES, str(tmp_path))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _git(repo_dir: Path, *args: str) -> None:
|
|
44
|
+
subprocess.run(
|
|
45
|
+
["git", "-C", str(repo_dir), *args],
|
|
46
|
+
check=True,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
timeout=GIT_TIMEOUT,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _init_repo(repo_dir: Path, origin_url: str | None) -> Path:
|
|
54
|
+
repo_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
subprocess.run(
|
|
56
|
+
["git", "init", "-q", str(repo_dir)],
|
|
57
|
+
check=True,
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
timeout=GIT_TIMEOUT,
|
|
61
|
+
)
|
|
62
|
+
if origin_url is not None:
|
|
63
|
+
_git(repo_dir, "remote", "add", "origin", origin_url)
|
|
64
|
+
return repo_dir
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize(
|
|
68
|
+
("remote_url", "expected_owner", "expected_repo"),
|
|
69
|
+
[
|
|
70
|
+
(
|
|
71
|
+
"https://github.com/jl-cmd/claude-code-config.git",
|
|
72
|
+
"jl-cmd",
|
|
73
|
+
"claude-code-config",
|
|
74
|
+
),
|
|
75
|
+
(
|
|
76
|
+
"https://github.com/jl-cmd/claude-code-config",
|
|
77
|
+
"jl-cmd",
|
|
78
|
+
"claude-code-config",
|
|
79
|
+
),
|
|
80
|
+
("git@github.com:JonEcho/llm-settings.git", "jonecho", "llm-settings"),
|
|
81
|
+
(
|
|
82
|
+
"ssh://git@github.com/jl-cmd/Claude-Code-Config",
|
|
83
|
+
"jl-cmd",
|
|
84
|
+
"claude-code-config",
|
|
85
|
+
),
|
|
86
|
+
("https://github.com/jl-cmd/repo/", "jl-cmd", "repo"),
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
def test_parse_repo_identity_accepts_every_github_remote_form(
|
|
90
|
+
remote_url: str, expected_owner: str, expected_repo: str
|
|
91
|
+
) -> None:
|
|
92
|
+
identity = preflight.parse_repo_identity(remote_url)
|
|
93
|
+
assert identity is not None
|
|
94
|
+
assert identity.owner == expected_owner
|
|
95
|
+
assert identity.repo == expected_repo
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize(
|
|
99
|
+
"remote_url",
|
|
100
|
+
["https://gitlab.com/foo/bar.git", "", "not-a-url"],
|
|
101
|
+
)
|
|
102
|
+
def test_parse_repo_identity_rejects_non_github_remotes(remote_url: str) -> None:
|
|
103
|
+
assert preflight.parse_repo_identity(remote_url) is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_classify_same_repo_when_origin_matches_pr(tmp_path: Path) -> None:
|
|
107
|
+
repo_dir = _init_repo(tmp_path / "session", CLAUDE_REMOTE)
|
|
108
|
+
pr_identity = preflight.RepoIdentity("jl-cmd", "claude-code-config")
|
|
109
|
+
verdict = preflight.classify_environment(repo_dir, pr_identity)
|
|
110
|
+
assert verdict.outcome == preflight.OUTCOME_SAME_REPO
|
|
111
|
+
assert verdict.cwd_identity == pr_identity
|
|
112
|
+
assert verdict.has_healthy_worktree_machinery is True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_classify_different_repo_when_origin_is_another_github_repo(
|
|
116
|
+
tmp_path: Path,
|
|
117
|
+
) -> None:
|
|
118
|
+
repo_dir = _init_repo(
|
|
119
|
+
tmp_path / "session", "https://github.com/jonecho/llm-settings.git"
|
|
120
|
+
)
|
|
121
|
+
pr_identity = preflight.RepoIdentity("jl-cmd", "claude-code-config")
|
|
122
|
+
verdict = preflight.classify_environment(repo_dir, pr_identity)
|
|
123
|
+
assert verdict.outcome == preflight.OUTCOME_DIFFERENT_REPO
|
|
124
|
+
assert verdict.cwd_identity == preflight.RepoIdentity("jonecho", "llm-settings")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_classify_different_repo_when_origin_is_non_github(tmp_path: Path) -> None:
|
|
128
|
+
repo_dir = _init_repo(tmp_path / "session", "https://gitlab.com/foo/bar.git")
|
|
129
|
+
pr_identity = preflight.RepoIdentity("jl-cmd", "claude-code-config")
|
|
130
|
+
verdict = preflight.classify_environment(repo_dir, pr_identity)
|
|
131
|
+
assert verdict.outcome == preflight.OUTCOME_DIFFERENT_REPO
|
|
132
|
+
assert verdict.cwd_identity is None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_classify_re_rooted_when_origin_is_absent(tmp_path: Path) -> None:
|
|
136
|
+
repo_dir = _init_repo(tmp_path / "session", None)
|
|
137
|
+
pr_identity = preflight.RepoIdentity("jl-cmd", "claude-code-config")
|
|
138
|
+
verdict = preflight.classify_environment(repo_dir, pr_identity)
|
|
139
|
+
assert verdict.outcome == preflight.OUTCOME_RE_ROOTED
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_classify_re_rooted_when_directory_is_not_a_work_tree(
|
|
143
|
+
tmp_path: Path,
|
|
144
|
+
) -> None:
|
|
145
|
+
plain_dir = tmp_path / "home"
|
|
146
|
+
plain_dir.mkdir()
|
|
147
|
+
pr_identity = preflight.RepoIdentity("jl-cmd", "claude-code-config")
|
|
148
|
+
verdict = preflight.classify_environment(plain_dir, pr_identity)
|
|
149
|
+
assert verdict.outcome == preflight.OUTCOME_RE_ROOTED
|
|
150
|
+
assert verdict.has_healthy_worktree_machinery is False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.parametrize(
|
|
154
|
+
("outcome", "is_healthy", "mode", "expected_exit"),
|
|
155
|
+
[
|
|
156
|
+
("same_repo", True, "strict", 0),
|
|
157
|
+
("same_repo", True, "classify", 0),
|
|
158
|
+
("same_repo", False, "strict", 1),
|
|
159
|
+
("same_repo", False, "classify", 1),
|
|
160
|
+
("different_repo", True, "strict", 1),
|
|
161
|
+
("different_repo", True, "classify", 0),
|
|
162
|
+
("re_rooted", False, "strict", 1),
|
|
163
|
+
("re_rooted", False, "classify", 1),
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
def test_decide_exit_code_matrix(
|
|
167
|
+
outcome: str, is_healthy: bool, mode: str, expected_exit: int
|
|
168
|
+
) -> None:
|
|
169
|
+
verdict = preflight.PreflightVerdict(
|
|
170
|
+
outcome=outcome,
|
|
171
|
+
cwd_identity=None,
|
|
172
|
+
has_healthy_worktree_machinery=is_healthy,
|
|
173
|
+
)
|
|
174
|
+
assert preflight.decide_exit_code(verdict, mode) == expected_exit
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_build_report_lines_marks_outcome_and_routes_cross_repo() -> None:
|
|
178
|
+
verdict = preflight.PreflightVerdict(
|
|
179
|
+
outcome=preflight.OUTCOME_DIFFERENT_REPO,
|
|
180
|
+
cwd_identity=preflight.RepoIdentity("jonecho", "llm-settings"),
|
|
181
|
+
has_healthy_worktree_machinery=True,
|
|
182
|
+
)
|
|
183
|
+
pr_identity = preflight.RepoIdentity("jl-cmd", "claude-code-config")
|
|
184
|
+
classify_lines = preflight.build_report_lines(
|
|
185
|
+
verdict, "classify", Path("/tmp/x"), pr_identity
|
|
186
|
+
)
|
|
187
|
+
assert classify_lines[0] == "PREFLIGHT_OUTCOME=different_repo"
|
|
188
|
+
assert any("ROUTE" in line for line in classify_lines)
|
|
189
|
+
strict_lines = preflight.build_report_lines(
|
|
190
|
+
verdict, "strict", Path("/tmp/x"), pr_identity
|
|
191
|
+
)
|
|
192
|
+
assert any("ABORT" in line for line in strict_lines)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_build_report_lines_aborts_on_broken_worktree_machinery() -> None:
|
|
196
|
+
verdict = preflight.PreflightVerdict(
|
|
197
|
+
outcome=preflight.OUTCOME_SAME_REPO,
|
|
198
|
+
cwd_identity=preflight.RepoIdentity("jl-cmd", "claude-code-config"),
|
|
199
|
+
has_healthy_worktree_machinery=False,
|
|
200
|
+
)
|
|
201
|
+
pr_identity = preflight.RepoIdentity("jl-cmd", "claude-code-config")
|
|
202
|
+
lines = preflight.build_report_lines(verdict, "strict", Path("/tmp/x"), pr_identity)
|
|
203
|
+
assert lines[0] == "PREFLIGHT_OUTCOME=same_repo"
|
|
204
|
+
assert any("git worktree prune" in line for line in lines)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_main_strict_passes_in_matching_repo(
|
|
208
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
209
|
+
) -> None:
|
|
210
|
+
repo_dir = _init_repo(tmp_path / "session", CLAUDE_REMOTE)
|
|
211
|
+
monkeypatch.chdir(repo_dir)
|
|
212
|
+
exit_code = preflight.main(
|
|
213
|
+
["--owner", "jl-cmd", "--repo", "claude-code-config", "--mode", "strict"]
|
|
214
|
+
)
|
|
215
|
+
captured = capsys.readouterr()
|
|
216
|
+
assert exit_code == preflight.EXIT_PREFLIGHT_OK
|
|
217
|
+
assert "PREFLIGHT_OUTCOME=same_repo" in captured.out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_main_strict_aborts_when_session_rooted_in_other_repo(
|
|
221
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
222
|
+
) -> None:
|
|
223
|
+
repo_dir = _init_repo(
|
|
224
|
+
tmp_path / "session", "https://github.com/jonecho/llm-settings.git"
|
|
225
|
+
)
|
|
226
|
+
monkeypatch.chdir(repo_dir)
|
|
227
|
+
exit_code = preflight.main(
|
|
228
|
+
["--owner", "jl-cmd", "--repo", "claude-code-config", "--mode", "strict"]
|
|
229
|
+
)
|
|
230
|
+
captured = capsys.readouterr()
|
|
231
|
+
assert exit_code == preflight.EXIT_PREFLIGHT_ABORT
|
|
232
|
+
assert "PREFLIGHT_OUTCOME=different_repo" in captured.out
|
|
233
|
+
assert "ABORT" in captured.out
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_main_classify_routes_cross_repo_without_abort(
|
|
237
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
238
|
+
) -> None:
|
|
239
|
+
repo_dir = _init_repo(
|
|
240
|
+
tmp_path / "session", "https://github.com/jonecho/llm-settings.git"
|
|
241
|
+
)
|
|
242
|
+
monkeypatch.chdir(repo_dir)
|
|
243
|
+
exit_code = preflight.main(
|
|
244
|
+
["--owner", "jl-cmd", "--repo", "claude-code-config", "--mode", "classify"]
|
|
245
|
+
)
|
|
246
|
+
captured = capsys.readouterr()
|
|
247
|
+
assert exit_code == preflight.EXIT_PREFLIGHT_OK
|
|
248
|
+
assert "PREFLIGHT_OUTCOME=different_repo" in captured.out
|
|
249
|
+
assert "ROUTE" in captured.out
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_main_classify_aborts_when_re_rooted(
|
|
253
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
254
|
+
) -> None:
|
|
255
|
+
plain_dir = tmp_path / "home"
|
|
256
|
+
plain_dir.mkdir()
|
|
257
|
+
monkeypatch.chdir(plain_dir)
|
|
258
|
+
exit_code = preflight.main(
|
|
259
|
+
["--owner", "jl-cmd", "--repo", "claude-code-config", "--mode", "classify"]
|
|
260
|
+
)
|
|
261
|
+
captured = capsys.readouterr()
|
|
262
|
+
assert exit_code == preflight.EXIT_PREFLIGHT_ABORT
|
|
263
|
+
assert "PREFLIGHT_OUTCOME=re_rooted" in captured.out
|
|
@@ -43,7 +43,17 @@ PR's owner.
|
|
|
43
43
|
ready, mark it draft first (`gh pr ready <n> --repo <o>/<r> --undo`) so the
|
|
44
44
|
loop owns the ready transition.
|
|
45
45
|
|
|
46
|
-
3. **
|
|
46
|
+
3. **Verify the worktree is the PR's repo (strict pre-flight).** Run
|
|
47
|
+
`python "$HOME/.claude/skills/_shared/pr-loop/scripts/preflight_worktree.py" --owner <owner> --repo <repo> --mode strict`.
|
|
48
|
+
It confirms the working directory is a checkout of the PR's own repo and
|
|
49
|
+
that `git worktree` machinery is healthy, so `EnterWorktree` can create and
|
|
50
|
+
enter the branch worktree. A non-zero exit prints a `PREFLIGHT_OUTCOME` line
|
|
51
|
+
and an `ABORT` line: report that line and stop. Autoconverge runs inside the
|
|
52
|
+
PR's own repo, so a working directory rooted in a different repo (for
|
|
53
|
+
example, `claude-code-config` while the PR lives in `llm-settings`) or in no
|
|
54
|
+
git checkout at all cannot continue.
|
|
55
|
+
|
|
56
|
+
4. **Grant project permissions.**
|
|
47
57
|
`python "$HOME/.claude/skills/bugteam/scripts/grant_project_claude_permissions.py"`
|
|
48
58
|
|
|
49
59
|
## Run the workflow
|
|
@@ -87,23 +97,44 @@ round records nothing resumable and replays dirty.
|
|
|
87
97
|
here; this step builds the one-shot closing report and the seam (marker comment +
|
|
88
98
|
gist URL) a future live-dashboard reuses.
|
|
89
99
|
|
|
90
|
-
a. **Resolve
|
|
100
|
+
a. **Resolve a seed journal path.** Glob
|
|
91
101
|
`~/.claude/projects/**/workflows/wf_<runId>.json` (where `runId` is the run id
|
|
92
|
-
the `Workflow` result returned) and take the match.
|
|
102
|
+
the `Workflow` result returned) and take the match. It seeds the merge, which
|
|
103
|
+
finds every other autoconverge journal for the same PR.
|
|
93
104
|
|
|
94
|
-
b. **
|
|
105
|
+
b. **Merge the PR's runs and build the summary prompt.**
|
|
106
|
+
```
|
|
107
|
+
python "<skill>/workflow/aggregate_runs.py" \
|
|
108
|
+
--journal "<seed journal>" \
|
|
109
|
+
--pr <owner>/<repo>#<n> \
|
|
110
|
+
--work-dir "$CLAUDE_JOB_DIR/tmp/autoconverge-agg-<prNumber>" \
|
|
111
|
+
--out-prompt "$CLAUDE_JOB_DIR/tmp/autoconverge-summary-prompt-<prNumber>.txt" \
|
|
112
|
+
[--standards-note "<standardsNote>"] [--copilot-note "<copilotNote>"]
|
|
113
|
+
```
|
|
114
|
+
It prints a JSON line with `combinedJournal`, `roundCount`, `finalSha`, and
|
|
115
|
+
`findingCount`. Pass `--standards-note`/`--copilot-note` only when the workflow
|
|
116
|
+
returned those notes.
|
|
117
|
+
|
|
118
|
+
c. **Write the summary.** Spawn a `convergence-summary` agent (a `general-purpose`
|
|
119
|
+
subagent) on the text of the prompt file from step b. The agent answers with the
|
|
120
|
+
`prProblem`/`prFix`/`problemScenes`/`fixScenes`/`verdictLine`/`issueClasses` JSON
|
|
121
|
+
object; write that object to
|
|
122
|
+
`$CLAUDE_JOB_DIR/tmp/autoconverge-summary-<prNumber>.json`.
|
|
123
|
+
|
|
124
|
+
d. **Build the report.**
|
|
95
125
|
```
|
|
96
126
|
python "<skill>/workflow/render_report.py" \
|
|
97
|
-
--journal "<
|
|
127
|
+
--journal "<combinedJournal>" \
|
|
128
|
+
--summary-file "<summary json>" \
|
|
98
129
|
--out "$CLAUDE_JOB_DIR/tmp/autoconverge-report-<prNumber>.html" \
|
|
99
130
|
--pr <owner>/<repo>#<n> \
|
|
100
131
|
--final-sha <finalSha> \
|
|
101
|
-
--rounds <
|
|
102
|
-
--repo <worktree>
|
|
132
|
+
--rounds <roundCount>
|
|
103
133
|
```
|
|
104
|
-
|
|
134
|
+
Use the `combinedJournal`, `finalSha`, and `roundCount` from step b. Capture the
|
|
135
|
+
output path from stdout.
|
|
105
136
|
|
|
106
|
-
|
|
137
|
+
e. **Publish as a secret gist** by reusing `doc-gist` (do not reimplement gist
|
|
107
138
|
creation):
|
|
108
139
|
```
|
|
109
140
|
python "$HOME/.claude/skills/doc-gist/scripts/gist_upload.py" \
|
|
@@ -114,17 +145,21 @@ round records nothing resumable and replays dirty.
|
|
|
114
145
|
Capture the htmlpreview URL from stdout. The gist is secret by default; pass
|
|
115
146
|
no public flag.
|
|
116
147
|
|
|
117
|
-
|
|
148
|
+
f. **Post one idempotent PR comment.** List the PR's issue comments; if one
|
|
118
149
|
carries the marker `<!-- autoconverge-report -->`, edit it in place, otherwise
|
|
119
150
|
create a new one. The body begins with `<!-- autoconverge-report -->`, then
|
|
120
|
-
the htmlpreview link,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
151
|
+
the htmlpreview link, then a plain-language summary that mirrors the report:
|
|
152
|
+
lead with the one-sentence `verdictLine`; then the plain Problem and Fix
|
|
153
|
+
sentences (`prProblem`, `prFix`); then the issue-class list — one bullet per
|
|
154
|
+
class as `plainName (×count, status)`. Place the raw finding list as
|
|
155
|
+
`file:line — P# — title` inside a collapsed
|
|
156
|
+
`<details><summary>Raw findings</summary>…</details>` block so the comment leads
|
|
157
|
+
with the human summary. Honor the gh-body-file rule: write a BOM-free temp file
|
|
158
|
+
and pass `--body-file` to `gh issue comment`/`gh issue comment edit`, or use the
|
|
124
159
|
GitHub MCP `add_issue_comment` tool (body as a structured parameter, no
|
|
125
160
|
`--body` flag).
|
|
126
161
|
|
|
127
|
-
|
|
162
|
+
g. **Open the report in Chrome.**
|
|
128
163
|
```
|
|
129
164
|
Start-Process chrome -ArgumentList '--new-window', '<report path>'
|
|
130
165
|
```
|
|
@@ -187,6 +222,8 @@ suite (`python -m pytest`) and keep scratch work in ephemeral temp dirs.
|
|
|
187
222
|
|
|
188
223
|
- `SKILL.md` — this hub.
|
|
189
224
|
- `workflow/converge.mjs` — the convergence workflow script.
|
|
190
|
-
- `workflow/
|
|
191
|
-
- `workflow/
|
|
225
|
+
- `workflow/aggregate_runs.py` — merges every autoconverge journal for a PR into one journal and returns its deduped findings, fix summaries, round count, and final SHA.
|
|
226
|
+
- `workflow/convergence_summary.py` — builds the convergence-summary agent prompt over a PR's merged findings.
|
|
227
|
+
- `workflow/render_report.py` — builds the closing convergence insights HTML report, taking the summary from `--summary-file`.
|
|
228
|
+
- `workflow/autoconverge_report_constants/` — named constants for the report builder and the summary prompt.
|
|
192
229
|
- `reference/` — convergence definition, stop conditions, gotchas, closing report.
|
|
@@ -4,35 +4,77 @@ When an autoconverge run converges (the workflow returns `converged: true`), the
|
|
|
4
4
|
|
|
5
5
|
## Data source
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A PR's full convergence is the union of every autoconverge run it took. `aggregate_runs.py` finds all of them and merges them into one run tree the report reads:
|
|
8
8
|
|
|
9
|
-
- **Run
|
|
10
|
-
- **Agent transcripts** — `~/.claude/projects/**/subagents/workflows/<runId>/agent-<agentId>.jsonl` — one file per agent; each line is a JSON object
|
|
9
|
+
- **Run journals** — `~/.claude/projects/**/workflows/wf_<runId>.json` — each carries the PR args, round log lines, a `workflowProgress` array (one entry per agent step), and the run result. `aggregate_runs.py` picks every journal whose `args` owner/repo/prNumber match the PR and whose `workflowName` is `autoconverge`, orders them by timestamp, and concatenates their `workflowProgress` into one merged journal under the work directory.
|
|
10
|
+
- **Agent transcripts** — `~/.claude/projects/**/subagents/workflows/<runId>/agent-<agentId>.jsonl` — one file per agent; each line is a JSON object. The merge copies every referenced transcript into the merged run tree, so the report reader extracts the last `StructuredOutput` tool_use from each exactly as it would for a single run.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
The merged journal's round count is the number of `resolve-head` steps across every run, and its final SHA is the latest run's result SHA.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Convergence summary
|
|
15
|
+
|
|
16
|
+
The plain-language account of what the PR does and what the run caught is written at teardown, over the merged findings. `convergence_summary.py` builds the agent prompt from the deduped findings and the per-round fix summaries `aggregate_runs.py` returns; the teardown spawns a `convergence-summary` agent on that prompt; the agent's JSON answer is the summary. `render_report.py --summary-file <path>` reads that JSON and draws from it, so the summary needs no journal transcript of its own.
|
|
17
|
+
|
|
18
|
+
The summary carries the structured visual data the report draws:
|
|
19
|
+
|
|
20
|
+
- `prProblem` and `prFix` — one plain sentence each: the problem this PR solves and the change that solves it.
|
|
21
|
+
- `problemScenes` and `fixScenes` — short cause→effect scenes. Each scene has a `trigger`, an optional `condition`, a `result`, and a one-line `caption`. A fix scene mirrors a problem scene with a good result.
|
|
22
|
+
- `verdictLine` — one factual sentence: converged, the distinct issue-class count, all fixed or deferred.
|
|
23
|
+
- `issueClasses` — one entry per distinct kind of problem, each with a `plainName`, a `count`, a `severity`, a `category`, a `status`, a plain `cause`, a `medium` (`terminal`, `code`, or `text`) that tells the report how to draw the before/after panels, and the literal `beforeLines` and `afterLines` shown in those panels. There is one class per kind, however many kinds there are — kinds are never folded together or dropped to hit a number.
|
|
24
|
+
|
|
25
|
+
Python owns the counts, severity, and file:line; the summary agent owns the plain-language narrative. The report draws the same HTML each time it runs over the same merged journal and summary.
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
python "<skill>/workflow/render_report.py" \
|
|
18
|
-
--journal "<journal path>" \
|
|
19
|
-
--out "<output path>.html" \
|
|
20
|
-
--pr <owner>/<repo>#<n> \
|
|
21
|
-
--final-sha <sha> \
|
|
22
|
-
--rounds <N> \
|
|
23
|
-
--repo <worktree path>
|
|
24
|
-
```
|
|
27
|
+
## What the report draws
|
|
25
28
|
|
|
26
|
-
The
|
|
29
|
+
The report body, in order:
|
|
30
|
+
|
|
31
|
+
1. A title and a subtitle (owner/repo, component when it can be derived, finding count over round count, date).
|
|
32
|
+
2. A **verdict banner** — a check circle, the `verdictLine`, and a Python-computed line giving the fix-commit count and the final short SHA.
|
|
33
|
+
3. **What this PR does** — a problem card and a fix card. Each card draws its scenes as a trigger chip → optional condition → result, each with its caption. When a scene list is empty, the card falls back to drawing `prProblem` or `prFix` as a single caption line.
|
|
34
|
+
4. **What was caught — and how it looked** — a lead line stating the bug-class count, the total finding count, the round count, and the fix-commit count, then one block per issue class: a name heading with a finding-count chip, a before panel and an after panel drawn in the style its `medium` names (a dark terminal window, a light code panel, or plain lines), and a cause line that states the plain cause plus a muted note with severity, category, count, and status. When both line lists are empty, the block drops to its heading and cause line.
|
|
35
|
+
5. A footer (owner/repo, PR, findings, rounds, fix commits, generated date).
|
|
36
|
+
|
|
37
|
+
A collapsed `<details>` raw-findings appendix (`file:line — P# — title`) closes the body and keeps the engineer grounding one click away.
|
|
38
|
+
|
|
39
|
+
When the summary is absent, the report falls back to a minimal layout: the title, the subtitle, a plain run-stats note, and the collapsed raw-findings appendix.
|
|
40
|
+
|
|
41
|
+
## Building the report
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
Three steps run at teardown, each a separate process:
|
|
44
|
+
|
|
45
|
+
1. **Merge the runs and build the summary prompt.**
|
|
46
|
+
```
|
|
47
|
+
python "<skill>/workflow/aggregate_runs.py" \
|
|
48
|
+
--journal "<seed journal path>" \
|
|
49
|
+
--pr <owner>/<repo>#<n> \
|
|
50
|
+
--work-dir "<work dir>" \
|
|
51
|
+
--out-prompt "<prompt path>.txt" \
|
|
52
|
+
[--standards-note "<note>"] [--copilot-note "<note>"]
|
|
53
|
+
```
|
|
54
|
+
`--journal` is any one of the PR's journals — the seed that locates the rest. The script prints a JSON line carrying `combinedJournal`, `roundCount`, `finalSha`, and `findingCount`.
|
|
55
|
+
|
|
56
|
+
2. **Write the summary.** Spawn a `convergence-summary` agent on the prompt file's text. Its JSON answer — the `prProblem`/`prFix`/`problemScenes`/`fixScenes`/`verdictLine`/`issueClasses` object — goes to a `.json` file.
|
|
57
|
+
|
|
58
|
+
3. **Draw the report.**
|
|
59
|
+
```
|
|
60
|
+
python "<skill>/workflow/render_report.py" \
|
|
61
|
+
--journal "<combinedJournal>" \
|
|
62
|
+
--summary-file "<summary json path>" \
|
|
63
|
+
--out "<output path>.html" \
|
|
64
|
+
--pr <owner>/<repo>#<n> \
|
|
65
|
+
--final-sha <finalSha> \
|
|
66
|
+
--rounds <roundCount>
|
|
67
|
+
```
|
|
68
|
+
The script reads the merged journal and transcripts, counts findings by severity and fix commits, takes the summary from `--summary-file`, draws the visual report, and writes a self-contained HTML file. It prints the output path to stdout and exits 0 on success.
|
|
69
|
+
|
|
70
|
+
Counting is deterministic: `generated_date` comes from the journal `timestamp`, not the system clock, so the same merged journal and summary always produce the same HTML.
|
|
29
71
|
|
|
30
72
|
## Publishing
|
|
31
73
|
|
|
32
74
|
After rendering, the main session:
|
|
33
75
|
|
|
34
76
|
1. **Uploads the HTML as a secret gist** using `doc-gist/scripts/gist_upload.py --no-open`. Captures the htmlpreview URL from stdout.
|
|
35
|
-
2. **Posts one idempotent PR comment** marked with `<!-- autoconverge-report -->`. If a comment with that marker already exists on the PR, it is edited in place; otherwise a new comment is created. The
|
|
77
|
+
2. **Posts one idempotent PR comment** marked with `<!-- autoconverge-report -->`. If a comment with that marker already exists on the PR, it is edited in place; otherwise a new comment is created. The body leads with the gist URL, then the one-sentence `verdictLine`, then the plain Problem and Fix sentences (`prProblem`, `prFix`), then the issue-class list — one bullet per class as `plainName (×count, status)` — and closes with the full finding list (`file:line — P# — title`) inside a collapsed `<details>` block. Write the body to a BOM-free temp file and pass `--body-file` to `gh issue comment` (never `--body`), or use the GitHub MCP `add_issue_comment` tool.
|
|
36
78
|
3. **Opens the report** with `Start-Process chrome -ArgumentList '--new-window', '<report path>'`. A missing Chrome does not abort teardown.
|
|
37
79
|
|
|
38
80
|
## Live-dashboard seam
|