claude-dev-env 1.57.2 → 1.59.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/CLAUDE.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +317 -54
- package/bin/install.test.mjs +478 -3
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_duplicate_body.py +287 -0
- package/hooks/blocking/code_rules_enforcer.py +175 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/intent_only_ending_blocker.py +155 -0
- package/hooks/blocking/session_handoff_blocker.py +190 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +175 -0
- package/hooks/blocking/test_session_handoff_blocker.py +312 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/hooks.json +25 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/messages.py +4 -0
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +10 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/hooks/workflow/auto_formatter.py +26 -1
- package/hooks/workflow/test_auto_formatter.py +134 -0
- package/package.json +1 -1
- package/rules/conservative-action.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/long-horizon-autonomy.md +43 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/autoconverge/SKILL.md +68 -6
- package/skills/autoconverge/reference/closing-report.md +44 -0
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +105 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +30 -1
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +106 -38
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +259 -0
- package/skills/autoconverge/workflow/render_report.py +903 -0
- package/skills/autoconverge/workflow/test_render_report.py +484 -0
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/update/SKILL.md +37 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Constants for the cross-file duplicate-function-body scan in ``code_rules_enforcer``.
|
|
2
|
+
|
|
3
|
+
The scan flags a top-level function whose body is structurally identical to a
|
|
4
|
+
top-level function already defined in a sibling ``.py`` module in the same
|
|
5
|
+
directory. This catches the Reuse-before-create / DRY violation where a helper
|
|
6
|
+
is copy-pasted across several modules instead of imported from one shared home.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
MINIMUM_DUPLICATE_BODY_STATEMENTS: int = 3
|
|
10
|
+
MAX_DUPLICATE_BODY_ISSUES: int = 25
|
|
11
|
+
DUNDER_INIT_FILENAME: str = "__init__.py"
|
|
12
|
+
PYTHON_SOURCE_SUFFIX: str = ".py"
|
|
13
|
+
DUPLICATE_BODY_GUIDANCE: str = (
|
|
14
|
+
"this function body is identical to one in a sibling module; "
|
|
15
|
+
"extract a single shared helper (for example in hooks_constants/) and "
|
|
16
|
+
"import it from both modules instead of copying it (Reuse before create / DRY)"
|
|
17
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Configuration constants for the hook_prose_detector_consistency PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
WRITE_TOOL_NAME: str = "Write"
|
|
4
|
+
EDIT_TOOL_NAME: str = "Edit"
|
|
5
|
+
|
|
6
|
+
HOOK_MODULE_PATH_SEGMENT: str = "/hooks/"
|
|
7
|
+
PYTHON_FILE_SUFFIX: str = ".py"
|
|
8
|
+
CONSTANTS_MODULE_SUFFIX: str = "_constants.py"
|
|
9
|
+
TEST_MODULE_PREFIX: str = "test_"
|
|
10
|
+
|
|
11
|
+
PATH_SEPARATOR_CLASS_PATTERN: str = (
|
|
12
|
+
r"\[[^\]/]*\\\\[^\]/]*\]|\[[^\]]*\\\\?/[^\]]*\]|\[[^\]]*/\\\\?[^\]]*\]"
|
|
13
|
+
)
|
|
14
|
+
OVERSTATED_OUTPUT_KEY_PHRASE_PATTERN: str = r"output[- ]key\s+segment"
|
|
15
|
+
|
|
16
|
+
CORRECTIVE_MESSAGE: str = (
|
|
17
|
+
"BLOCKED [hook-prose-detector-consistency]: A hook module's user-facing prose "
|
|
18
|
+
"(its docstring lead narrative or CORRECTIVE_MESSAGE) claims the hook blocks an "
|
|
19
|
+
"'output-key segment', but the module's detector keys off a path separator only "
|
|
20
|
+
"(it matches a token next to `\\` or `/`). A quoted structured-output key alone "
|
|
21
|
+
"never triggers a block, so the prose overstates the contract: an author whose "
|
|
22
|
+
"only per-iteration token is an output key would never see this message, and an "
|
|
23
|
+
"author who does see it is told an output key caused a block it cannot cause.\n\n"
|
|
24
|
+
"Describe only the trigger the detector implements: a per-iteration path segment "
|
|
25
|
+
"next to a path separator. Drop 'or output-key segment' (or restate it as 'a "
|
|
26
|
+
"per-iteration path segment') so the message and docstring match what the regex "
|
|
27
|
+
"catches.\n\n"
|
|
28
|
+
"Invariant: a hook's docstring and corrective message describe exactly the shapes "
|
|
29
|
+
"its detector flags -- no broader trigger surface than the regex enforces."
|
|
30
|
+
)
|
|
@@ -5,3 +5,7 @@ USER_FACING_TDD_NOTICE = "TDD gate held - writing the failing test first..."
|
|
|
5
5
|
USER_FACING_ASKUSERQUESTION_NOTICE = (
|
|
6
6
|
"Agent asked the user in prose - rerouting through AskUserQuestion..."
|
|
7
7
|
)
|
|
8
|
+
USER_FACING_INTENT_ENDING_NOTICE = "Agent ended on a promise - doing the work now..."
|
|
9
|
+
USER_FACING_CONTEXT_REASSURANCE_NOTICE = (
|
|
10
|
+
"Agent moved to wrap up early - continuing the work..."
|
|
11
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Shared compiled patterns for the session_handoff_blocker hook."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
FIRST_PERSON_SUBJECT_PATTERN = re.compile(
|
|
6
|
+
r"\b(?:i['’]?m|i['’]?ll|i\s+will|i\s+am|i\s+need\s+to|i\s+should"
|
|
7
|
+
r"|i\s+recommend|i\s+suggest|let\s+me|let['’]?s"
|
|
8
|
+
r"|we\s+(?:should|can|could)|we['’]?ll|we\s+are|we\s+will)\b",
|
|
9
|
+
re.IGNORECASE,
|
|
10
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Configuration constants for the workflow_substitution_slot_blocker PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
WRITE_TOOL_NAME: str = "Write"
|
|
4
|
+
EDIT_TOOL_NAME: str = "Edit"
|
|
5
|
+
MULTI_EDIT_TOOL_NAME: str = "MultiEdit"
|
|
6
|
+
|
|
7
|
+
WORKFLOW_FILE_SUFFIX: str = ".workflow.js"
|
|
8
|
+
|
|
9
|
+
CORRECTIVE_MESSAGE: str = (
|
|
10
|
+
"BLOCKED [workflow-substitution-slot]: A bare per-iteration index token "
|
|
11
|
+
"(for example `cand_i`) appears as a per-iteration path segment inside a "
|
|
12
|
+
".workflow.js agent-prompt block that loops over an index. A bare `_i` "
|
|
13
|
+
"token reads as a fixed literal, so an agent can create one literal "
|
|
14
|
+
"directory and overwrite it across every iteration -- collapsing an "
|
|
15
|
+
"N-iteration gate into one.\n\n"
|
|
16
|
+
"Mark the index as a substitution slot with the angle-bracket convention "
|
|
17
|
+
"this template already uses for per-call values (`<plate.svg>`, `<glow_hex>`): "
|
|
18
|
+
"write `cand_<i>` instead of `cand_i`, or spell out 'replace <i> with the "
|
|
19
|
+
"iteration index 0, 1, 2' in the step text.\n\n"
|
|
20
|
+
"Convention: every per-call substitution slot in a .workflow.js template is "
|
|
21
|
+
"marked with angle brackets, so an agent fills in a fresh value per call."
|
|
22
|
+
)
|
|
@@ -46,7 +46,7 @@ JS_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"}
|
|
|
46
46
|
JSON_EXTENSIONS = {".json"}
|
|
47
47
|
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
48
48
|
HOOKS_DIR = os.path.join(PLUGIN_ROOT, "hooks") + os.sep
|
|
49
|
-
PYTHON_FORMAT_TIMEOUT_SECONDS =
|
|
49
|
+
PYTHON_FORMAT_TIMEOUT_SECONDS = 12
|
|
50
50
|
JS_FORMAT_TIMEOUT_SECONDS = 30
|
|
51
51
|
PRETTIER_CONFIG_NAMES = {
|
|
52
52
|
".prettierrc",
|
|
@@ -76,6 +76,20 @@ def has_prettier_config(file_path: str) -> bool:
|
|
|
76
76
|
return False
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
def budgeted_python_format_seconds() -> int:
|
|
80
|
+
"""Return the wall-clock budget for the two-subprocess happy path.
|
|
81
|
+
|
|
82
|
+
The Python branch breaks out of each loop the moment a command runs, so
|
|
83
|
+
the common case spends one fix subprocess plus one format subprocess. This
|
|
84
|
+
is a budget for that assumed path, not a guaranteed upper bound: when a
|
|
85
|
+
command is missing or times out the loops fall through to the next entry,
|
|
86
|
+
so a degraded run can spend more than this budget.
|
|
87
|
+
"""
|
|
88
|
+
fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
89
|
+
format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
|
|
90
|
+
return fix_phase_seconds + format_phase_seconds
|
|
91
|
+
|
|
92
|
+
|
|
79
93
|
def is_untracked_in_git(file_path: str) -> bool:
|
|
80
94
|
"""Check if file is untracked (brand new) by git."""
|
|
81
95
|
containing_directory = str(Path(file_path).parent)
|
|
@@ -115,6 +129,17 @@ def main() -> None:
|
|
|
115
129
|
suffix = Path(file_path).suffix.lower()
|
|
116
130
|
|
|
117
131
|
if suffix in PYTHON_EXTENSIONS:
|
|
132
|
+
for each_fix_command in [
|
|
133
|
+
["ruff", "check", "--fix", file_path],
|
|
134
|
+
[sys.executable, "-m", "ruff", "check", "--fix", file_path],
|
|
135
|
+
]:
|
|
136
|
+
try:
|
|
137
|
+
subprocess.run(each_fix_command, capture_output=True, text=True, timeout=PYTHON_FORMAT_TIMEOUT_SECONDS, check=False)
|
|
138
|
+
break
|
|
139
|
+
except FileNotFoundError:
|
|
140
|
+
continue
|
|
141
|
+
except subprocess.TimeoutExpired:
|
|
142
|
+
break
|
|
118
143
|
for each_formatter_command in [
|
|
119
144
|
["ruff", "format", file_path],
|
|
120
145
|
[sys.executable, "-m", "ruff", "format", file_path],
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Tests for the auto_formatter hook.
|
|
2
|
+
|
|
3
|
+
Exercises the real hook against real ruff inside a real git repository. A
|
|
4
|
+
brand-new (untracked) Python file carrying an unused import is fixed in
|
|
5
|
+
place, while the same file arriving through the Edit tool is left untouched
|
|
6
|
+
so the fix stays scoped to newly created files.
|
|
7
|
+
|
|
8
|
+
The sandbox is rooted under the user's home directory via ``tempfile.mkdtemp``
|
|
9
|
+
rather than the OS temp directory, matching the sibling workflow-hook tests.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import functools
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
import stat
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Generator
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "auto_formatter.py")
|
|
27
|
+
HOOKS_JSON_PATH = os.path.join(
|
|
28
|
+
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "hooks", "hooks.json"
|
|
29
|
+
)
|
|
30
|
+
AUTO_FORMATTER_COMMAND_FRAGMENT = "workflow/auto_formatter.py"
|
|
31
|
+
UNUSED_IMPORT_SOURCE = "import os\n\n\nVALUE = 1\n"
|
|
32
|
+
HOOK_RUN_TIMEOUT_SECONDS = 60
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _strip_read_only_and_retry(removal_function, target_path, *_exc_info):
|
|
36
|
+
try:
|
|
37
|
+
os.chmod(target_path, stat.S_IWRITE)
|
|
38
|
+
removal_function(target_path)
|
|
39
|
+
except OSError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _force_rmtree(target_path: str) -> None:
|
|
44
|
+
handler_kw = (
|
|
45
|
+
{"onexc": _strip_read_only_and_retry}
|
|
46
|
+
if sys.version_info >= (3, 12)
|
|
47
|
+
else {"onerror": _strip_read_only_and_retry}
|
|
48
|
+
)
|
|
49
|
+
try:
|
|
50
|
+
shutil.rmtree(target_path, **handler_kw)
|
|
51
|
+
except OSError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@functools.lru_cache(maxsize=1)
|
|
56
|
+
def _get_sandbox_parent_directory() -> str:
|
|
57
|
+
return tempfile.mkdtemp(prefix="pytest_auto_formatter_", dir=str(Path.home()))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
61
|
+
def _cleanup_sandbox_parent_directory() -> Generator[None, None, None]:
|
|
62
|
+
yield
|
|
63
|
+
if _get_sandbox_parent_directory.cache_info().currsize:
|
|
64
|
+
_force_rmtree(_get_sandbox_parent_directory())
|
|
65
|
+
_get_sandbox_parent_directory.cache_clear()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.fixture
|
|
69
|
+
def git_repository() -> Generator[Path, None, None]:
|
|
70
|
+
repository_path = Path(tempfile.mkdtemp(dir=_get_sandbox_parent_directory()))
|
|
71
|
+
subprocess.run(["git", "init"], cwd=repository_path, capture_output=True, check=True)
|
|
72
|
+
yield repository_path
|
|
73
|
+
_force_rmtree(str(repository_path))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _run_hook(tool_name: str, file_path: Path) -> subprocess.CompletedProcess[str]:
|
|
77
|
+
hook_input = json.dumps({"tool_name": tool_name, "tool_input": {"file_path": str(file_path)}})
|
|
78
|
+
return subprocess.run(
|
|
79
|
+
[sys.executable, HOOK_SCRIPT_PATH],
|
|
80
|
+
input=hook_input,
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
timeout=HOOK_RUN_TIMEOUT_SECONDS,
|
|
84
|
+
check=False,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestRuffFixOnNewFiles:
|
|
89
|
+
def should_remove_unused_import_from_new_untracked_python_file(
|
|
90
|
+
self, git_repository: Path
|
|
91
|
+
) -> None:
|
|
92
|
+
new_file = git_repository / "brand_new.py"
|
|
93
|
+
new_file.write_text(UNUSED_IMPORT_SOURCE, encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
completed_hook = _run_hook("Write", new_file)
|
|
96
|
+
|
|
97
|
+
assert completed_hook.returncode == 0
|
|
98
|
+
assert "import os" not in new_file.read_text(encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
def should_leave_file_arriving_through_edit_untouched(self, git_repository: Path) -> None:
|
|
101
|
+
edited_file = git_repository / "edited.py"
|
|
102
|
+
edited_file.write_text(UNUSED_IMPORT_SOURCE, encoding="utf-8")
|
|
103
|
+
|
|
104
|
+
completed_hook = _run_hook("Edit", edited_file)
|
|
105
|
+
|
|
106
|
+
assert completed_hook.returncode == 0
|
|
107
|
+
assert "import os" in edited_file.read_text(encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _load_auto_formatter_module() -> object:
|
|
111
|
+
module_spec = importlib.util.spec_from_file_location("auto_formatter", HOOK_SCRIPT_PATH)
|
|
112
|
+
assert module_spec is not None and module_spec.loader is not None
|
|
113
|
+
auto_formatter_module = importlib.util.module_from_spec(module_spec)
|
|
114
|
+
module_spec.loader.exec_module(auto_formatter_module)
|
|
115
|
+
return auto_formatter_module
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _registered_auto_formatter_timeout() -> int:
|
|
119
|
+
with open(HOOKS_JSON_PATH, encoding="utf-8") as hooks_file:
|
|
120
|
+
hooks_configuration = json.load(hooks_file)
|
|
121
|
+
for each_event in hooks_configuration["hooks"].values():
|
|
122
|
+
for each_matcher in each_event:
|
|
123
|
+
for each_hook in each_matcher["hooks"]:
|
|
124
|
+
if AUTO_FORMATTER_COMMAND_FRAGMENT in each_hook["command"]:
|
|
125
|
+
return int(each_hook["timeout"])
|
|
126
|
+
raise AssertionError("auto_formatter hook is not registered in hooks.json")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestPythonFormatTimeoutBudget:
|
|
130
|
+
def should_keep_both_sequential_python_subprocesses_under_the_harness_budget(self) -> None:
|
|
131
|
+
auto_formatter_module = _load_auto_formatter_module()
|
|
132
|
+
budgeted_total = auto_formatter_module.budgeted_python_format_seconds()
|
|
133
|
+
|
|
134
|
+
assert budgeted_total < _registered_auto_formatter_timeout()
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ Proceed with edits, file modifications, or implementations only when the user ex
|
|
|
13
13
|
- If the user asks a question, answer the question. Do not also fix the thing they asked about.
|
|
14
14
|
- If the user describes a problem, investigate and recommend. Do not jump to implementation.
|
|
15
15
|
- If the user says "do it", "go ahead", "make the change", or similarly explicit language, proceed with action.
|
|
16
|
+
- Once the user has explicitly asked and you have what you need, act — do not re-open settled facts or decisions, and do not re-survey options you will not pursue. This rule governs the ambiguous case; the clear case belongs to `long-horizon-autonomy`.
|
|
16
17
|
- When in doubt, ask: "Would you like me to make this change, or just show you the approach?"
|
|
17
18
|
|
|
18
19
|
## Why
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Docstring Prose Matches Implementation
|
|
2
|
+
|
|
3
|
+
**When this applies:** Any Write or Edit to a public function, method, class, or module whose docstring prose makes an enumerable claim about behavior — a list of inputs the code handles, the conditions it treats as a match, the cases it skips, or the order of its steps.
|
|
4
|
+
|
|
5
|
+
## Rule
|
|
6
|
+
|
|
7
|
+
When a docstring enumerates the behaviors a body applies, the enumeration covers every behavior the body applies. A reader trusts the list to be complete: an item the code applies but the prose omits is a silent gap that misleads every future reader and reviewer.
|
|
8
|
+
|
|
9
|
+
The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"` — has no signature to compare against, so the gate cannot catch its drift. This rule is the judgment standard for that prose. It carries documented-but-pending hook coverage; the audit lane below is the enforcement until a deterministic gate check exists.
|
|
10
|
+
|
|
11
|
+
## What to check before you write the docstring
|
|
12
|
+
|
|
13
|
+
Read the body and the docstring side by side:
|
|
14
|
+
|
|
15
|
+
- **Read-source / match-source unions.** A body that computes `read_names = a | b | c` (or any union of "what counts") names each union member in the prose enumeration. A union member the code applies but the prose omits is a gap.
|
|
16
|
+
- **Suppressor / skip lists.** A body with several early returns that suppress the check names each suppressor in the prose.
|
|
17
|
+
- **Step order.** A docstring that says `A then B then C` matches the call order in the body.
|
|
18
|
+
- **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
|
|
19
|
+
|
|
20
|
+
When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
|
|
21
|
+
|
|
22
|
+
## Worked example
|
|
23
|
+
|
|
24
|
+
A `@dataclass` dead-field check builds its set of "field counts as read" sources by union:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
read_names = (
|
|
28
|
+
attribute_read_names
|
|
29
|
+
| dynamic_literal_names
|
|
30
|
+
| _match_pattern_attribute_names(tree)
|
|
31
|
+
| _exported_names(tree)
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A docstring that enumerates "attribute read, augmented-assignment target, class-pattern keyword, literal `getattr`/`attrgetter`" but omits the `__all__` source (`_exported_names`) is drifted: a field whose name appears in `__all__` is treated as read, and the prose hides that. The fix adds the missing source to the enumeration so the list matches the union.
|
|
36
|
+
|
|
37
|
+
## Enforcement (audit lane)
|
|
38
|
+
|
|
39
|
+
This drift class is sub-bucket **O6** in `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` (free-form `Note:` / `Returns:` / responsibility-list claims). The audit teammate lists every prose enumeration in a changed docstring and verifies each item against the body, and lists every union member / suppressor / step in the body and verifies each appears in the prose. A union member or suppressor in the body that the prose omits is an O6 finding.
|
|
40
|
+
|
|
41
|
+
## Why
|
|
42
|
+
|
|
43
|
+
A docstring enumeration earns its place by being trustworthy. A complete list lets a reader reason about the function without scanning the body; a list missing one item is worse than no list, because it asserts completeness it does not have. Naming this standard makes the gap a first-class finding at write time and at audit, rather than a surprise a reader hits months later.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths: **/hooks/**/*.py
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Hook Prose Matches Its Detector
|
|
6
|
+
|
|
7
|
+
**When this applies:** Any Write or Edit to a hook module (`.py` under `hooks/`) or its `*_constants.py` companion.
|
|
8
|
+
|
|
9
|
+
**Hook enforcement:** `hook-prose-detector-consistency` (PreToolUse on Write|Edit) blocks a hook whose user-facing prose claims a trigger its detector never fires on. See `hooks.json` for registration.
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
|
|
13
|
+
A hook's docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags — no broader trigger surface than the regex enforces. An author reads the corrective message to learn what they did wrong; an author reads the docstring to learn what the hook guards. When either claims a trigger the detector cannot fire on, both audiences are misled: an author whose only token is that shape never sees the block, and an author who does see the block is told the wrong cause.
|
|
14
|
+
|
|
15
|
+
## The path-shape blocker case
|
|
16
|
+
|
|
17
|
+
A path-shape blocker detects a per-iteration token only when the token sits next to a path separator (its detection regex keys off a `[\\/]`-style character class). Such a hook must not claim it blocks an "output-key segment": a quoted structured-output key alone, with no looped path, is never flagged. The `*_constants.py` companion holds the corrective message and not the detector, so the phrase "output-key segment" describing a blocked trigger is itself the violation there, regardless of which file holds the regex.
|
|
18
|
+
|
|
19
|
+
| Prohibited claim | Why it overstates | Correct phrasing |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| "appears as a path or output-key segment" | the detector keys off a path separator only | "appears as a per-iteration path segment" |
|
|
22
|
+
| docstring: "blocks a bare token like `cand_i`" | a bare prose token next to no separator is not flagged | "blocks a per-iteration path like `${work}\cand_i\plate.svg`" |
|
|
23
|
+
|
|
24
|
+
## The test
|
|
25
|
+
|
|
26
|
+
After writing a hook, ask: **would a token that matches every word of this message actually trip the detector?** When the message names a shape the regex skips, rewrite the message to name only what the regex catches.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Long-Horizon Autonomy
|
|
2
|
+
|
|
3
|
+
Source: [Anthropic - Prompting Claude Fable 5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5)
|
|
4
|
+
|
|
5
|
+
**When this applies:** Long, multi-step, or unwatched runs — autonomous pipelines, background jobs, convergence loops, and any task that spans many tool calls or a long stretch where the user is away. The behaviors below carry a run to completion rather than letting it stall, drift, or stop early.
|
|
6
|
+
|
|
7
|
+
## Act on what you have
|
|
8
|
+
|
|
9
|
+
When you have enough to act, act. Do not re-derive facts already settled in the conversation, re-open a decision the user already made, or narrate options you will not pursue in user-facing text. When you weigh a choice, give a recommendation, not a full survey. This shapes user-facing messages, not your private reasoning.
|
|
10
|
+
|
|
11
|
+
This is the autonomous-run partner to `conservative-action`: that rule covers the ambiguous case (research and recommend first); this one covers the clear case (once the evidence is in hand, act).
|
|
12
|
+
|
|
13
|
+
## Do not end a turn on a promise
|
|
14
|
+
|
|
15
|
+
Pause for the user only when the work truly needs them: a destructive or irreversible action, a real scope change, or input only they can give. When you hit one, ask through `AskUserQuestion` and end the turn. Do not end on a promise about work you have not done.
|
|
16
|
+
|
|
17
|
+
Before you end any turn, read your last paragraph. If it is a plan, an analysis, a list of next steps, or a statement of intent ("I'll run the tests", "next I'll wire it up"), do that work with tool calls before you stop. End the turn only when the task is done or you are blocked on input only the user can give.
|
|
18
|
+
|
|
19
|
+
In an autonomous pipeline the user cannot answer mid-task. For reversible actions that follow from the original request, act without asking; save any follow-up offers for after the task is done.
|
|
20
|
+
|
|
21
|
+
## Delegate and keep working
|
|
22
|
+
|
|
23
|
+
Hand independent subtasks to subagents and keep working while they run; let them run in the background rather than block until each one returns. Reuse a long-lived subagent across related subtasks so its context carries forward and saves repeated reads. Step in when a subagent drifts off track or is missing context.
|
|
24
|
+
|
|
25
|
+
## Verify your work at intervals
|
|
26
|
+
|
|
27
|
+
On a long build, set a checkpoint cadence and hold to it. At each interval, check the work so far against the task's stated goals with a fresh-context verifier subagent. A separate verifier in a clean context catches what self-review misses.
|
|
28
|
+
|
|
29
|
+
## Ground every progress claim
|
|
30
|
+
|
|
31
|
+
Before you report progress, check each claim against a tool result from this session. State only what the evidence backs; name anything unverified as unverified. If tests fail, say so with the output; if a step was skipped, say that.
|
|
32
|
+
|
|
33
|
+
## Re-ground the final message
|
|
34
|
+
|
|
35
|
+
Terse shorthand between tool calls is fine — that is you thinking. The final message is for a reader who saw none of it. After a long or unwatched run, write it as a fresh briefing: the outcome in one sentence, then the one or two things you need from the reader, each explained as if new. Drop the working vocabulary, arrow chains, and stacked-hyphen compounds; give each file, commit, or flag its own plain clause. When short and clear pull apart, choose clear.
|
|
36
|
+
|
|
37
|
+
## Keep going on context
|
|
38
|
+
|
|
39
|
+
A remaining-context or token count is not a reason to stop. Do not pause, summarize, or float a fresh session on account of context limits; keep working. When the user must see content word-for-word (a partial deliverable, a direct answer to a mid-run question), surface it through the channel the harness gives for that, not by ending the turn.
|
|
40
|
+
|
|
41
|
+
## Why
|
|
42
|
+
|
|
43
|
+
A capable model under-delivers on long runs for predictable reasons: it overplans when it could act, stops on a promise, blocks on subagents, skips its own verification, fabricates progress, buries the result in working shorthand, or quits early over a context count. Each section above removes one of those failure modes so the run finishes.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# No Inline Destructive-Command Literals in Bash
|
|
2
|
+
|
|
3
|
+
The `destructive_command_blocker` PreToolUse hook matches destructive patterns (`rm -rf`, `git reset --hard`, `dd`, `mkfs`, `chmod -R`, fork bombs) as raw text anywhere in a Bash-tool command, with no quote-awareness — so a destructive literal carried only as DATA (a commit message, a PR/issue/review-comment body, an echoed string, a `python -c`/`node -e`/`awk` argument, a heredoc) trips the confirmation prompt even though the shell never executes it. In a background or auto-mode run no human can answer that prompt, so the call stalls.
|
|
4
|
+
|
|
5
|
+
Keep destructive literals out of the Bash command string:
|
|
6
|
+
|
|
7
|
+
- Commit messages and PR/issue/review-comment bodies that describe destructive-command behavior go in a file passed by path — `git commit -F <file>`, `gh ... --body-file <file>` (see [`gh-body-file`](gh-body-file.md)) — never `git commit -m` / `gh ... -b`.
|
|
8
|
+
- To exercise or verify `destructive_command_blocker` (or any hook) behavior, run the committed test suite (`python -m pytest <test_file>`), which passes the command strings as in-language data, not as a shell command — never an inline `python -c` harness.
|
|
9
|
+
- Genuine cleanup targets the OS temp dir or `$CLAUDE_JOB_DIR/tmp` (auto-allowed as ephemeral), never a repository or worktree path.
|
|
10
|
+
|
|
11
|
+
The `destructive_command_blocker` hook is the enforcement surface; this rule is how to keep a non-executing mention from tripping it.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Workflow Substitution Slot Rule
|
|
2
|
+
|
|
3
|
+
In a `.workflow.js` agent-prompt template, every per-call or per-iteration value an agent must fill in is marked with the angle-bracket convention — `<plate.svg>`, `<object.svg>`, `<glow_hex>`, `cand_<i>`. A bare token such as `cand_i` reads as a fixed literal, so an agent can create one literal directory named `cand_i` and overwrite it across every iteration of a loop, collapsing an N-iteration gate into a single run.
|
|
4
|
+
|
|
5
|
+
When a loop builds a per-iteration path or output key, write the index as a slot — `cand_<i>` — or spell out `replace <i> with the iteration index 0, 1, 2` in the step text. Every per-call value in a `.workflow.js` template carries angle brackets so an agent fills in a fresh value per call.
|
|
6
|
+
|
|
7
|
+
`workflow_substitution_slot_blocker.py` (PreToolUse on Write/Edit) blocks a `.workflow.js` write whose looped content carries a bare `<word>_<i|j|k>` token as a per-iteration path segment, and returns the corrective message.
|
|
@@ -66,7 +66,7 @@ own. The workflow runs in the background and notifies this session on
|
|
|
66
66
|
completion. Watch live progress with `/workflows`.
|
|
67
67
|
|
|
68
68
|
The workflow returns
|
|
69
|
-
`{ converged, rounds, finalSha, blocker, standardsNote }`.
|
|
69
|
+
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote }`.
|
|
70
70
|
|
|
71
71
|
## Budget-aware round boundaries
|
|
72
72
|
|
|
@@ -81,15 +81,64 @@ round records nothing resumable and replays dirty.
|
|
|
81
81
|
|
|
82
82
|
## Teardown (on workflow completion)
|
|
83
83
|
|
|
84
|
-
1. **When `converged` is true
|
|
84
|
+
1. **When `converged` is true — build and publish the closing report.**
|
|
85
|
+
Skip this entire step (report, gist, comment, Chrome open) when the workflow
|
|
86
|
+
returned a non-null `blocker`. Per-round live-dashboard refresh is out of scope
|
|
87
|
+
here; this step builds the one-shot closing report and the seam (marker comment +
|
|
88
|
+
gist URL) a future live-dashboard reuses.
|
|
89
|
+
|
|
90
|
+
a. **Resolve the journal path.** Glob
|
|
91
|
+
`~/.claude/projects/**/workflows/wf_<runId>.json` (where `runId` is the run id
|
|
92
|
+
the `Workflow` result returned) and take the match.
|
|
93
|
+
|
|
94
|
+
b. **Build the report.**
|
|
95
|
+
```
|
|
96
|
+
python "<skill>/workflow/render_report.py" \
|
|
97
|
+
--journal "<journal>" \
|
|
98
|
+
--out "$CLAUDE_JOB_DIR/tmp/autoconverge-report-<prNumber>.html" \
|
|
99
|
+
--pr <owner>/<repo>#<n> \
|
|
100
|
+
--final-sha <finalSha> \
|
|
101
|
+
--rounds <rounds> \
|
|
102
|
+
--repo <worktree>
|
|
103
|
+
```
|
|
104
|
+
Capture the output path from stdout.
|
|
105
|
+
|
|
106
|
+
c. **Publish as a secret gist** by reusing `doc-gist` (do not reimplement gist
|
|
107
|
+
creation):
|
|
108
|
+
```
|
|
109
|
+
python "$HOME/.claude/skills/doc-gist/scripts/gist_upload.py" \
|
|
110
|
+
--input "<html path>" \
|
|
111
|
+
--no-open \
|
|
112
|
+
--description "autoconverge report PR #<n>"
|
|
113
|
+
```
|
|
114
|
+
Capture the htmlpreview URL from stdout. The gist is secret by default; pass
|
|
115
|
+
no public flag.
|
|
116
|
+
|
|
117
|
+
d. **Post one idempotent PR comment.** List the PR's issue comments; if one
|
|
118
|
+
carries the marker `<!-- autoconverge-report -->`, edit it in place, otherwise
|
|
119
|
+
create a new one. The body begins with `<!-- autoconverge-report -->`, then
|
|
120
|
+
the htmlpreview link, headline counts (findings by severity, rounds, tests
|
|
121
|
+
added), and the full finding list as `file:line — P# — title` grouped by
|
|
122
|
+
severity. Honor the gh-body-file rule: write a BOM-free temp file and pass
|
|
123
|
+
`--body-file` to `gh issue comment`/`gh issue comment edit`, or use the
|
|
124
|
+
GitHub MCP `add_issue_comment` tool (body as a structured parameter, no
|
|
125
|
+
`--body` flag).
|
|
126
|
+
|
|
127
|
+
e. **Open the report in Chrome.**
|
|
128
|
+
```
|
|
129
|
+
Start-Process chrome -ArgumentList '--new-window', '<report path>'
|
|
130
|
+
```
|
|
131
|
+
Tolerate a missing Chrome without aborting the rest of teardown.
|
|
132
|
+
|
|
133
|
+
2. **When `converged` is true:** rewrite the PR description and clean the
|
|
85
134
|
working tree — see
|
|
86
135
|
[`bugteam/reference/teardown-publish-permissions.md` § Step 4 and § Step 4.5](../bugteam/reference/teardown-publish-permissions.md).
|
|
87
136
|
The workflow already marked the PR ready.
|
|
88
137
|
|
|
89
|
-
|
|
138
|
+
3. **Always revoke project permissions** (including on a blocker exit):
|
|
90
139
|
`python "$HOME/.claude/skills/bugteam/scripts/revoke_project_claude_permissions.py"`
|
|
91
140
|
|
|
92
|
-
|
|
141
|
+
4. **Print the final report:**
|
|
93
142
|
|
|
94
143
|
```
|
|
95
144
|
/autoconverge exit: <converged | blocked>
|
|
@@ -97,6 +146,7 @@ round records nothing resumable and replays dirty.
|
|
|
97
146
|
Final commit: <finalSha>
|
|
98
147
|
Blocker: <blocker> # only when blocked
|
|
99
148
|
Standards: <standardsNote> # only when a round deferred code-standard findings
|
|
149
|
+
Copilot: <copilotNote> # only when Copilot was down or out of quota
|
|
100
150
|
```
|
|
101
151
|
|
|
102
152
|
## What the workflow does each round
|
|
@@ -107,6 +157,13 @@ shape and the exact convergence definition, and
|
|
|
107
157
|
run ends short of ready. Hard-won failure lessons live in
|
|
108
158
|
[`reference/gotchas.md`](reference/gotchas.md).
|
|
109
159
|
|
|
160
|
+
Every agent prompt carries a headless-safety preamble: the run is unattended, so
|
|
161
|
+
agents never inline a destructive-command literal (`rm -rf`, `git reset --hard`,
|
|
162
|
+
`dd`) into a Bash command — the `destructive_command_blocker` hook matches those
|
|
163
|
+
patterns as raw text, and a confirmation prompt no human can answer would stall
|
|
164
|
+
the run. Agents verify destructive-blocker behavior through the committed test
|
|
165
|
+
suite (`python -m pytest`) and keep scratch work in ephemeral temp dirs.
|
|
166
|
+
|
|
110
167
|
- **Converge:** `parallel([Bugbot lens, code-review lens, bug-audit lens])` on
|
|
111
168
|
the current HEAD, full `origin/main...HEAD` diff. Dedup findings; one
|
|
112
169
|
`clean-coder` applies all fixes in a single commit, pushes, replies to and
|
|
@@ -119,7 +176,10 @@ run ends short of ready. Hard-won failure lessons live in
|
|
|
119
176
|
any bot threads with a deferral note, and reports the deferral in
|
|
120
177
|
`standardsNote`.
|
|
121
178
|
- **Copilot gate:** request a Copilot review, poll up to three times; findings
|
|
122
|
-
route back into Converge
|
|
179
|
+
route back into Converge. When Copilot is down or out of quota — it posts an
|
|
180
|
+
out-of-usage notice (the requester hit their quota) on the HEAD, or surfaces no
|
|
181
|
+
review at all after the cap — the gate logs a notice and the run marks the PR
|
|
182
|
+
ready with the Copilot gate bypassed. `copilotNote` records the bypass.
|
|
123
183
|
- **Convergence check:** `check_convergence.py` is the authoritative gate; on a
|
|
124
184
|
full pass the workflow marks `draft=false`.
|
|
125
185
|
|
|
@@ -127,4 +187,6 @@ run ends short of ready. Hard-won failure lessons live in
|
|
|
127
187
|
|
|
128
188
|
- `SKILL.md` — this hub.
|
|
129
189
|
- `workflow/converge.mjs` — the convergence workflow script.
|
|
130
|
-
- `
|
|
190
|
+
- `workflow/render_report.py` — builds the closing convergence insights HTML report.
|
|
191
|
+
- `workflow/autoconverge_report_constants/` — named constants for the report builder.
|
|
192
|
+
- `reference/` — convergence definition, stop conditions, gotchas, closing report.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Closing Report
|
|
2
|
+
|
|
3
|
+
When an autoconverge run converges (the workflow returns `converged: true`), the main session generates a convergence insights HTML report and publishes it as a secret gist with an idempotent PR comment linking to it.
|
|
4
|
+
|
|
5
|
+
## Data source
|
|
6
|
+
|
|
7
|
+
The report reads two file types written by the workflow during the run:
|
|
8
|
+
|
|
9
|
+
- **Run journal** — `~/.claude/projects/**/workflows/wf_<runId>.json` — the PR args, round log lines, `workflowProgress` array (one entry per agent step), and the final result.
|
|
10
|
+
- **Agent transcripts** — `~/.claude/projects/**/subagents/workflows/<runId>/agent-<agentId>.jsonl` — one file per agent; each line is a JSON object; the renderer extracts the last `StructuredOutput` tool_use from each file.
|
|
11
|
+
|
|
12
|
+
`converge.mjs` is not modified. The report is a pure reader of files the workflow already writes.
|
|
13
|
+
|
|
14
|
+
## Building the report
|
|
15
|
+
|
|
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
|
+
```
|
|
25
|
+
|
|
26
|
+
The script reads the journal and transcripts, computes aggregated metrics (findings by severity, round, and theme; fix commits; tests added per round), and writes a self-contained HTML report. It prints the output path to stdout and exits 0 on success.
|
|
27
|
+
|
|
28
|
+
All aggregation is deterministic: `generated_date` comes from the journal `timestamp`, not the system clock, so the same inputs always produce the same HTML.
|
|
29
|
+
|
|
30
|
+
## Publishing
|
|
31
|
+
|
|
32
|
+
After rendering, the main session:
|
|
33
|
+
|
|
34
|
+
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 comment body has the gist URL and a summary of findings by severity, rounds, and tests added, followed by the full finding list grouped by severity (`file:line — P# — title`). 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
|
+
3. **Opens the report** with `Start-Process chrome -ArgumentList '--new-window', '<report path>'`. A missing Chrome does not abort teardown.
|
|
37
|
+
|
|
38
|
+
## Live-dashboard seam
|
|
39
|
+
|
|
40
|
+
The marker comment and gist together form a seam for future per-round dashboard refreshes: a live-dashboard step re-renders with the same `render_report_html` function (pure, no side effects), runs `gh gist edit` on the same gist, and edits the same marker comment. That per-round refresh path is out of scope here; this document describes the one-shot closing report only.
|
|
41
|
+
|
|
42
|
+
## Scope
|
|
43
|
+
|
|
44
|
+
The closing report runs only when `converged === true`. On a blocker exit (`blocker: "budget"` or similar), the report, gist, comment, and Chrome open are all skipped.
|
|
@@ -42,7 +42,9 @@ tracks CONVERGE passes only and is never the cap.
|
|
|
42
42
|
to three times, 360 seconds apart.
|
|
43
43
|
- Copilot findings → fix them and return to CONVERGE on the new HEAD.
|
|
44
44
|
- Copilot clean or approved → move to the convergence check.
|
|
45
|
-
-
|
|
45
|
+
- Copilot down or out of quota (an out-of-usage notice, or no review after three
|
|
46
|
+
polls) → log a notice and move to the convergence check with the Copilot gate
|
|
47
|
+
bypassed.
|
|
46
48
|
|
|
47
49
|
**Convergence check**:
|
|
48
50
|
|
|
@@ -67,13 +69,15 @@ the current HEAD:
|
|
|
67
69
|
2. The Bugbot review body on HEAD reports no findings (checked when a Bugbot
|
|
68
70
|
review is present).
|
|
69
71
|
3. A CLEAN bugteam audit review sits on HEAD.
|
|
70
|
-
4. The Copilot review on HEAD is clean or approved
|
|
72
|
+
4. The Copilot review on HEAD is clean or approved (bypassed when Copilot is down
|
|
73
|
+
or out of quota this run).
|
|
71
74
|
5. Zero unresolved bot review threads anywhere on the PR — counting Cursor,
|
|
72
75
|
Claude, and Copilot authored threads where `isResolved` is false (`isOutdated`
|
|
73
76
|
threads are excluded by the gate, but the fix lens still verifies and resolves
|
|
74
77
|
them during the round).
|
|
75
78
|
6. The PR is mergeable (`mergeable` true and `mergeable_state` clean).
|
|
76
|
-
7. No requested reviewers are still pending
|
|
79
|
+
7. No requested reviewers are still pending (bypassed when Copilot is down or out
|
|
80
|
+
of quota this run).
|
|
77
81
|
|
|
78
82
|
## Audit-trail design
|
|
79
83
|
|