claude-dev-env 8.21.0 → 8.22.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/.agents/skills/sync-dev-env/SKILL.md +51 -0
- package/.agents/skills/sync-dev-env/scripts/link_plugin_skills.py +211 -0
- package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/__init__.py +0 -0
- package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/config/__init__.py +1 -0
- package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/config/constants.py +26 -0
- package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/config/test_constants.py +19 -0
- package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/test___init__.py +5 -0
- package/.agents/skills/sync-dev-env/scripts/test_link_plugin_skills.py +142 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/merge_settings_defaults.test.mjs +43 -0
- package/hooks/blocking/edit_marker_gate.py +74 -0
- package/hooks/blocking/reply_length_gate.py +119 -0
- package/hooks/blocking/test_edit_marker_gate.py +110 -0
- package/hooks/blocking/test_reply_length_gate.py +193 -0
- package/hooks/hooks.json +20 -0
- package/hooks/hooks_constants/edit_marker_gate_constants.py +18 -0
- package/hooks/hooks_constants/reply_length_gate_constants.py +28 -0
- package/hooks/hooks_constants/test_edit_marker_gate_constants.py +9 -0
- package/hooks/hooks_constants/test_reply_length_gate_constants.py +15 -0
- package/package.json +1 -1
- package/rules/asd-ste100-language.md +1 -0
- package/rules/ci-owns-the-gate.md +28 -0
- package/rules/memory-stores-durable-facts.md +57 -0
- package/rules/review-closure-is-a-check.md +1 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +4 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +13 -0
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -0
- package/scripts/invoke_code_review.py +4 -1
- package/scripts/local_report_cli.py +50 -53
- package/scripts/policy_lint/adapter_configuration.py +8 -0
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/resolve_worker_spawn.py +4 -1
- package/scripts/review_closure_model.py +16 -3
- package/scripts/shared_tree_paths.py +27 -0
- package/scripts/test_resolve_worker_spawn.py +28 -0
- package/scripts/test_shared_tree_paths.py +27 -0
- package/scripts/tests/test_adapter_configuration.py +16 -0
- package/scripts/tests/test_local_report_publisher_cli.py +54 -0
- package/scripts/tests/test_review_closure.py +1 -0
- package/scripts/tests/test_review_closure_model.py +70 -0
|
@@ -137,6 +137,19 @@ USER_TYPE_KEY: str = "type"
|
|
|
137
137
|
BOT_USER_TYPE: str = "Bot"
|
|
138
138
|
"""The user type GitHub reports for an app account such as a review bot."""
|
|
139
139
|
|
|
140
|
+
ALL_NOTICE_COMMENT_MARKERS: tuple[str, ...] = (
|
|
141
|
+
"No code changes since the last review",
|
|
142
|
+
"was updated up to the latest commit",
|
|
143
|
+
"<!-- graphite-review-comment -->",
|
|
144
|
+
"<h3>PR Summary by Qodo</h3>",
|
|
145
|
+
"<h3>Qodo is busy working</h3>",
|
|
146
|
+
"<h3>Great, no issues found!</h3>",
|
|
147
|
+
)
|
|
148
|
+
"""Text a bot notice carries: a Qodo skip note, a Qodo pointer to its updated
|
|
149
|
+
summary, a Graphite verdict mirror whose findings arrive as review threads, a
|
|
150
|
+
Qodo description of the change, a Qodo placeholder posted while it reviews, and
|
|
151
|
+
a Qodo review that found nothing."""
|
|
152
|
+
|
|
140
153
|
SHORT_SHA_LENGTH: int = 7
|
|
141
154
|
"""How much of a commit identifier the verdict line prints."""
|
|
142
155
|
|
|
@@ -12,5 +12,9 @@ PROCESS_TREE_DIRECTORY_NAME: str = "process-tree"
|
|
|
12
12
|
|
|
13
13
|
PROCESS_TREE_KILL_MODULE_FILENAME: str = "process_tree_kill.py"
|
|
14
14
|
|
|
15
|
+
ADVISOR_DIRECTORY_NAME: str = "advisor"
|
|
16
|
+
|
|
17
|
+
TIER_MODEL_IDS_MODULE_FILENAME: str = "tier_model_ids.py"
|
|
18
|
+
|
|
15
19
|
AGENTS_DIRECTORY_SUFFIX: str = ".agents"
|
|
16
20
|
DEFAULT_MANAGED_ROOT_NAME: str = ".claude"
|
|
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
|
|
27
27
|
import argparse
|
|
28
28
|
import json
|
|
29
|
+
import os
|
|
29
30
|
import subprocess
|
|
30
31
|
import sys
|
|
31
32
|
from collections.abc import Callable, Sequence
|
|
@@ -41,8 +42,10 @@ sys.path[:] = [
|
|
|
41
42
|
]
|
|
42
43
|
sys.path[:0] = [_scripts_directory]
|
|
43
44
|
|
|
45
|
+
from shared_tree_paths import resolve_shared_advisor_scripts_directory
|
|
46
|
+
|
|
44
47
|
_advisor_scripts_path = str(
|
|
45
|
-
|
|
48
|
+
resolve_shared_advisor_scripts_directory(__file__, all_environment=os.environ)
|
|
46
49
|
)
|
|
47
50
|
sys.path[:] = [
|
|
48
51
|
each_existing_entry
|
|
@@ -2,16 +2,16 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from collections.abc import Mapping, Sequence
|
|
7
|
-
from dataclasses import dataclass
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import TextIO
|
|
10
10
|
|
|
11
11
|
from local_report_core import PublicationOutcome, publish_local_report
|
|
12
12
|
from local_verification.config import SUCCESS_EXIT_CODE
|
|
13
13
|
from pr_verification.config.constants import GIT_BARE_SUFFIX, GITHUB_API_URL
|
|
14
|
-
from pr_verification.github import GitHubAppAuthenticator, GitHubError
|
|
14
|
+
from pr_verification.github import GitHubApi, GitHubAppAuthenticator, GitHubError
|
|
15
15
|
from pr_verification.model import RepositorySettings
|
|
16
16
|
|
|
17
17
|
|
|
@@ -34,8 +34,7 @@ def main(
|
|
|
34
34
|
parser = _build_parser()
|
|
35
35
|
parsed_arguments = parser.parse_args(list(all_arguments))
|
|
36
36
|
try:
|
|
37
|
-
|
|
38
|
-
outcome = _publish_from_settings(settings, parsed_arguments)
|
|
37
|
+
outcome = _publish_from_arguments(parsed_arguments)
|
|
39
38
|
except (OSError, TypeError, ValueError, GitHubError) as error:
|
|
40
39
|
stderr.write(f"{error}\n")
|
|
41
40
|
return 3
|
|
@@ -43,31 +42,57 @@ def main(
|
|
|
43
42
|
return SUCCESS_EXIT_CODE
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
settings.repository, settings.repository + GIT_BARE_SUFFIX
|
|
45
|
+
def _publish_from_arguments(parsed_arguments: argparse.Namespace) -> PublicationOutcome:
|
|
46
|
+
all_settings = _read_settings_file(parsed_arguments.settings)
|
|
47
|
+
api_url = _required_setting(
|
|
48
|
+
parsed_arguments.api_url, all_settings, "api_url", GITHUB_API_URL
|
|
51
49
|
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
settings.app_id,
|
|
55
|
-
settings.installation_id,
|
|
56
|
-
settings.private_key_path,
|
|
50
|
+
repository_slug = _required_repository_setting(
|
|
51
|
+
parsed_arguments.repository, all_settings
|
|
57
52
|
)
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
pull_request_number = _required_integer_setting(
|
|
54
|
+
parsed_arguments.pull_number, all_settings, "pull_number"
|
|
60
55
|
)
|
|
56
|
+
repository = RepositorySettings(repository_slug, repository_slug + GIT_BARE_SUFFIX)
|
|
57
|
+
github = _issue_repository_api(parsed_arguments, all_settings, api_url, repository)
|
|
61
58
|
return publish_local_report(
|
|
62
59
|
github,
|
|
63
60
|
repository,
|
|
64
|
-
|
|
61
|
+
pull_request_number,
|
|
65
62
|
parsed_arguments.local_repo,
|
|
66
63
|
parsed_arguments.manifest,
|
|
67
64
|
parsed_arguments.report,
|
|
68
65
|
)
|
|
69
66
|
|
|
70
67
|
|
|
68
|
+
def _issue_repository_api(
|
|
69
|
+
parsed_arguments: argparse.Namespace,
|
|
70
|
+
all_settings: Mapping[str, object],
|
|
71
|
+
api_url: str,
|
|
72
|
+
repository: RepositorySettings,
|
|
73
|
+
) -> GitHubApi:
|
|
74
|
+
if parsed_arguments.token_environment is not None:
|
|
75
|
+
return GitHubApi(api_url, _environment_token(parsed_arguments.token_environment))
|
|
76
|
+
authenticator = GitHubAppAuthenticator(
|
|
77
|
+
api_url,
|
|
78
|
+
_required_integer_setting(parsed_arguments.app_id, all_settings, "app_id"),
|
|
79
|
+
_required_integer_setting(
|
|
80
|
+
parsed_arguments.installation_id, all_settings, "installation_id"
|
|
81
|
+
),
|
|
82
|
+
_required_path_setting(parsed_arguments.private_key_path, all_settings),
|
|
83
|
+
)
|
|
84
|
+
return authenticator.issue_repository_api(
|
|
85
|
+
repository, should_write_issue_labels=True
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _environment_token(variable_name: str) -> str:
|
|
90
|
+
token = os.environ.get(variable_name)
|
|
91
|
+
if not token:
|
|
92
|
+
raise TypeError(f"Missing publisher token in environment variable: {variable_name}")
|
|
93
|
+
return token
|
|
94
|
+
|
|
95
|
+
|
|
71
96
|
def _outcome_mapping(outcome: PublicationOutcome) -> dict[str, object]:
|
|
72
97
|
return {
|
|
73
98
|
"status": outcome.status.value,
|
|
@@ -76,16 +101,6 @@ def _outcome_mapping(outcome: PublicationOutcome) -> dict[str, object]:
|
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
|
|
79
|
-
@dataclass(frozen=True)
|
|
80
|
-
class _PublisherSettings:
|
|
81
|
-
api_url: str
|
|
82
|
-
app_id: int
|
|
83
|
-
installation_id: int
|
|
84
|
-
private_key_path: Path
|
|
85
|
-
repository: str
|
|
86
|
-
pull_request_number: int
|
|
87
|
-
|
|
88
|
-
|
|
89
104
|
def _build_parser() -> argparse.ArgumentParser:
|
|
90
105
|
parser = argparse.ArgumentParser(prog="cde publish-local-report")
|
|
91
106
|
parser.add_argument("--settings", type=Path)
|
|
@@ -93,6 +108,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
93
108
|
parser.add_argument("--app-id", type=int)
|
|
94
109
|
parser.add_argument("--installation-id", type=int)
|
|
95
110
|
parser.add_argument("--private-key-path", type=Path)
|
|
111
|
+
parser.add_argument("--token-environment")
|
|
96
112
|
parser.add_argument("--repository")
|
|
97
113
|
parser.add_argument("--pull-number", type=int)
|
|
98
114
|
parser.add_argument("--local-repo", type=Path, required=True)
|
|
@@ -101,32 +117,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
101
117
|
return parser
|
|
102
118
|
|
|
103
119
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
raise ValueError("Publisher settings must be an object")
|
|
112
|
-
all_settings = parsed_settings
|
|
113
|
-
return _PublisherSettings(
|
|
114
|
-
_required_setting(
|
|
115
|
-
parsed_arguments.api_url,
|
|
116
|
-
all_settings,
|
|
117
|
-
"api_url",
|
|
118
|
-
GITHUB_API_URL,
|
|
119
|
-
),
|
|
120
|
-
_required_integer_setting(parsed_arguments.app_id, all_settings, "app_id"),
|
|
121
|
-
_required_integer_setting(
|
|
122
|
-
parsed_arguments.installation_id, all_settings, "installation_id"
|
|
123
|
-
),
|
|
124
|
-
_required_path_setting(parsed_arguments.private_key_path, all_settings),
|
|
125
|
-
_required_repository_setting(parsed_arguments.repository, all_settings),
|
|
126
|
-
_required_integer_setting(
|
|
127
|
-
parsed_arguments.pull_number, all_settings, "pull_number"
|
|
128
|
-
),
|
|
129
|
-
)
|
|
120
|
+
def _read_settings_file(settings_path: Path | None) -> Mapping[str, object]:
|
|
121
|
+
if settings_path is None:
|
|
122
|
+
return {}
|
|
123
|
+
parsed_settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
124
|
+
if not isinstance(parsed_settings, Mapping):
|
|
125
|
+
raise ValueError("Publisher settings must be an object")
|
|
126
|
+
return parsed_settings
|
|
130
127
|
|
|
131
128
|
|
|
132
129
|
def _required_setting(
|
|
@@ -128,6 +128,8 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
128
128
|
|
|
129
129
|
hooks/blocking/bash_pre_tool_use_dispatcher.py -> exempt
|
|
130
130
|
hooks/blocking/step_note_gate.py -> exempt
|
|
131
|
+
hooks/blocking/reply_length_gate.py -> exempt
|
|
132
|
+
hooks/blocking/edit_marker_gate.py -> exempt
|
|
131
133
|
hooks/blocking/some_new_blocker.py -> flagged
|
|
132
134
|
|
|
133
135
|
The Bash PreToolUse dispatcher sits under ``blocking/`` for layout reasons
|
|
@@ -137,6 +139,12 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
137
139
|
The step-note gate allows every call until the user runs ``/step-notes on``.
|
|
138
140
|
It asks for a readable status line and decides no code or safety policy.
|
|
139
141
|
|
|
142
|
+
The reply length gate caps the sentences in a chat reply to the user. It
|
|
143
|
+
decides the shape of prose and no code or safety policy.
|
|
144
|
+
|
|
145
|
+
The edit marker gate keeps strikethrough and edit notes out of an edited
|
|
146
|
+
chat message. It decides the shape of prose and no code or safety policy.
|
|
147
|
+
|
|
140
148
|
Args:
|
|
141
149
|
registered_string: One command, path, script, or entrypoint string.
|
|
142
150
|
"""
|
|
@@ -64,6 +64,8 @@ ALL_ACTION_BOUNDARY_PREFIXES = ("deny_", "block_", "ask_")
|
|
|
64
64
|
ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
65
65
|
"blocking/bash_pre_tool_use_dispatcher.py",
|
|
66
66
|
"blocking/step_note_gate.py",
|
|
67
|
+
"blocking/reply_length_gate.py",
|
|
68
|
+
"blocking/edit_marker_gate.py",
|
|
67
69
|
)
|
|
68
70
|
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
69
71
|
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
@@ -24,6 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
26
|
import json
|
|
27
|
+
import os
|
|
27
28
|
import subprocess
|
|
28
29
|
import sys
|
|
29
30
|
from collections.abc import Callable, Sequence
|
|
@@ -40,8 +41,10 @@ sys.path[:] = [
|
|
|
40
41
|
]
|
|
41
42
|
sys.path[:0] = [_scripts_directory]
|
|
42
43
|
|
|
44
|
+
from shared_tree_paths import resolve_shared_advisor_scripts_directory
|
|
45
|
+
|
|
43
46
|
_advisor_scripts_path = str(
|
|
44
|
-
|
|
47
|
+
resolve_shared_advisor_scripts_directory(__file__, all_environment=os.environ)
|
|
45
48
|
)
|
|
46
49
|
sys.path[:] = [
|
|
47
50
|
each_existing_entry
|
|
@@ -15,12 +15,16 @@ and the approvals conclusion, and name what waits.
|
|
|
15
15
|
top-level: a bot comment, then an agent one -> closed
|
|
16
16
|
top-level: the bot edits after that reply -> closed
|
|
17
17
|
top-level: a person edits after that reply -> open
|
|
18
|
+
top-level: a bot notice with no finding -> closed
|
|
18
19
|
|
|
19
20
|
A red circle marks a finding a review states as blocking, so resolution alone
|
|
20
21
|
leaves it open. The reply says what changed, or why the finding stands.
|
|
21
22
|
A review bot rewrites its summary comment on each pass, and any new finding it
|
|
22
23
|
has arrives as a review thread or a new comment, so its edit alone reopens
|
|
23
|
-
nothing.
|
|
24
|
+
nothing. A bot notice carries a skip note, a pointer to an updated summary, a
|
|
25
|
+
verdict mirror, a description of the change, a placeholder posted while the bot
|
|
26
|
+
reviews, or a review that found nothing, and never a finding, so it waits on
|
|
27
|
+
nobody.
|
|
24
28
|
"""
|
|
25
29
|
|
|
26
30
|
from __future__ import annotations
|
|
@@ -33,6 +37,7 @@ from dev_env_scripts_constants.review_closure_constants import (
|
|
|
33
37
|
ALL_BLOCKING_CONCLUSIONS,
|
|
34
38
|
ALL_COMMENT_AUTHOR_KEYS,
|
|
35
39
|
ALL_COMMENT_LIST_KEYS,
|
|
40
|
+
ALL_NOTICE_COMMENT_MARKERS,
|
|
36
41
|
ALL_OUTDATED_KEYS,
|
|
37
42
|
ALL_RESOLVED_KEYS,
|
|
38
43
|
APPROVALS_CHECK_NAME,
|
|
@@ -93,6 +98,7 @@ class TopLevelComment:
|
|
|
93
98
|
identifier: object
|
|
94
99
|
author_login: str
|
|
95
100
|
is_bot: bool
|
|
101
|
+
is_notice: bool
|
|
96
102
|
created_at: datetime
|
|
97
103
|
updated_at: datetime
|
|
98
104
|
url: str
|
|
@@ -217,7 +223,7 @@ def top_level_finding(
|
|
|
217
223
|
Returns:
|
|
218
224
|
The finding this comment leaves open, or None when it is answered.
|
|
219
225
|
"""
|
|
220
|
-
if comment.author_login in all_driver_logins:
|
|
226
|
+
if comment.author_login in all_driver_logins or comment.is_notice:
|
|
221
227
|
return None
|
|
222
228
|
last_counted_at = comment.created_at if comment.is_bot else comment.updated_at
|
|
223
229
|
if latest_driver_time is not None and last_counted_at <= latest_driver_time:
|
|
@@ -423,6 +429,9 @@ def parse_top_level_comment(
|
|
|
423
429
|
) -> TopLevelComment:
|
|
424
430
|
"""Read one top-level comment from the REST answer.
|
|
425
431
|
|
|
432
|
+
A bot comment whose body carries a notice marker is a notice. A person's
|
|
433
|
+
comment carrying the same text still counts, since anyone can paste it.
|
|
434
|
+
|
|
426
435
|
Args:
|
|
427
436
|
all_comment_fields: The comment as the issue comments route reports it.
|
|
428
437
|
|
|
@@ -432,10 +441,14 @@ def parse_top_level_comment(
|
|
|
432
441
|
Raises:
|
|
433
442
|
ValueError: A timestamp is missing or does not parse.
|
|
434
443
|
"""
|
|
444
|
+
is_bot = _is_bot_author(all_comment_fields)
|
|
445
|
+
body = str(all_comment_fields.get(COMMENT_BODY_KEY) or "")
|
|
435
446
|
return TopLevelComment(
|
|
436
447
|
identifier=all_comment_fields.get(COMMENT_IDENTIFIER_KEY),
|
|
437
448
|
author_login=_comment_author_login(all_comment_fields),
|
|
438
|
-
is_bot=
|
|
449
|
+
is_bot=is_bot,
|
|
450
|
+
is_notice=is_bot
|
|
451
|
+
and any(each_marker in body for each_marker in ALL_NOTICE_COMMENT_MARKERS),
|
|
439
452
|
created_at=datetime.fromisoformat(str(all_comment_fields.get(CREATED_AT_KEY))),
|
|
440
453
|
updated_at=datetime.fromisoformat(str(all_comment_fields.get(UPDATED_AT_KEY))),
|
|
441
454
|
url=str(all_comment_fields.get(HTML_URL_KEY) or ""),
|
|
@@ -11,6 +11,7 @@ from collections.abc import Mapping
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from dev_env_scripts_constants.shared_tree_constants import (
|
|
14
|
+
ADVISOR_DIRECTORY_NAME,
|
|
14
15
|
AGENTS_DIRECTORY_SUFFIX,
|
|
15
16
|
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
16
17
|
DEFAULT_MANAGED_ROOT_NAME,
|
|
@@ -18,6 +19,7 @@ from dev_env_scripts_constants.shared_tree_constants import (
|
|
|
18
19
|
PROCESS_TREE_KILL_MODULE_FILENAME,
|
|
19
20
|
SCRIPTS_DIRECTORY_NAME,
|
|
20
21
|
SHARED_PACKAGE_DIRECTORY_NAME,
|
|
22
|
+
TIER_MODEL_IDS_MODULE_FILENAME,
|
|
21
23
|
)
|
|
22
24
|
|
|
23
25
|
|
|
@@ -131,3 +133,28 @@ def resolve_shared_process_tree_scripts_directory(
|
|
|
131
133
|
PROCESS_TREE_KILL_MODULE_FILENAME,
|
|
132
134
|
1,
|
|
133
135
|
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def resolve_shared_advisor_scripts_directory(
|
|
139
|
+
module_file: str | Path,
|
|
140
|
+
all_environment: Mapping[str, str],
|
|
141
|
+
) -> Path:
|
|
142
|
+
"""Return the advisor scripts directory that holds the tier model ids.
|
|
143
|
+
|
|
144
|
+
Names the advisor sub-package for a caller sitting one level under the
|
|
145
|
+
installed root, such as a dispatcher in the managed ``scripts`` directory.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
module_file: Path of the importing module (``__file__``).
|
|
149
|
+
all_environment: Mapping that may hold ``CLAUDE_CONFIG_DIR``.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Directory that should hold ``tier_model_ids.py``.
|
|
153
|
+
"""
|
|
154
|
+
return resolve_shared_scripts_directory(
|
|
155
|
+
module_file,
|
|
156
|
+
all_environment,
|
|
157
|
+
ADVISOR_DIRECTORY_NAME,
|
|
158
|
+
TIER_MODEL_IDS_MODULE_FILENAME,
|
|
159
|
+
1,
|
|
160
|
+
)
|
|
@@ -56,6 +56,7 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
56
56
|
REASON_GROK_AUTH_FAILED,
|
|
57
57
|
REASON_PROMPT_FILE_MISSING,
|
|
58
58
|
REASON_TIMEOUT_OUT_OF_BOUNDS,
|
|
59
|
+
ROLE_TRANSLATOR,
|
|
59
60
|
RESULT_KEY_ATTEMPTS,
|
|
60
61
|
RESULT_KEY_OK,
|
|
61
62
|
RESULT_KEY_OUTPUT,
|
|
@@ -865,6 +866,33 @@ def test_tier_three_argv_includes_agent_for_default_role(
|
|
|
865
866
|
)
|
|
866
867
|
|
|
867
868
|
|
|
869
|
+
def test_translator_role_dispatches_the_poteto_agent_on_tier_three(
|
|
870
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
871
|
+
) -> None:
|
|
872
|
+
prompt_file, working_directory, run_state_directory = _paths(tmp_path)
|
|
873
|
+
call_log = _install_seams(
|
|
874
|
+
monkeypatch,
|
|
875
|
+
grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
|
|
876
|
+
claude_outcome=_claude_served(),
|
|
877
|
+
host_profile=HOST_PROFILE_CLAUDE,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
spawn_outcome = dispatcher.resolve_worker_spawn(
|
|
881
|
+
role=ROLE_TRANSLATOR,
|
|
882
|
+
prompt_file=prompt_file,
|
|
883
|
+
working_directory=working_directory,
|
|
884
|
+
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
885
|
+
is_claude_tier_enabled=True,
|
|
886
|
+
run_state_directory=run_state_directory,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
|
|
890
|
+
assert call_log.claude_arguments is not None
|
|
891
|
+
agent_flag_index = call_log.claude_arguments.index(AGENT_FLAG)
|
|
892
|
+
assert call_log.claude_arguments[agent_flag_index + 1] == "poteto-agent"
|
|
893
|
+
assert ROLE_TRANSLATOR not in call_log.claude_arguments
|
|
894
|
+
|
|
895
|
+
|
|
868
896
|
def test_large_prompt_stays_out_of_claude_argv(
|
|
869
897
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
870
898
|
) -> None:
|
|
@@ -21,6 +21,10 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
21
21
|
SCRIPTS_DIRECTORY_NAME,
|
|
22
22
|
SHARED_PACKAGE_DIRECTORY_NAME,
|
|
23
23
|
)
|
|
24
|
+
from dev_env_scripts_constants.shared_tree_constants import (
|
|
25
|
+
ADVISOR_DIRECTORY_NAME,
|
|
26
|
+
TIER_MODEL_IDS_MODULE_FILENAME,
|
|
27
|
+
)
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
def _create_directory_link(*, from_link: Path, to_target: Path) -> None:
|
|
@@ -143,6 +147,29 @@ def test_missing_module_returns_unresolved_candidate(tmp_path: Path) -> None:
|
|
|
143
147
|
).is_file()
|
|
144
148
|
|
|
145
149
|
|
|
150
|
+
def test_junction_scripts_dir_finds_advisor_scripts_on_unresolved_parent(
|
|
151
|
+
tmp_path: Path,
|
|
152
|
+
) -> None:
|
|
153
|
+
module_file = _build_junction_scripts_layout(tmp_path)
|
|
154
|
+
expected_scripts_directory = (
|
|
155
|
+
tmp_path
|
|
156
|
+
/ "root"
|
|
157
|
+
/ SHARED_PACKAGE_DIRECTORY_NAME
|
|
158
|
+
/ ADVISOR_DIRECTORY_NAME
|
|
159
|
+
/ SCRIPTS_DIRECTORY_NAME
|
|
160
|
+
)
|
|
161
|
+
expected_scripts_directory.mkdir(parents=True)
|
|
162
|
+
(expected_scripts_directory / TIER_MODEL_IDS_MODULE_FILENAME).write_text(
|
|
163
|
+
"", encoding="utf-8"
|
|
164
|
+
)
|
|
165
|
+
resolved_scripts_directory = (
|
|
166
|
+
shared_tree_paths.resolve_shared_advisor_scripts_directory(
|
|
167
|
+
module_file, all_environment={}
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
assert resolved_scripts_directory == expected_scripts_directory
|
|
171
|
+
|
|
172
|
+
|
|
146
173
|
def test_generalized_resolver_serves_a_deeper_anchor(tmp_path: Path) -> None:
|
|
147
174
|
"""A caller three levels under the root resolves its own sub-package.
|
|
148
175
|
|
|
@@ -58,6 +58,22 @@ def test_new_step_note_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
|
58
58
|
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def test_new_reply_length_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
62
|
+
current_document = _hook_document(
|
|
63
|
+
["hooks/blocking/reply_length_gate.py"],
|
|
64
|
+
'{"hooks": {}}',
|
|
65
|
+
)
|
|
66
|
+
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_new_edit_marker_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
70
|
+
current_document = _hook_document(
|
|
71
|
+
["hooks/blocking/edit_marker_gate.py"],
|
|
72
|
+
'{"hooks": {}}',
|
|
73
|
+
)
|
|
74
|
+
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
75
|
+
|
|
76
|
+
|
|
61
77
|
def test_new_blocker_in_staged_change_is_rejected(tmp_path: Path) -> None:
|
|
62
78
|
current_document = _hook_document(
|
|
63
79
|
["hooks/blocking/new.py"],
|
|
@@ -6,6 +6,8 @@ import subprocess
|
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
9
11
|
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
10
12
|
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
11
13
|
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
@@ -83,3 +85,55 @@ def test_publisher_script_displays_help_without_authentication() -> None:
|
|
|
83
85
|
|
|
84
86
|
assert completed_process.returncode == 0
|
|
85
87
|
assert "usage:" in completed_process.stdout
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _token_arguments(tmp_path: Path, variable_name: str) -> list[str]:
|
|
91
|
+
return [
|
|
92
|
+
"--api-url",
|
|
93
|
+
"http://127.0.0.1:9",
|
|
94
|
+
"--token-environment",
|
|
95
|
+
variable_name,
|
|
96
|
+
"--repository",
|
|
97
|
+
"owner/repository",
|
|
98
|
+
"--pull-number",
|
|
99
|
+
"7",
|
|
100
|
+
"--local-repo",
|
|
101
|
+
str(tmp_path),
|
|
102
|
+
"--manifest",
|
|
103
|
+
str(tmp_path / "manifest.json"),
|
|
104
|
+
"--report",
|
|
105
|
+
str(tmp_path / "report.json"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_cli_names_missing_token_variable_without_network_access(
|
|
110
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
111
|
+
) -> None:
|
|
112
|
+
monkeypatch.delenv("PUBLISHER_TEST_TOKEN", raising=False)
|
|
113
|
+
standard_output = io.StringIO()
|
|
114
|
+
standard_error = io.StringIO()
|
|
115
|
+
|
|
116
|
+
exit_code = main(
|
|
117
|
+
_token_arguments(tmp_path, "PUBLISHER_TEST_TOKEN"),
|
|
118
|
+
stdout=standard_output,
|
|
119
|
+
stderr=standard_error,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
assert exit_code == 3
|
|
123
|
+
assert standard_output.getvalue() == ""
|
|
124
|
+
assert "PUBLISHER_TEST_TOKEN" in standard_error.getvalue()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_cli_token_path_reaches_the_api_without_app_settings(
|
|
128
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
129
|
+
) -> None:
|
|
130
|
+
monkeypatch.setenv("PUBLISHER_TEST_TOKEN", "token")
|
|
131
|
+
standard_error = io.StringIO()
|
|
132
|
+
|
|
133
|
+
exit_code = main(
|
|
134
|
+
_token_arguments(tmp_path, "PUBLISHER_TEST_TOKEN"), stderr=standard_error
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert exit_code == 3
|
|
138
|
+
assert "Missing publisher setting" not in standard_error.getvalue()
|
|
139
|
+
assert "refused" in standard_error.getvalue().lower()
|
|
@@ -4,6 +4,8 @@ import sys
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
7
9
|
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
8
10
|
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
9
11
|
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
@@ -315,11 +317,13 @@ def top_level_comment(
|
|
|
315
317
|
edited_minute: int | None = None,
|
|
316
318
|
url: str = BOT_SUMMARY_URL,
|
|
317
319
|
is_bot: bool = True,
|
|
320
|
+
is_notice: bool = False,
|
|
318
321
|
) -> model.TopLevelComment:
|
|
319
322
|
return model.TopLevelComment(
|
|
320
323
|
identifier=posted_minute,
|
|
321
324
|
author_login=author_login,
|
|
322
325
|
is_bot=is_bot,
|
|
326
|
+
is_notice=is_notice,
|
|
323
327
|
created_at=at_minute(posted_minute),
|
|
324
328
|
updated_at=at_minute(posted_minute if edited_minute is None else edited_minute),
|
|
325
329
|
url=url,
|
|
@@ -445,6 +449,72 @@ def should_open_a_bot_comment_posted_after_the_driver_time() -> None:
|
|
|
445
449
|
)
|
|
446
450
|
|
|
447
451
|
|
|
452
|
+
def should_close_a_bot_notice_posted_after_the_driver_time() -> None:
|
|
453
|
+
comment = top_level_comment(REVIEW_BOT, 5, is_notice=True)
|
|
454
|
+
|
|
455
|
+
assert model.top_level_finding(comment, DRIVING_AGENT, at_minute(4)) is None
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@pytest.mark.parametrize(
|
|
459
|
+
"body",
|
|
460
|
+
[
|
|
461
|
+
"No code changes since the last review \u2014 review skipped",
|
|
462
|
+
"[Code review](https://example.test/pull/7#issuecomment-1) by qodo was "
|
|
463
|
+
"updated up to the latest commit https://example.test/commit/abc",
|
|
464
|
+
"<!-- graphite-review-comment -->\n\n### Graphite AI review",
|
|
465
|
+
"<h3>PR Summary by Qodo</h3>\n\nBuild the default map per instance",
|
|
466
|
+
"\n<h3>Qodo is busy working</h3>\n\nCheck back in a few minutes.",
|
|
467
|
+
"\n<h3>Code Review by Qodo</h3>\n<code>\U0001f41e Bugs (0)</code>\n\n"
|
|
468
|
+
"<h3>Great, no issues found!</h3>\nQodo reviewed your code",
|
|
469
|
+
],
|
|
470
|
+
)
|
|
471
|
+
def should_read_a_bot_notice_as_a_notice(body: str) -> None:
|
|
472
|
+
comment = model.parse_top_level_comment(notice_record(body, "Bot"))
|
|
473
|
+
|
|
474
|
+
assert comment.is_notice
|
|
475
|
+
assert model.top_level_findings((comment,), DRIVING_AGENT) == ()
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def should_read_a_person_pasting_notice_text_as_a_comment() -> None:
|
|
479
|
+
comment = model.parse_top_level_comment(
|
|
480
|
+
notice_record("No code changes since the last review", "User")
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
assert not comment.is_notice
|
|
484
|
+
assert len(model.top_level_findings((comment,), DRIVING_AGENT)) == 1
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def should_read_a_bot_finding_as_a_comment() -> None:
|
|
488
|
+
comment = model.parse_top_level_comment(
|
|
489
|
+
notice_record("A finding: the loop never ends.", "Bot")
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
assert not comment.is_notice
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def should_read_a_qodo_review_with_bugs_as_a_comment() -> None:
|
|
496
|
+
comment = model.parse_top_level_comment(
|
|
497
|
+
notice_record(
|
|
498
|
+
"\n<h3>Code Review by Qodo</h3>\n<code>\U0001f41e Bugs (2)</code>\n\n"
|
|
499
|
+
"1. The loop never ends.",
|
|
500
|
+
"Bot",
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
assert not comment.is_notice
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def notice_record(body: str, user_type: str) -> dict[str, object]:
|
|
508
|
+
return {
|
|
509
|
+
"id": 1,
|
|
510
|
+
"user": {"login": REVIEW_BOT, "type": user_type},
|
|
511
|
+
"body": body,
|
|
512
|
+
"created_at": "2026-09-26T12:05:00+00:00",
|
|
513
|
+
"updated_at": "2026-09-26T12:05:00+00:00",
|
|
514
|
+
"html_url": BOT_SUMMARY_URL,
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
|
|
448
518
|
def should_count_waiting_top_level_comments_among_the_findings() -> None:
|
|
449
519
|
all_findings = model.all_open_findings(
|
|
450
520
|
(), (top_level_comment(REVIEW_BOT, 1),), DRIVING_AGENT, None
|