claude-dev-env 8.72.0 → 8.72.2
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/bin/install.mjs +1 -0
- package/bin/install.test.mjs +1 -0
- package/docs/rule-guides/asd-ste100-language.md +10 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +9 -1
- package/hooks/blocking/headless_claude_broker_gate.py +14 -1
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +2 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +46 -0
- package/hooks/blocking/test_headless_claude_broker_gate.py +12 -0
- package/hooks/conftest.py +18 -0
- package/hooks/features/session-context.md +1 -3
- package/hooks/hooks.json +0 -5
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/headless_claude_broker_gate_constants.py +7 -1
- package/hooks/hooks_constants/mod_handoff.py +91 -0
- package/hooks/hooks_constants/mod_handoff_constants.py +38 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -1
- package/hooks/hooks_constants/test_mod_handoff.py +100 -0
- package/hooks/session/advisor_rules_prompt.py +5 -1
- package/hooks/session/orchestrator_auto_starter.py +5 -1
- package/hooks/session/task_tool_prompt.py +5 -1
- package/hooks/session/test_advisor_rules_prompt.py +11 -0
- package/hooks/session/test_orchestrator_auto_starter.py +22 -0
- package/hooks/session/test_task_tool_prompt.py +15 -0
- package/hooks/session/test_working_style_prompt.py +15 -0
- package/hooks/session/working_style_prompt.py +5 -1
- package/package.json +1 -1
- package/rules/asd-ste100-language.md +1 -1
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +24 -0
- package/scripts/review_closure_model.py +48 -12
- package/scripts/test_files/review_closure/codex_comments.json +1266 -0
- package/scripts/tests/test_review_closure_model.py +130 -0
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +0 -22
- package/hooks/session/issue_tracker_session_starter.py +0 -136
- package/hooks/session/test_issue_tracker_session_starter.py +0 -106
|
@@ -35,6 +35,7 @@ def _run_main() -> str:
|
|
|
35
35
|
@pytest.fixture
|
|
36
36
|
def config_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
37
37
|
monkeypatch.setenv(CLAUDE_CONFIG_DIR_ENV_VAR, str(tmp_path))
|
|
38
|
+
monkeypatch.delenv("CLAUDE_CODE_REMOTE", raising=False)
|
|
38
39
|
monkeypatch.delenv(ADVISOR_DISABLE_ENV_VAR, raising=False)
|
|
39
40
|
return tmp_path
|
|
40
41
|
|
|
@@ -56,6 +57,16 @@ class TestAdvisorRulesPrompt:
|
|
|
56
57
|
_write_settings(config_directory, {"permissions": {}})
|
|
57
58
|
assert _run_main() == ""
|
|
58
59
|
|
|
60
|
+
def test_should_emit_nothing_while_the_session_prompts_mod_is_on(
|
|
61
|
+
self, config_directory: Path, monkeypatch: pytest.MonkeyPatch
|
|
62
|
+
) -> None:
|
|
63
|
+
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(config_directory))
|
|
64
|
+
_write_settings(
|
|
65
|
+
config_directory,
|
|
66
|
+
{"advisorModel": "fable", "enabledPlugins": {"session-prompts@mods-marketplace": True}},
|
|
67
|
+
)
|
|
68
|
+
assert _run_main() == ""
|
|
69
|
+
|
|
59
70
|
def test_should_emit_nothing_when_advisor_model_is_blank(self, config_directory: Path) -> None:
|
|
60
71
|
_write_settings(config_directory, {"advisorModel": " "})
|
|
61
72
|
assert _run_main() == ""
|
|
@@ -99,3 +99,25 @@ def test_main_stdin_disabled_prints_nothing() -> None:
|
|
|
99
99
|
)
|
|
100
100
|
assert completed.returncode == 0
|
|
101
101
|
assert completed.stdout.strip() == ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_main_prints_nothing_while_the_session_prompts_mod_is_on(tmp_path: Path) -> None:
|
|
105
|
+
(tmp_path / "settings.json").write_text(
|
|
106
|
+
json.dumps({"enabledPlugins": {"session-prompts@mods-marketplace": True}}), encoding="utf-8"
|
|
107
|
+
)
|
|
108
|
+
completed = subprocess.run(
|
|
109
|
+
[sys.executable, str(STARTER_SCRIPT)],
|
|
110
|
+
input=json.dumps({"source": "startup"}),
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
check=False,
|
|
114
|
+
env={
|
|
115
|
+
**os.environ,
|
|
116
|
+
ORCHESTRATOR_AUTO_STARTER_ENABLED_ENV_VAR: "1",
|
|
117
|
+
"CLAUDE_CONFIG_DIR": str(tmp_path),
|
|
118
|
+
"CLAUDE_CODE_REMOTE": "",
|
|
119
|
+
"CLAUDE_PROJECT_DIR": str(tmp_path),
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
assert completed.returncode == 0
|
|
123
|
+
assert completed.stdout.strip() == ""
|
|
@@ -7,6 +7,8 @@ from io import StringIO
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from unittest.mock import patch
|
|
9
9
|
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
10
12
|
_SESSION_DIR = Path(__file__).resolve().parent
|
|
11
13
|
_HOOKS_ROOT = _SESSION_DIR.parent
|
|
12
14
|
for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
|
|
@@ -63,3 +65,16 @@ class TestSessionDirective:
|
|
|
63
65
|
|
|
64
66
|
def test_build_session_directive_returns_the_shared_constant(self) -> None:
|
|
65
67
|
assert prompt.build_session_directive() == TASK_TOOL_DIRECTIVE
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestModHandoff:
|
|
71
|
+
def test_should_stay_silent_while_the_session_prompts_mod_is_on(
|
|
72
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
73
|
+
) -> None:
|
|
74
|
+
(tmp_path / "settings.json").write_text(
|
|
75
|
+
json.dumps({"enabledPlugins": {"session-prompts@mods-marketplace": True}}), encoding="utf-8"
|
|
76
|
+
)
|
|
77
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(tmp_path))
|
|
78
|
+
monkeypatch.delenv("CLAUDE_CODE_REMOTE", raising=False)
|
|
79
|
+
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path))
|
|
80
|
+
assert _run_main() == ""
|
|
@@ -6,6 +6,8 @@ from io import StringIO
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from unittest.mock import patch
|
|
8
8
|
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
9
11
|
_SESSION_DIR = Path(__file__).resolve().parent
|
|
10
12
|
_HOOKS_ROOT = _SESSION_DIR.parent
|
|
11
13
|
for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
|
|
@@ -60,3 +62,16 @@ class TestWorkingStylePrompt:
|
|
|
60
62
|
|
|
61
63
|
def test_build_session_directive_returns_the_shared_constant(self) -> None:
|
|
62
64
|
assert starter.build_session_directive() == WORKING_STYLE_PROMPT
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestModHandoff:
|
|
68
|
+
def test_should_stay_silent_while_the_session_prompts_mod_is_on(
|
|
69
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
70
|
+
) -> None:
|
|
71
|
+
(tmp_path / "settings.json").write_text(
|
|
72
|
+
json.dumps({"enabledPlugins": {"session-prompts@mods-marketplace": True}}), encoding="utf-8"
|
|
73
|
+
)
|
|
74
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(tmp_path))
|
|
75
|
+
monkeypatch.delenv("CLAUDE_CODE_REMOTE", raising=False)
|
|
76
|
+
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path))
|
|
77
|
+
assert _run_main() == ""
|
|
@@ -17,6 +17,8 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
17
17
|
if _hooks_dir not in sys.path:
|
|
18
18
|
sys.path.insert(0, _hooks_dir)
|
|
19
19
|
|
|
20
|
+
from hooks_constants.mod_handoff import is_mod_plugin_enabled
|
|
21
|
+
from hooks_constants.mod_handoff_constants import SESSION_PROMPTS_PLUGIN_NAME
|
|
20
22
|
from hooks_constants.working_style_prompt_constants import ( # noqa: E402
|
|
21
23
|
WORKING_STYLE_PROMPT,
|
|
22
24
|
)
|
|
@@ -28,7 +30,9 @@ def build_session_directive() -> str:
|
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def main() -> None:
|
|
31
|
-
"""Emit the working-style prompt as SessionStart additionalContext."""
|
|
33
|
+
"""Emit the working-style prompt as SessionStart additionalContext, unless the session-prompts mod is on."""
|
|
34
|
+
if is_mod_plugin_enabled(SESSION_PROMPTS_PLUGIN_NAME):
|
|
35
|
+
return
|
|
32
36
|
print(
|
|
33
37
|
json.dumps(
|
|
34
38
|
{
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**When:** Writing chat, tool narration, or repository prose.
|
|
4
4
|
|
|
5
|
-
Use the sole general language rule. Write short, complete sentences on one topic. Use active voice; lead with conditions;
|
|
5
|
+
Use the sole general language rule. Write short, complete sentences on one topic. Use active voice; lead with conditions; one action per step. Use plain, precise words, stable terms. Expand abbreviations and contractions; name unclear pronouns. Be inclusive; punctuate clearly. Preserve exact labels, identifiers, formulas, titles, interface text. Say each fact once. Send a result, blocker, or question. Mark injury or death `WARNING`, equipment damage `CAUTION`; state condition then result. Aim for 20 words per step, 25 per description. Treat checks as aids; have a human verify accuracy, terms, safety, confidentiality, meaning.
|
|
6
6
|
|
|
7
7
|
**Enforcement:** none.
|
|
8
8
|
|
|
@@ -155,6 +155,30 @@ summary, a Graphite verdict mirror whose findings arrive as review threads, a
|
|
|
155
155
|
Qodo description of the change, a Qodo placeholder posted while it reviews, and
|
|
156
156
|
a Qodo review that found nothing."""
|
|
157
157
|
|
|
158
|
+
CODEX_REVIEW_BOT_LOGIN = "chatgpt-codex-connector[bot]"
|
|
159
|
+
ALL_REVIEW_REQUEST_COMMANDS = ("@codex review", "@codex security review")
|
|
160
|
+
ALL_CODEX_NOTICE_DETAILS = (
|
|
161
|
+
"<details> <summary>\u2139\ufe0f About Codex in GitHub</summary>\n<br/>\n\n[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you\n- Open a pull request for review\n- Mark a draft as ready\n- Comment \"@codex review\" or \"@codex security review\".\n\nCodex reacts with \U0001f440 while any review is running, comments if it has suggestions, and reacts with \U0001f44d once all reviews finish with no findings.\n\n</details>",
|
|
162
|
+
"<details> <summary>\u2139\ufe0f About Codex in GitHub</summary>\n<br/>\n\n[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you\n- Open a pull request for review\n- Mark a draft as ready\n- Comment \"@codex review\".\n\nIf Codex has suggestions, it will comment; otherwise it will react with \U0001f44d.\n\n\n\n\nCodex can also answer questions or update the PR. Try commenting \"@codex address that feedback\".\n \n</details>",
|
|
163
|
+
)
|
|
164
|
+
CODEX_SUMMARY_PREFIX = (
|
|
165
|
+
"<!-- codex-pull-request-review-summary -->\n\n"
|
|
166
|
+
"## Codex Review Summary\n\n"
|
|
167
|
+
"This comment shows the latest Codex review activity on this pull request.\n\n"
|
|
168
|
+
"| Review | Status | Commit | Review trigger |\n"
|
|
169
|
+
"| --- | --- | --- | --- |\n"
|
|
170
|
+
)
|
|
171
|
+
CODEX_SUMMARY_ROW_PATTERN = (
|
|
172
|
+
r"\| (?:\U0001f4dd \*\*Code Review\*\*|\U0001f6e1\ufe0f \*\*Security Review\*\*) "
|
|
173
|
+
r"\| \u2705 \*\*Completed\*\* <relative-time datetime=\"[^\"\n]+\">"
|
|
174
|
+
r"[^<\n]+</relative-time> \| `[a-f0-9]{7,40}` \| Manual request \|"
|
|
175
|
+
)
|
|
176
|
+
CODEX_CLEAN_REVIEW_PATTERN = (
|
|
177
|
+
r"Codex Review: Didn't find any major issues\."
|
|
178
|
+
r"(?: Nice work!| Chef's kiss\.| You're on a roll\.| Swish!)?"
|
|
179
|
+
r"\n\n\*\*Reviewed commit:\*\* `[a-f0-9]{7,40}`"
|
|
180
|
+
)
|
|
181
|
+
|
|
158
182
|
SHORT_SHA_LENGTH: int = 7
|
|
159
183
|
"""How much of a commit identifier the verdict line prints."""
|
|
160
184
|
|
|
@@ -29,6 +29,7 @@ nobody.
|
|
|
29
29
|
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
|
+
import re
|
|
32
33
|
from collections.abc import Iterable, Mapping, Sequence
|
|
33
34
|
from dataclasses import dataclass
|
|
34
35
|
from datetime import datetime
|
|
@@ -38,6 +39,8 @@ from dev_env_scripts_constants.review_closure_constants import (
|
|
|
38
39
|
ALL_COMMENT_AUTHOR_KEYS,
|
|
39
40
|
ALL_COMMENT_LIST_KEYS,
|
|
40
41
|
ALL_NOTICE_COMMENT_MARKERS,
|
|
42
|
+
ALL_CODEX_NOTICE_DETAILS,
|
|
43
|
+
ALL_REVIEW_REQUEST_COMMANDS,
|
|
41
44
|
ALL_OUTDATED_KEYS,
|
|
42
45
|
ALL_RESOLVED_KEYS,
|
|
43
46
|
APPROVALS_CHECK_NAME,
|
|
@@ -48,6 +51,10 @@ from dev_env_scripts_constants.review_closure_constants import (
|
|
|
48
51
|
CHECK_RUN_NAME_KEY,
|
|
49
52
|
CLOSED_DETAIL,
|
|
50
53
|
CLOSED_VERDICT_LABEL,
|
|
54
|
+
CODEX_CLEAN_REVIEW_PATTERN,
|
|
55
|
+
CODEX_REVIEW_BOT_LOGIN,
|
|
56
|
+
CODEX_SUMMARY_PREFIX,
|
|
57
|
+
CODEX_SUMMARY_ROW_PATTERN,
|
|
51
58
|
COMMENT_BODY_KEY,
|
|
52
59
|
COMMENT_IDENTIFIER_KEY,
|
|
53
60
|
COMMENT_IDS_KEY,
|
|
@@ -187,21 +194,21 @@ def latest_driver_comment_time(
|
|
|
187
194
|
all_comments: Iterable[TopLevelComment],
|
|
188
195
|
all_driver_logins: frozenset[str],
|
|
189
196
|
) -> datetime | None:
|
|
190
|
-
"""Find when the driving agent last posted a top-level comment.
|
|
197
|
+
"""Find when the driving agent last posted a substantive top-level comment.
|
|
191
198
|
|
|
192
199
|
Args:
|
|
193
200
|
all_comments: The top-level comments on the pull request.
|
|
194
201
|
all_driver_logins: The logins that count as the driving agent.
|
|
195
202
|
|
|
196
203
|
Returns:
|
|
197
|
-
The creation time of the driving agent's latest
|
|
204
|
+
The creation time of the driving agent's latest substantive comment, or
|
|
198
205
|
None when it posted none.
|
|
199
206
|
"""
|
|
200
207
|
return max(
|
|
201
208
|
(
|
|
202
209
|
each_comment.created_at
|
|
203
210
|
for each_comment in all_comments
|
|
204
|
-
if each_comment.author_login in all_driver_logins
|
|
211
|
+
if each_comment.author_login in all_driver_logins and not each_comment.is_notice
|
|
205
212
|
),
|
|
206
213
|
default=None,
|
|
207
214
|
)
|
|
@@ -241,10 +248,10 @@ def top_level_findings(
|
|
|
241
248
|
"""Decide which top-level comments still wait on the driving agent.
|
|
242
249
|
|
|
243
250
|
The driving agent answers a top-level comment by posting a top-level
|
|
244
|
-
comment of its own. Its latest one answers every comment last touched
|
|
251
|
+
substantive comment of its own. Its latest one answers every comment last touched
|
|
245
252
|
before it. A person's comment edited after it waits again. A bot's edit
|
|
246
253
|
leaves its comment answered, since a bot posts each new finding as a
|
|
247
|
-
review thread or a new comment.
|
|
254
|
+
review thread or a new comment. Requests and notices answer no findings.
|
|
248
255
|
|
|
249
256
|
Args:
|
|
250
257
|
all_comments: The top-level comments on the pull request.
|
|
@@ -424,14 +431,47 @@ def comment_records_by_id(
|
|
|
424
431
|
}
|
|
425
432
|
|
|
426
433
|
|
|
434
|
+
def _is_codex_notice(body: str) -> bool:
|
|
435
|
+
"""Match the verified summary or clean format, including its metadata."""
|
|
436
|
+
normalized = body.strip()
|
|
437
|
+
for each_details in ALL_CODEX_NOTICE_DETAILS:
|
|
438
|
+
if not normalized.endswith(each_details):
|
|
439
|
+
continue
|
|
440
|
+
content = normalized[:-len(each_details)].rstrip()
|
|
441
|
+
if content.startswith(CODEX_SUMMARY_PREFIX):
|
|
442
|
+
all_rows = content[len(CODEX_SUMMARY_PREFIX):].splitlines()
|
|
443
|
+
return bool(all_rows) and all(
|
|
444
|
+
re.fullmatch(CODEX_SUMMARY_ROW_PATTERN, each_row) for each_row in all_rows
|
|
445
|
+
)
|
|
446
|
+
return re.fullmatch(CODEX_CLEAN_REVIEW_PATTERN, content) is not None
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _is_notice_comment(
|
|
451
|
+
all_comment_fields: Mapping[str, object], body: str, all_extra_markers: Sequence[str]
|
|
452
|
+
) -> bool:
|
|
453
|
+
"""Separate exact review requests and trusted notices from findings."""
|
|
454
|
+
is_command = body.strip().casefold() in ALL_REVIEW_REQUEST_COMMANDS
|
|
455
|
+
if is_command:
|
|
456
|
+
return is_command
|
|
457
|
+
if not _is_bot_author(all_comment_fields):
|
|
458
|
+
return False
|
|
459
|
+
return any(
|
|
460
|
+
each_marker in body for each_marker in (*ALL_NOTICE_COMMENT_MARKERS, *all_extra_markers)
|
|
461
|
+
) or (
|
|
462
|
+
_comment_author_login(all_comment_fields) == CODEX_REVIEW_BOT_LOGIN and _is_codex_notice(body)
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
427
466
|
def parse_top_level_comment(
|
|
428
467
|
all_comment_fields: Mapping[str, object],
|
|
429
468
|
all_extra_notice_markers: Sequence[str] = (),
|
|
430
469
|
) -> TopLevelComment:
|
|
431
470
|
"""Read one top-level comment from the REST answer.
|
|
432
471
|
|
|
433
|
-
|
|
434
|
-
comment
|
|
472
|
+
Command-only review requests and verified bot notices carry no findings.
|
|
473
|
+
A person's comment pasting bot notice text still counts. Notice metadata
|
|
474
|
+
and format must match before a Codex bot comment is exempted.
|
|
435
475
|
|
|
436
476
|
Args:
|
|
437
477
|
all_comment_fields: The comment as the issue comments route reports it.
|
|
@@ -450,11 +490,7 @@ def parse_top_level_comment(
|
|
|
450
490
|
identifier=all_comment_fields.get(COMMENT_IDENTIFIER_KEY),
|
|
451
491
|
author_login=_comment_author_login(all_comment_fields),
|
|
452
492
|
is_bot=is_bot,
|
|
453
|
-
is_notice=
|
|
454
|
-
and any(
|
|
455
|
-
each_marker in body
|
|
456
|
-
for each_marker in (*ALL_NOTICE_COMMENT_MARKERS, *all_extra_notice_markers)
|
|
457
|
-
),
|
|
493
|
+
is_notice=_is_notice_comment(all_comment_fields, body, all_extra_notice_markers),
|
|
458
494
|
created_at=datetime.fromisoformat(str(all_comment_fields.get(CREATED_AT_KEY))),
|
|
459
495
|
updated_at=datetime.fromisoformat(str(all_comment_fields.get(UPDATED_AT_KEY))),
|
|
460
496
|
url=str(all_comment_fields.get(HTML_URL_KEY) or ""),
|