claude-dev-env 1.93.1 → 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 (215) 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/fix_hookspath.py +47 -12
  7. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
  8. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
  9. package/_shared/pr-loop/state-schema.md +3 -22
  10. package/bin/CLAUDE.md +1 -1
  11. package/bin/install.mjs +2 -7
  12. package/bin/install.test.mjs +10 -4
  13. package/hooks/blocking/CLAUDE.md +2 -0
  14. package/hooks/blocking/pii_commit_command.py +398 -0
  15. package/hooks/blocking/pii_payload_scan.py +133 -0
  16. package/hooks/blocking/pii_prevention_blocker.py +151 -449
  17. package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
  18. package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
  19. package/hooks/hooks_constants/CLAUDE.md +2 -2
  20. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
  21. package/hooks/hooks_constants/local_identity.py +65 -15
  22. package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
  23. package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
  24. package/hooks/hooks_constants/test_local_identity.py +68 -0
  25. package/package.json +1 -1
  26. package/scripts/CLAUDE.md +10 -1
  27. package/scripts/Get-SessionAccount.ps1 +210 -0
  28. package/scripts/tests/CLAUDE.md +15 -1
  29. package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
  30. package/skills/CLAUDE.md +3 -25
  31. package/skills/anthropic-plan/SKILL.md +1 -13
  32. package/skills/autoconverge/CLAUDE.md +5 -2
  33. package/skills/autoconverge/SKILL.md +112 -387
  34. package/skills/autoconverge/reference/CLAUDE.md +4 -1
  35. package/skills/autoconverge/reference/closing-report.md +9 -2
  36. package/skills/autoconverge/reference/convergence.md +26 -22
  37. package/skills/autoconverge/reference/headless-safety.md +44 -0
  38. package/skills/autoconverge/reference/multi-pr.md +88 -0
  39. package/skills/autoconverge/reference/self-closing-loop.md +84 -0
  40. package/skills/autoconverge/reference/stop-conditions.md +21 -13
  41. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
  42. package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
  43. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
  44. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  45. package/skills/autoconverge/workflow/converge.mjs +284 -94
  46. package/skills/bugteam/CLAUDE.md +1 -1
  47. package/skills/bugteam/CONSTRAINTS.md +3 -3
  48. package/skills/bugteam/PROMPTS.md +8 -19
  49. package/skills/bugteam/SKILL.md +13 -14
  50. package/skills/bugteam/reference/CLAUDE.md +1 -1
  51. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  52. package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
  53. package/skills/bugteam/reference/github-pr-reviews.md +56 -38
  54. package/skills/bugteam/reference/team-setup.md +11 -11
  55. package/skills/bugteam/scripts/CLAUDE.md +6 -6
  56. package/skills/bugteam/scripts/README.md +20 -8
  57. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
  58. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
  59. package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
  60. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
  61. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
  62. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
  63. package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
  64. package/skills/copilot-finding-triage/SKILL.md +80 -28
  65. package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
  66. package/skills/copilot-finding-triage/templates/notification.md +8 -3
  67. package/skills/everything-search/SKILL.md +1 -2
  68. package/skills/grokify/SKILL.md +58 -0
  69. package/skills/grokify/templates/handoff-template.md +92 -0
  70. package/skills/orchestrator/SKILL.md +12 -8
  71. package/skills/orchestrator-refresh/SKILL.md +3 -3
  72. package/skills/pr-converge/SKILL.md +132 -233
  73. package/skills/pr-converge/reference/convergence-gates.md +102 -66
  74. package/skills/pr-converge/reference/examples.md +37 -25
  75. package/skills/pr-converge/reference/fix-protocol.md +7 -7
  76. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
  77. package/skills/pr-converge/reference/per-tick.md +64 -41
  78. package/skills/pr-converge/reference/state-schema.md +27 -2
  79. package/skills/pr-converge/scripts/CLAUDE.md +8 -0
  80. package/skills/pr-converge/scripts/README.md +4 -3
  81. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
  82. package/skills/pr-converge/scripts/check_convergence.py +189 -645
  83. package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
  84. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
  85. package/skills/pr-converge/scripts/conftest.py +8 -0
  86. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
  87. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
  88. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
  89. package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
  90. package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
  91. package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
  92. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
  93. package/skills/privacy-hygiene/SKILL.md +4 -3
  94. package/skills/reviewer-gates/SKILL.md +26 -8
  95. package/skills/session-log/SKILL.md +11 -12
  96. package/skills/session-tidy/SKILL.md +40 -30
  97. package/skills/skill-builder/CLAUDE.md +13 -10
  98. package/skills/skill-builder/SKILL.md +31 -31
  99. package/skills/skill-builder/references/CLAUDE.md +10 -6
  100. package/skills/skill-builder/references/delegation-map.md +13 -4
  101. package/skills/skill-builder/references/description-field.md +113 -0
  102. package/skills/skill-builder/references/self-audit-checklist.md +21 -4
  103. package/skills/skill-builder/references/skill-modularity.md +113 -0
  104. package/skills/skill-builder/references/skill-types.md +4 -0
  105. package/skills/skill-builder/templates/CLAUDE.md +4 -4
  106. package/skills/skill-builder/templates/gap-analysis.md +25 -1
  107. package/skills/skill-builder/workflows/CLAUDE.md +6 -4
  108. package/skills/skill-builder/workflows/improve-skill.md +17 -16
  109. package/skills/skill-builder/workflows/new-skill.md +34 -10
  110. package/skills/skill-builder/workflows/polish-skill.md +59 -43
  111. package/skills/usage-pause/SKILL.md +10 -14
  112. package/skills/bdd-protocol/CLAUDE.md +0 -26
  113. package/skills/bdd-protocol/SKILL.md +0 -30
  114. package/skills/bdd-protocol/references/CLAUDE.md +0 -10
  115. package/skills/bdd-protocol/references/anti-patterns.md +0 -26
  116. package/skills/bdd-protocol/references/example-mapping.md +0 -23
  117. package/skills/bg-agent/CLAUDE.md +0 -17
  118. package/skills/bg-agent/SKILL.md +0 -69
  119. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
  120. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
  121. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
  122. package/skills/caveman/CLAUDE.md +0 -15
  123. package/skills/caveman/SKILL.md +0 -38
  124. package/skills/code/CLAUDE.md +0 -17
  125. package/skills/code/SKILL.md +0 -178
  126. package/skills/deep-research/CLAUDE.md +0 -17
  127. package/skills/deep-research/SKILL.md +0 -80
  128. package/skills/findbugs/CLAUDE.md +0 -20
  129. package/skills/findbugs/SKILL.md +0 -241
  130. package/skills/fixbugs/CLAUDE.md +0 -19
  131. package/skills/fixbugs/SKILL.md +0 -142
  132. package/skills/gh-paginate/CLAUDE.md +0 -18
  133. package/skills/gh-paginate/SKILL.md +0 -84
  134. package/skills/gotcha/CLAUDE.md +0 -33
  135. package/skills/gotcha/SKILL.md +0 -73
  136. package/skills/implement/CLAUDE.md +0 -27
  137. package/skills/implement/SKILL.md +0 -66
  138. package/skills/implement/scripts/CLAUDE.md +0 -22
  139. package/skills/implement/scripts/append_note.py +0 -133
  140. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
  141. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  142. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
  143. package/skills/implement/scripts/test_append_note.py +0 -191
  144. package/skills/log-audit/CLAUDE.md +0 -20
  145. package/skills/log-audit/SKILL.md +0 -66
  146. package/skills/log-audit/reference/CLAUDE.md +0 -9
  147. package/skills/log-audit/reference/charter.md +0 -52
  148. package/skills/log-audit/scripts/CLAUDE.md +0 -27
  149. package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
  150. package/skills/log-audit/scripts/collect_log_window.py +0 -199
  151. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
  152. package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
  153. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
  154. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
  155. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
  156. package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
  157. package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
  158. package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
  159. package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
  160. package/skills/logifix/CLAUDE.md +0 -36
  161. package/skills/logifix/SKILL.md +0 -69
  162. package/skills/logifix/scripts/CLAUDE.md +0 -16
  163. package/skills/logifix/scripts/logifix.ps1 +0 -205
  164. package/skills/monitor-open-prs/CLAUDE.md +0 -34
  165. package/skills/monitor-open-prs/SKILL.md +0 -88
  166. package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
  167. package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
  168. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -27
  170. package/skills/post-audit-findings/SKILL.md +0 -80
  171. package/skills/pr-consistency-audit/CLAUDE.md +0 -34
  172. package/skills/pr-consistency-audit/SKILL.md +0 -116
  173. package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
  174. package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
  175. package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
  176. package/skills/pr-fix-protocol/SKILL.md +0 -65
  177. package/skills/pr-review-responder/CLAUDE.md +0 -35
  178. package/skills/pr-review-responder/EXAMPLES.md +0 -590
  179. package/skills/pr-review-responder/PRINCIPLES.md +0 -533
  180. package/skills/pr-review-responder/README.md +0 -168
  181. package/skills/pr-review-responder/SKILL.md +0 -203
  182. package/skills/pr-review-responder/TESTING.md +0 -350
  183. package/skills/pr-scope-resolve/SKILL.md +0 -45
  184. package/skills/pre-compact/CLAUDE.md +0 -24
  185. package/skills/pre-compact/SKILL.md +0 -134
  186. package/skills/qbug/CLAUDE.md +0 -40
  187. package/skills/qbug/SKILL.md +0 -387
  188. package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
  189. package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
  190. package/skills/refine/CLAUDE.md +0 -44
  191. package/skills/refine/SKILL.md +0 -262
  192. package/skills/refine/templates/CLAUDE.md +0 -17
  193. package/skills/refine/templates/implementation-notes-template.html +0 -56
  194. package/skills/refine/templates/plan-template.md +0 -60
  195. package/skills/research-mode/CLAUDE.md +0 -35
  196. package/skills/research-mode/SKILL.md +0 -53
  197. package/skills/structure-prompt/CLAUDE.md +0 -42
  198. package/skills/structure-prompt/SKILL.md +0 -44
  199. package/skills/structure-prompt/reference/CLAUDE.md +0 -28
  200. package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
  201. package/skills/structure-prompt/reference/block-classification.md +0 -27
  202. package/skills/structure-prompt/reference/canonical-case.md +0 -48
  203. package/skills/structure-prompt/reference/citation-depth.md +0 -70
  204. package/skills/structure-prompt/reference/cleanup.md +0 -33
  205. package/skills/structure-prompt/reference/constraints.md +0 -33
  206. package/skills/structure-prompt/reference/directives.md +0 -37
  207. package/skills/structure-prompt/reference/examples.md +0 -72
  208. package/skills/structure-prompt/reference/instantiation.md +0 -51
  209. package/skills/structure-prompt/reference/output-contract.md +0 -72
  210. package/skills/structure-prompt/reference/per-category.md +0 -23
  211. package/skills/structure-prompt/reference/persona.md +0 -38
  212. package/skills/structure-prompt/reference/research.md +0 -33
  213. package/skills/structure-prompt/reference/structure.md +0 -28
  214. package/skills/verified-build/CLAUDE.md +0 -33
  215. package/skills/verified-build/SKILL.md +0 -33
@@ -0,0 +1,321 @@
1
+ """Contract-pin and flag tests for check_convergence.
2
+
3
+ ::
4
+
5
+ check_all(all gates passing) -> exit 0, "All pre-conditions met ..."
6
+ check_all(one gate failing) -> exit 1, "One or more ... do not mark ready."
7
+ _get_pr_head_sha raises SystemExit -> exit 2 propagates
8
+ parse_arguments([... --bugteam-post-blocked]) -> bugteam_post_blocked True
9
+
10
+ The pin tests fix the stdout the four consumers parse (converge.mjs, pr-converge,
11
+ bugteam, qbug) byte-for-byte, plus the exit codes and CLI surface. The flag tests
12
+ drive the ``--bugteam-post-blocked`` bypass.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import importlib.util
18
+ import sys
19
+ from pathlib import Path
20
+ from types import ModuleType
21
+
22
+ import pytest
23
+
24
+ import _pr_converge_path_setup # noqa: F401
25
+ from pr_converge_skill_constants.constants import EXIT_CODE_GH_ERROR
26
+
27
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
28
+ _PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
29
+
30
+ CURRENT_HEAD_SHA = "abcdef1234567890abcdef1234567890abcdef12"
31
+
32
+ _ALL_LEAF_GATE_NAMES = [
33
+ "_check_bugbot",
34
+ "_check_bugbot_not_dirty",
35
+ "_check_bugteam_clean",
36
+ "_check_bot_review",
37
+ "_count_unresolved_bot_threads",
38
+ "_get_mergeable",
39
+ "_check_no_pending_reviews",
40
+ ]
41
+
42
+ EXPECTED_ALL_PASS_STDOUT = (
43
+ "HEAD: abcdef1\n\n"
44
+ "1. bugbot_clean_at == current_head: PASS — clean\n"
45
+ "2. bugbot review body clean: PASS — clean\n"
46
+ "3. bugteam_clean_at == current_head: PASS — clean\n"
47
+ "4. copilot_clean_at == current_head: PASS — clean\n"
48
+ "5. zero unresolved bot threads: PASS — clean\n"
49
+ "6. PR is mergeable: PASS — clean\n"
50
+ "7. no pending requested reviews: PASS — clean\n\n"
51
+ "All pre-conditions met — PR is ready to mark ready.\n"
52
+ )
53
+
54
+ EXPECTED_BUGBOT_DOWN_STDOUT = (
55
+ "HEAD: abcdef1\n\n"
56
+ "1. bugbot_clean_at == current_head: PASS — bypassed (bugbot_down)\n"
57
+ "2. bugteam_clean_at == current_head: PASS — clean\n"
58
+ "3. copilot_clean_at == current_head: PASS — clean\n"
59
+ "4. zero unresolved bot threads: PASS — clean\n"
60
+ "5. PR is mergeable: PASS — clean\n"
61
+ "6. no pending requested reviews: PASS — clean\n\n"
62
+ "All pre-conditions met — PR is ready to mark ready.\n"
63
+ )
64
+
65
+ EXPECTED_COPILOT_DOWN_STDOUT = (
66
+ "HEAD: abcdef1\n\n"
67
+ "1. bugbot_clean_at == current_head: PASS — clean\n"
68
+ "2. bugbot review body clean: PASS — clean\n"
69
+ "3. bugteam_clean_at == current_head: PASS — clean\n"
70
+ "4. copilot_clean_at == current_head: PASS — bypassed (copilot_down)\n"
71
+ "5. zero unresolved bot threads: PASS — clean\n"
72
+ "6. PR is mergeable: PASS — clean\n"
73
+ "7. no pending requested reviews: PASS — bypassed (copilot_down)\n\n"
74
+ "All pre-conditions met — PR is ready to mark ready.\n"
75
+ )
76
+
77
+ EXPECTED_FAIL_SUMMARY = "One or more pre-conditions not met — do not mark ready.\n"
78
+
79
+
80
+ def _load_check_convergence() -> ModuleType:
81
+ if str(_PR_CONVERGE_DIRECTORY) not in sys.path:
82
+ sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
83
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
84
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
85
+ module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
86
+ spec = importlib.util.spec_from_file_location(
87
+ "check_convergence_contract_under_test", module_path
88
+ )
89
+ assert spec is not None
90
+ assert spec.loader is not None
91
+ module = importlib.util.module_from_spec(spec)
92
+ spec.loader.exec_module(module)
93
+ return module
94
+
95
+
96
+ check_convergence = _load_check_convergence()
97
+
98
+
99
+ def _clean_head(**_call_keywords: object) -> str:
100
+ return CURRENT_HEAD_SHA
101
+
102
+
103
+ def _clean_gate(**_call_keywords: object) -> tuple[bool, str]:
104
+ return True, "clean"
105
+
106
+
107
+ def _refuse_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
108
+ raise AssertionError(
109
+ "_check_bugteam_clean must not run when is_bugteam_post_blocked=True"
110
+ )
111
+
112
+
113
+ def _patch_gates_clean(monkeypatch: pytest.MonkeyPatch) -> None:
114
+ monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _clean_head)
115
+ for each_gate_name in _ALL_LEAF_GATE_NAMES:
116
+ monkeypatch.setattr(check_convergence, each_gate_name, _clean_gate)
117
+
118
+
119
+ def test_all_pass_stdout_and_exit_code_match_the_pinned_contract(
120
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
121
+ ) -> None:
122
+ _patch_gates_clean(monkeypatch)
123
+ exit_code = check_convergence.check_all(
124
+ owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
125
+ )
126
+ assert capsys.readouterr().out == EXPECTED_ALL_PASS_STDOUT
127
+ assert exit_code == 0
128
+
129
+
130
+ def test_bugbot_down_bypass_line_matches_the_pinned_contract(
131
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
132
+ ) -> None:
133
+ _patch_gates_clean(monkeypatch)
134
+ exit_code = check_convergence.check_all(
135
+ owner="o", repo="r", number=1, is_bugbot_down=True, is_copilot_down=False
136
+ )
137
+ assert capsys.readouterr().out == EXPECTED_BUGBOT_DOWN_STDOUT
138
+ assert exit_code == 0
139
+
140
+
141
+ def test_copilot_down_bypass_lines_match_the_pinned_contract(
142
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
143
+ ) -> None:
144
+ _patch_gates_clean(monkeypatch)
145
+ exit_code = check_convergence.check_all(
146
+ owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=True
147
+ )
148
+ assert capsys.readouterr().out == EXPECTED_COPILOT_DOWN_STDOUT
149
+ assert exit_code == 0
150
+
151
+
152
+ def test_a_failing_gate_yields_exit_one_and_the_fail_summary(
153
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
154
+ ) -> None:
155
+ _patch_gates_clean(monkeypatch)
156
+
157
+ def _blocked_mergeable(**_call_keywords: object) -> tuple[bool, str]:
158
+ return False, "blocked"
159
+
160
+ monkeypatch.setattr(check_convergence, "_get_mergeable", _blocked_mergeable)
161
+ exit_code = check_convergence.check_all(
162
+ owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
163
+ )
164
+ captured_stdout = capsys.readouterr().out
165
+ assert "6. PR is mergeable: FAIL — blocked\n" in captured_stdout
166
+ assert captured_stdout.endswith(EXPECTED_FAIL_SUMMARY)
167
+ assert exit_code == 1
168
+
169
+
170
+ def test_gh_error_exit_code_is_two_and_propagates_from_head_fetch(
171
+ monkeypatch: pytest.MonkeyPatch,
172
+ ) -> None:
173
+ def _raise_gh_error(**_call_keywords: object) -> str:
174
+ raise SystemExit(EXIT_CODE_GH_ERROR)
175
+
176
+ monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _raise_gh_error)
177
+ with pytest.raises(SystemExit) as raised:
178
+ check_convergence.check_all(
179
+ owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
180
+ )
181
+ assert EXIT_CODE_GH_ERROR == 2
182
+ assert raised.value.code == 2
183
+
184
+
185
+ def test_existing_cli_flags_stay_on_the_argument_surface() -> None:
186
+ arguments = check_convergence.parse_arguments(
187
+ [
188
+ "--owner",
189
+ "o",
190
+ "--repo",
191
+ "r",
192
+ "--pr-number",
193
+ "1",
194
+ "--bugbot-down",
195
+ "--copilot-down",
196
+ ]
197
+ )
198
+ assert arguments.owner == "o"
199
+ assert arguments.repo == "r"
200
+ assert getattr(arguments, "pr_number") == 1
201
+ assert arguments.bugbot_down is True
202
+ assert arguments.copilot_down is True
203
+
204
+
205
+ def test_skip_bugteam_gate_when_bugteam_post_blocked_is_true(
206
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
207
+ ) -> None:
208
+ _patch_gates_clean(monkeypatch)
209
+ monkeypatch.setattr(
210
+ check_convergence, "_check_bugteam_clean", _refuse_bugteam_clean
211
+ )
212
+ exit_code = check_convergence.check_all(
213
+ owner="o",
214
+ repo="r",
215
+ number=1,
216
+ is_bugbot_down=False,
217
+ is_copilot_down=False,
218
+ is_bugteam_post_blocked=True,
219
+ )
220
+ assert "bypassed (bugteam_post_blocked)" in capsys.readouterr().out
221
+ assert exit_code == 0
222
+
223
+
224
+ def test_run_bugteam_gate_when_bugteam_post_blocked_is_false(
225
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
226
+ ) -> None:
227
+ invoked_gate_names: list[str] = []
228
+
229
+ def _recording_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
230
+ invoked_gate_names.append("_check_bugteam_clean")
231
+ return True, "clean"
232
+
233
+ _patch_gates_clean(monkeypatch)
234
+ monkeypatch.setattr(
235
+ check_convergence, "_check_bugteam_clean", _recording_bugteam_clean
236
+ )
237
+ exit_code = check_convergence.check_all(
238
+ owner="o",
239
+ repo="r",
240
+ number=1,
241
+ is_bugbot_down=False,
242
+ is_copilot_down=False,
243
+ is_bugteam_post_blocked=False,
244
+ )
245
+ assert "_check_bugteam_clean" in invoked_gate_names
246
+ assert "bypassed (bugteam_post_blocked)" not in capsys.readouterr().out
247
+ assert exit_code == 0
248
+
249
+
250
+ def test_bugteam_post_blocked_flag_round_trips_through_parse_arguments() -> None:
251
+ arguments = check_convergence.parse_arguments(
252
+ ["--owner", "o", "--repo", "r", "--pr-number", "1", "--bugteam-post-blocked"]
253
+ )
254
+ assert arguments.bugteam_post_blocked is True
255
+
256
+
257
+ def test_bugteam_post_blocked_defaults_to_false_when_flag_absent() -> None:
258
+ arguments = check_convergence.parse_arguments(
259
+ ["--owner", "o", "--repo", "r", "--pr-number", "1"]
260
+ )
261
+ assert arguments.bugteam_post_blocked is False
262
+
263
+
264
+ def test_resolve_bugteam_post_blocked_true_when_flag_set(
265
+ monkeypatch: pytest.MonkeyPatch,
266
+ ) -> None:
267
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
268
+ assert check_convergence._resolve_bugteam_post_blocked(True) is True
269
+
270
+
271
+ def test_resolve_bugteam_post_blocked_true_when_env_disables_bugteam(
272
+ monkeypatch: pytest.MonkeyPatch,
273
+ ) -> None:
274
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
275
+ assert check_convergence._resolve_bugteam_post_blocked(False) is True
276
+
277
+
278
+ def test_resolve_bugteam_post_blocked_false_when_flag_unset_and_env_empty(
279
+ monkeypatch: pytest.MonkeyPatch,
280
+ ) -> None:
281
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
282
+ assert check_convergence._resolve_bugteam_post_blocked(False) is False
283
+
284
+
285
+ def test_resolve_bugteam_post_blocked_false_when_env_disables_only_copilot(
286
+ monkeypatch: pytest.MonkeyPatch,
287
+ ) -> None:
288
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
289
+ assert check_convergence._resolve_bugteam_post_blocked(False) is False
290
+
291
+
292
+ def test_env_bugteam_token_does_not_disable_copilot_or_bugbot_opt_out(
293
+ monkeypatch: pytest.MonkeyPatch,
294
+ ) -> None:
295
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
296
+ assert check_convergence._resolve_copilot_down(False) is False
297
+ assert check_convergence._resolve_bugteam_post_blocked(False) is True
298
+
299
+
300
+ def test_main_derives_bugteam_post_blocked_from_env_when_flag_omitted(
301
+ monkeypatch: pytest.MonkeyPatch,
302
+ ) -> None:
303
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
304
+ captured_bugteam_post_blocked: list[bool] = []
305
+
306
+ def stub_check_all(
307
+ *,
308
+ owner: str,
309
+ repo: str,
310
+ number: int,
311
+ is_bugbot_down: bool,
312
+ is_copilot_down: bool,
313
+ is_bugteam_post_blocked: bool = False,
314
+ ) -> int:
315
+ captured_bugteam_post_blocked.append(is_bugteam_post_blocked)
316
+ return 0
317
+
318
+ monkeypatch.setattr(check_convergence, "check_all", stub_check_all)
319
+ exit_code = check_convergence.main(["--owner", "o", "--repo", "r", "--pr-number", "1"])
320
+ assert exit_code == 0
321
+ assert captured_bugteam_post_blocked == [True]
@@ -0,0 +1,68 @@
1
+ """Behavioral tests for the review and Bugbot convergence gate leaves.
2
+
3
+ ::
4
+
5
+ _flatten_paginated_reviews(two pages) -> newest-first flat list
6
+ _bugbot_run_conclusion_detail(success) -> (True, "check run ...")
7
+ _check_bugbot(named complete run) -> (True, "check run ...")
8
+
9
+ Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ import pytest
17
+
18
+ import check_convergence_gates as gates
19
+
20
+
21
+ def test_flatten_paginated_reviews_flattens_and_sorts_newest_first() -> None:
22
+ pages = [
23
+ [{"id": 1, "submitted_at": "2026-01-01T00:00:00Z"}],
24
+ [{"id": 2, "submitted_at": "2026-03-01T00:00:00Z"}],
25
+ ]
26
+ flattened = gates._flatten_paginated_reviews(json.dumps(pages))
27
+ assert flattened is not None
28
+ assert [each_review["id"] for each_review in flattened] == [2, 1]
29
+
30
+
31
+ def test_flatten_paginated_reviews_returns_none_for_non_list_payload() -> None:
32
+ assert (
33
+ gates._flatten_paginated_reviews(json.dumps({"message": "Not Found"})) is None
34
+ )
35
+
36
+
37
+ def test_bugbot_run_conclusion_detail_passes_on_a_complete_conclusion() -> None:
38
+ complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
39
+ passed, detail = gates._bugbot_run_conclusion_detail(
40
+ {"id": 55, "conclusion": complete_conclusion, "html_url": ""}
41
+ )
42
+ assert passed is True
43
+ assert "check run 55" in detail
44
+
45
+
46
+ def test_check_bugbot_reports_success_when_the_named_run_is_complete(
47
+ monkeypatch: pytest.MonkeyPatch,
48
+ ) -> None:
49
+ complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
50
+ run_name = f"x {gates.BUGBOT_CHECK_RUN_NAME_SUBSTRING} y"
51
+ payload = {
52
+ "check_runs": [
53
+ {
54
+ "name": run_name,
55
+ "id": 9,
56
+ "conclusion": complete_conclusion,
57
+ "html_url": "",
58
+ }
59
+ ]
60
+ }
61
+
62
+ def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
63
+ return 0, json.dumps(payload)
64
+
65
+ monkeypatch.setattr(gates, "_gh_api", _stub_gh_api)
66
+ passed, detail = gates._check_bugbot(owner="o", repo="r", sha="abc")
67
+ assert passed is True
68
+ assert "check run 9" in detail
@@ -0,0 +1,75 @@
1
+ """Behavioral tests for the review-thread and pending-reviewer gate leaves.
2
+
3
+ ::
4
+
5
+ _is_unresolved_bot_thread(cursor, unresolved) -> True
6
+ _count_unresolved_bot_threads(one cursor thread) -> (False, "1 unresolved ...")
7
+ _format_unresolved_detail(over limit) -> "... and N more"
8
+
9
+ Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ import pytest
17
+
18
+ import check_convergence_thread_gates as thread_gates
19
+
20
+
21
+ def test_is_unresolved_bot_thread_true_for_cursor_authored_open_thread() -> None:
22
+ thread = {
23
+ "isResolved": False,
24
+ "isOutdated": False,
25
+ "comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
26
+ }
27
+ logins = (thread_gates.CURSOR_LOGIN_FILTER_SUBSTRING,)
28
+ assert thread_gates._is_unresolved_bot_thread(thread, logins) is True
29
+
30
+
31
+ def test_is_unresolved_bot_thread_false_for_resolved_thread() -> None:
32
+ thread = {
33
+ "isResolved": True,
34
+ "isOutdated": False,
35
+ "comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
36
+ }
37
+ logins = (thread_gates.CURSOR_LOGIN_FILTER_SUBSTRING,)
38
+ assert thread_gates._is_unresolved_bot_thread(thread, logins) is False
39
+
40
+
41
+ def test_count_unresolved_bot_threads_counts_a_single_cursor_thread(
42
+ monkeypatch: pytest.MonkeyPatch,
43
+ ) -> None:
44
+ thread = {
45
+ "isResolved": False,
46
+ "isOutdated": False,
47
+ "path": "src/app.py",
48
+ "comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
49
+ }
50
+ review_threads = {
51
+ "nodes": [thread],
52
+ "pageInfo": {"hasNextPage": False, "endCursor": None},
53
+ }
54
+ graphql_payload = {
55
+ "data": {"repository": {"pullRequest": {"reviewThreads": review_threads}}}
56
+ }
57
+
58
+ def _stub_gh_graphql(query: str, variables: dict[str, object]) -> tuple[int, str]:
59
+ return 0, json.dumps(graphql_payload)
60
+
61
+ monkeypatch.setattr(thread_gates, "_gh_graphql", _stub_gh_graphql)
62
+ passed, detail = thread_gates._count_unresolved_bot_threads(
63
+ owner="o", repo="r", number=1
64
+ )
65
+ assert passed is False
66
+ assert detail.startswith("1 unresolved")
67
+ assert "src/app.py" in detail
68
+
69
+
70
+ def test_format_unresolved_detail_bounds_the_listed_paths() -> None:
71
+ over_limit = thread_gates.UNRESOLVED_THREAD_DETAIL_MAX + 2
72
+ all_threads = [{"path": f"file{each_index}.py"} for each_index in range(over_limit)]
73
+ detail = thread_gates._format_unresolved_detail(all_threads)
74
+ assert detail.startswith(f"{over_limit} unresolved")
75
+ assert "more" in detail
@@ -22,8 +22,8 @@ Find and remove personal data and high-confidence secrets before they land in gi
22
22
 
23
23
  | Category | Blocked examples | Allowed residual |
24
24
  |---|---|---|
25
- | Email | `person@company.io` | `user@example.com`, `user@example.org`, `user@example.net` |
26
- | Home path | `C:/Users/realname/...`, `/Users/realname/...`, `/home/realname/...` | `C:/Users/example/...`, `C:/Users/<you>/...`, `/Users/alice/...` |
25
+ | Email | `user@example.com` | `user@example.com`, `user@example.org`, `user@example.net` |
26
+ | Home path | `C:/Users/example/...`, `/Users/example/...`, `/home/example/...` | `C:/Users/example/...`, `C:/Users/<you>/...`, `/Users/alice/...` |
27
27
  | LAN address | Unlisted `10.x` / `172.16–31.x` / `192.168.x` | Public addresses; your NAS host from `CLAUDE_NAS_HOST` or `~/.claude/local-identity.json`; entries in `ALL_ALLOWLISTED_PRIVATE_IP_ADDRESSES` |
28
28
  | Secret | `ghp_…`, `github_pat_…`, `AKIA…`, PEM private-key headers | Public keys, redacted `***`, env var names without values |
29
29
 
@@ -55,7 +55,7 @@ git diff --name-only origin/main...HEAD
55
55
  Run from the repo root (PowerShell-friendly example):
56
56
 
57
57
  ```
58
- rg -n --hidden -g '!node_modules' -g '!.git' -e '@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' -e 'Users[/\\][^/\\]+' -e '/home/[^/]+' -e '\b(10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+)\b' -e '\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b' -e '\bgithub_pat_[A-Za-z0-9_]{20,}\b' -e '\bAKIA[0-9A-Z]{16}\b' -e '-----BEGIN [A-Z ]*PRIVATE KEY-----'
58
+ rg -n --hidden -g '!node_modules' -g '!.git' -e '@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' -e 'Users[/\\][^/\\]+' -e '[/]home/[^/]+' -e '\b(10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+)\b' -e '\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b' -e '\bgithub_pat_[A-Za-z0-9_]{20,}\b' -e '\bAKIA[0-9A-Z]{16}\b' -e '-----BEGIN [A-Z ]*PRIVATE KEY-----'
59
59
  ```
60
60
 
61
61
  Review each hit. Ignore:
@@ -104,6 +104,7 @@ Hooks register via `hooks/hooks.json` into `~/.claude/settings.json`. Once insta
104
104
  ## Open knobs
105
105
 
106
106
  - **NAS / LAN allowlist:** Unlisted private IPs are blocked. The scanner resolves your NAS host from `CLAUDE_NAS_HOST`, then `~/.claude/local-identity.json` (`nas.host`), and allowlists it when it is a private address, so the committed tree holds no real host. `ALL_ALLOWLISTED_PRIVATE_IP_ADDRESSES` in `hooks_constants` holds the static allowlist for any host every machine must share.
107
+ - **Commit-scan exempt repositories:** named owner/repo slugs skip the staged-commit PII scan only. Set `CLAUDE_PII_EXEMPT_REPOS` (comma-separated `owner/repo` values) or list them under `pii_exempt_repositories` in `~/.claude/local-identity.json`. Matching uses the repository's `remote.origin.url` and accepts only the exact host `github.com` (https, ssh scheme, or scp-style). A repository with no readable origin is never exempt (fail-closed to scanning). Write / Edit / MultiEdit and durable post bodies still scan in every repository.
107
108
  - **Public maintainer identity:** when a real email or name is intentional product surface, keep it and note that in the PR body so reviewers do not treat it as a leak.
108
109
 
109
110
  ## What this skill does not do
@@ -61,26 +61,44 @@ Run the pre-check once per run, not per tick or per round. Every later tick read
61
61
 
62
62
  ## Gate 3: Bugbot trigger, acknowledge, and CI-detect
63
63
 
64
- Cursor Bugbot signals through CI check runs, not always through a posted review. The decision tree runs against `<current_head>` with `check_bugbot_ci.py` (installed at `$HOME/.claude/skills/pr-converge/scripts/check_bugbot_ci.py`; `--help` documents every mode):
64
+ Cursor Bugbot signals through CI check runs.
65
+ Mode flags and exit codes for `check_bugbot_ci.py` live in the script docstring
66
+ — run
67
+ `python "$HOME/.claude/skills/pr-converge/scripts/check_bugbot_ci.py" --help`
68
+ for every mode. Caller action sequence against `<current_head>`:
65
69
 
66
70
  1. **Silent-pass pre-check (always first).**
67
71
  `python "$HOME/.claude/skills/pr-converge/scripts/check_bugbot_ci.py" --check-clean --owner <O> --repo <R> --sha <current_head>`
68
- - Exit 0 Bugbot CI completed with a `success`/`neutral` conclusion and posted no review: a silent pass. Record `bugbot_clean_at = <current_head>` and stop; do not trigger.
69
- - Exit 1 (not a silent pass) or exit 2 (gh CLI error, silent pass not confirmable) continue.
72
+ - Silent pass confirmed record `bugbot_clean_at = <current_head>` and stop; do not trigger.
73
+ - Silent pass not confirmed continue.
70
74
  2. **Already-queued check.**
71
75
  `... check_bugbot_ci.py --check-active --owner <O> --repo <R> --sha <current_head>`
72
- - Exit 0 — Bugbot is already queued on this commit. Skip posting; wait for completion (callers schedule their own wakeup).
73
- - Exit 1 continue.
74
- 3. **Trigger.** Post exactly `bugbot run` as an issue comment (`add_issue_comment`) — no `@cursor[bot]` mention, no other text. `bugbot run` is empirically the only re-trigger Cursor Bugbot recognizes; every other phrasing silently no-ops. Wait 8 seconds with an in-turn Monitor delay, never a foreground `sleep` (blocked in headless runs).
76
+ - Already queued skip posting; wait for completion (callers schedule their own wakeup).
77
+ - Not active continue.
78
+ 3. **Trigger.** Post exactly `bugbot run` as an issue comment (`add_issue_comment`) — no `@cursor[bot]` mention, no other text. `bugbot run` is the only re-trigger Cursor Bugbot recognizes; every other phrasing silently no-ops. Wait 8 seconds with an in-turn Monitor delay, never a foreground `sleep` (blocked in headless runs).
75
79
  4. **Acknowledge check.**
76
80
  `... check_bugbot_ci.py --owner <O> --repo <R> --sha <current_head>`
77
- - Exit 0 — a check run is present: record `bugbot_acknowledged_at = <now, ISO 8601>`; the caller polls on its own cadence.
78
- - Exit non-zero Bugbot is down: set `bugbot_down = true` and route past the Bugbot phase; the run continues on its remaining signals.
81
+ - Check run present record `bugbot_acknowledged_at = <now, ISO 8601>`; the caller polls on its own cadence.
82
+ - Check run absent Bugbot is down: set `bugbot_down = true` and route past the Bugbot phase; the run continues on its remaining signals.
79
83
 
80
84
  The silent-pass pre-check runs first so a bot that already finished cleanly is never re-prompted — Bugbot refuses to re-run on an evaluated commit, and without the pre-check the acknowledge step would falsely mark `bugbot_down = true`.
81
85
 
82
86
  **Flag reset rule:** `bugbot_down` resets to false whenever `<current_head>` changes — a new HEAD invalidates the old down-detection. `copilot_down` never resets mid-run; it holds until the run ends.
83
87
 
88
+ ## Convergence-check bypass flags
89
+
90
+ When a reviewer is down, `check_convergence.py` must skip that reviewer's gate so the run still marks ready on its remaining signals. Each condition has one CLI flag and one matching `CLAUDE_REVIEWS_DISABLED` token:
91
+
92
+ | Condition | `check_convergence.py` flag | `CLAUDE_REVIEWS_DISABLED` token |
93
+ |---|---|---|
94
+ | Bugbot down or off | `--bugbot-down` | `bugbot` |
95
+ | Copilot down or opted out | `--copilot-down` | `copilot` |
96
+ | Bugteam CLEAN post blocked | `--bugteam-post-blocked` | `bugteam` |
97
+
98
+ The caller passes the flag on its own `check_convergence.py` run. The flag alone does not reach the independent mark-ready blocker hook, which re-runs `check_convergence.py` with no flags on `gh pr ready`. Each flag also honors its token: `check_convergence.py` reads the same bypass from `CLAUDE_REVIEWS_DISABLED`. So export the matching token in the same shell before `gh pr ready`, and the hook's no-flag re-check inherits the bypass through the env. Combine tokens with commas when more than one reviewer is down (`CLAUDE_REVIEWS_DISABLED="copilot,bugteam"`).
99
+
100
+ The `bugteam` token here means the bugteam CLEAN-review gate is skipped for this convergence check. It does not disable the bugbot or copilot gates: each token is scoped to its own reviewer.
101
+
84
102
  ## Gotchas
85
103
 
86
104
  - **`bugbot run` is load-bearing text.** Alternative phrasings (`@cursor run`, `cursor review`, `bugbot please`) return no error and do nothing.
@@ -31,12 +31,14 @@ Sessions come in many shapes — convergence loops, feature builds, research div
31
31
 
32
32
  ## Backend Detection (run before Step 1)
33
33
 
34
- Determine which storage backend is available. First success wins.
34
+ Determine which storage backend is available for the write path. First success wins.
35
35
 
36
36
  1. **Headless vault** — Bash `ob --version` to verify the obsidian-headless CLI is installed. Check `OBSIDIAN_VAULT_PATH` env var or `~/.claude/vault/` for a vault directory. When the CLI check succeeds AND at least one of those paths resolves to a vault directory, set `backend = "headless"`.
37
37
  2. **Local vault** — fall back to `~/.claude/vault/`. Create `~/.claude/vault/sessions` via `mkdir -p` if missing. Set `backend = "local"`.
38
38
 
39
- **Session-number detection:** Bash `ls` the project's session folder, parse filenames matching `[N]. *.html` or `[N]. *.md` to preserve sequence across the format migration. Highest N + 1. New project start at 1.
39
+ session-log writes with Write/Edit and publishes with Artifact, so it uses headless or local filesystem paths only. `/session-tidy` Phase 0 reuses this headless local order and adds an Obsidian MCP branch for vault maintenance.
40
+
41
+ **Session-number detection:** Bash `ls` the project's session folder, parse filenames matching `[N]. *.html` or `[N]. *.md`. Highest N + 1. New project → start at 1.
40
42
 
41
43
  **Output paths:**
42
44
  - headless: `$OBSIDIAN_VAULT_PATH/sessions/[Project]/[N]. [Title].html` (falls back to `~/.claude/vault/` when the env var is unset)
@@ -139,15 +141,12 @@ Only invoke `/remember` for decisions the user confirms; `/remember` writes the
139
141
  Scope: the current project's session folder only.
140
142
 
141
143
  1. **List files** in the project's vault session folder via Bash `ls`.
142
- 2. **Quick audit** each `.html` file for:
143
- - **Naming convention:** must match `[N]. [Title].html`
144
- - **Frontmatter completeness:** HTML comment block at top of `<body>` contains `type`, `project`, `session`, `date`, `status`, `blocked`, `vault_context_retrieved`, `tags`
145
- - **Status coherence:** `status: completed` with `blocked: true` is contradictory. `status: in-progress` or `status: blocked` on sessions older than 7 days is stale.
146
- 3. **Auto-fix minor issues** via Edit:
147
- - Missing frontmatter fields that can be inferred (e.g., `blocked: false` when status is `completed`; `vault_context_retrieved: false` when the field is absent, since the field defaults to false in pre-existing sessions)
144
+ 2. **Audit** each `.html` and `.md` session file using the audit rules in `session-tidy` Phase 1 (naming, frontmatter completeness, type, status coherence, orphaned next-steps, categorization). session-tidy SKILL.md is the single home for those rules.
145
+ 3. **Auto-fix minor HTML metadata issues** via Edit (this step's write path is HTML-oriented):
146
+ - Missing frontmatter fields that can be inferred (e.g., `blocked: false` when status is `completed`; `vault_context_retrieved: false` when the field is absent)
148
147
  - `type` field set to a wrong value (correct to `session-report`)
149
148
 
150
- When the Edit touches the session report created in this run, call `Artifact` again with the same `file_path` and `favicon` from step 2 to redeploy — the URL captured in step 2 stays unchanged. When the Edit touches an older session report published in a prior run, call `Artifact` on that `file_path` with no `url` argument, since this skill keeps no record of that file's earlier URL — this publishes the older report at a new URL. Surface a `Session [N] republished at a new URL: <url>` line so the user can update any prior shares.
149
+ When the Edit touches the session report created in this run, call `Artifact` again with the same `file_path` and `favicon` from step 2 to redeploy — the URL captured in step 2 stays unchanged. When the Edit touches an older HTML session report published in a prior run, call `Artifact` on that `file_path` with no `url` argument, since this skill keeps no record of that file's earlier URL — this publishes that report at a new URL. Surface a `Session [N] republished at a new URL: <url>` line so the user can update any prior shares.
151
150
  4. **Report issues that need user input:**
152
151
  - Files with wrong naming convention (propose new name)
153
152
  - Stale statuses (propose update to `completed` or ask)
@@ -155,7 +154,7 @@ Scope: the current project's session folder only.
155
154
 
156
155
  If no issues are found, skip silently. Do not report "all clean."
157
156
  5. **Rollup check:** if the project has 5+ sessions and no `Summary.html` or `Summary.md`, mention it:
158
- > "This project has [N] sessions and no summary. A rollup would help; `/session-tidy` is defined for the Markdown session format and may mis-audit or propose destructive renames against HTML sessions, so a manual rollup is the safe path."
157
+ > "This project has [N] sessions and no summary. Run `/session-tidy` for a project rollup."
159
158
 
160
159
  ## Step 6: Finalize
161
160
 
@@ -176,7 +175,7 @@ The primary outcome comes from the session title resolved in step 1.
176
175
  ## Run-and-report checklist
177
176
 
178
177
  - [ ] Backend detected and announced
179
- - [ ] Session number resolved from `[N]. *.html` and `[N]. *.md` files (both parsed to preserve sequence across the format migration)
178
+ - [ ] Session number resolved from `[N]. *.html` and `[N]. *.md` files (highest N + 1)
180
179
  - [ ] `artifact-design` skill loaded before composing page content
181
180
  - [ ] HTML composed for this session's character (no document wrapper tags; self-contained, responsive, theme-aware)
182
181
  - [ ] Frontmatter HTML comment present as the first content line
@@ -185,7 +184,7 @@ The primary outcome comes from the session title resolved in step 1.
185
184
  - [ ] Published via the `Artifact` tool with the fixed favicon `📓`; URL captured (or HTML emitted to chat when step 2 Write failed)
186
185
  - [ ] Vault-context line appended via Edit (step 3); `Artifact` redeployed on the same URL
187
186
  - [ ] Decision extraction surfaced any unrecorded items
188
- - [ ] Session tidy reported anomalies or stayed silent
187
+ - [ ] Session tidy ran session-tidy Phase 1 audit rules on the project folder (or stayed silent when clean)
189
188
  - [ ] `/rename` command copied to clipboard via `pwsh Set-Clipboard`
190
189
 
191
190
  ## Folder map