claude-dev-env 8.21.0 → 8.23.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/eli5/SKILL.md +7 -3
- package/.agents/skills/eli5/reference/task-seeds.md +5 -4
- 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 +25 -0
- package/hooks/hooks_constants/advisor_rules_prompt_constants.py +43 -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/hooks/session/advisor_rules_prompt.py +76 -0
- package/hooks/session/test_advisor_rules_prompt.py +86 -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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Tests for advisor_rules_prompt: SessionStart hook that injects advisor consult rules."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
_SESSION_DIR = Path(__file__).resolve().parent
|
|
12
|
+
_HOOKS_ROOT = _SESSION_DIR.parent
|
|
13
|
+
_ADVISOR_TOOL_DOC = _HOOKS_ROOT.parent / "docs" / "references" / "advisor-tool.md"
|
|
14
|
+
for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
|
|
15
|
+
if each_sys_path_entry not in sys.path:
|
|
16
|
+
sys.path.insert(0, each_sys_path_entry)
|
|
17
|
+
|
|
18
|
+
import advisor_rules_prompt as starter
|
|
19
|
+
|
|
20
|
+
from hooks_constants.advisor_rules_prompt_constants import (
|
|
21
|
+
ADVISOR_DISABLE_ENV_VAR,
|
|
22
|
+
ADVISOR_RULES_PROMPT,
|
|
23
|
+
ALL_ADVISOR_RULE_SENTENCES,
|
|
24
|
+
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run_main() -> str:
|
|
29
|
+
captured_stdout = StringIO()
|
|
30
|
+
with patch("sys.stdout", captured_stdout):
|
|
31
|
+
starter.main()
|
|
32
|
+
return captured_stdout.getvalue()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def config_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
37
|
+
monkeypatch.setenv(CLAUDE_CONFIG_DIR_ENV_VAR, str(tmp_path))
|
|
38
|
+
monkeypatch.delenv(ADVISOR_DISABLE_ENV_VAR, raising=False)
|
|
39
|
+
return tmp_path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _write_settings(config_directory: Path, settings: object) -> None:
|
|
43
|
+
(config_directory / "settings.json").write_text(json.dumps(settings), encoding="utf-8")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestAdvisorRulesPrompt:
|
|
47
|
+
def test_should_emit_rules_when_advisor_model_is_set(self, config_directory: Path) -> None:
|
|
48
|
+
_write_settings(config_directory, {"advisorModel": "fable"})
|
|
49
|
+
hook_output = json.loads(_run_main())["hookSpecificOutput"]
|
|
50
|
+
assert hook_output["hookEventName"] == "SessionStart"
|
|
51
|
+
assert hook_output["additionalContext"] == ADVISOR_RULES_PROMPT
|
|
52
|
+
|
|
53
|
+
def test_should_emit_nothing_when_advisor_model_is_missing(
|
|
54
|
+
self, config_directory: Path
|
|
55
|
+
) -> None:
|
|
56
|
+
_write_settings(config_directory, {"permissions": {}})
|
|
57
|
+
assert _run_main() == ""
|
|
58
|
+
|
|
59
|
+
def test_should_emit_nothing_when_advisor_model_is_blank(self, config_directory: Path) -> None:
|
|
60
|
+
_write_settings(config_directory, {"advisorModel": " "})
|
|
61
|
+
assert _run_main() == ""
|
|
62
|
+
|
|
63
|
+
def test_should_emit_nothing_when_settings_file_is_absent(self, config_directory: Path) -> None:
|
|
64
|
+
assert _run_main() == ""
|
|
65
|
+
|
|
66
|
+
def test_should_emit_nothing_when_settings_file_is_not_json(
|
|
67
|
+
self, config_directory: Path
|
|
68
|
+
) -> None:
|
|
69
|
+
(config_directory / "settings.json").write_text("{not json", encoding="utf-8")
|
|
70
|
+
assert _run_main() == ""
|
|
71
|
+
|
|
72
|
+
def test_should_emit_nothing_when_environment_disables_the_advisor(
|
|
73
|
+
self, config_directory: Path, monkeypatch: pytest.MonkeyPatch
|
|
74
|
+
) -> None:
|
|
75
|
+
_write_settings(config_directory, {"advisorModel": "fable"})
|
|
76
|
+
monkeypatch.setenv(ADVISOR_DISABLE_ENV_VAR, "1")
|
|
77
|
+
assert _run_main() == ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestAdvisorRulesMatchTheDoc:
|
|
81
|
+
@pytest.mark.parametrize("each_rule_sentence", ALL_ADVISOR_RULE_SENTENCES)
|
|
82
|
+
def test_should_quote_each_sentence_from_advisor_tool_doc(
|
|
83
|
+
self, each_rule_sentence: str
|
|
84
|
+
) -> None:
|
|
85
|
+
doc_text = " ".join(_ADVISOR_TOOL_DOC.read_text(encoding="utf-8").split())
|
|
86
|
+
assert each_rule_sentence in doc_text
|
package/package.json
CHANGED
|
@@ -26,6 +26,7 @@ definitions and dictionary decisions.
|
|
|
26
26
|
- Use inclusive, neutral language.
|
|
27
27
|
- Use periods, commas, colons, and bullets to show structure.
|
|
28
28
|
- Preserve exact quoted labels, identifiers, formulas, titles, and interface text.
|
|
29
|
+
- Send the reader only a result, a blocker, or a question. Leave out a line that says nothing is needed from them, and leave out which agent, session, or coordinator did the work.
|
|
29
30
|
- Use `WARNING` for a risk of injury or death. Use `CAUTION` for a risk of equipment, tool, or machine damage. State the command or condition first, then state the result.
|
|
30
31
|
- Aim for 20 words or fewer in a procedure sentence when the technical content allows.
|
|
31
32
|
- Aim for 25 words or fewer in a descriptive sentence when the technical content allows.
|
|
@@ -64,6 +64,34 @@ A cache stays available on one condition. CI derives the key itself from the
|
|
|
64
64
|
tree it is about to test, looks for a previous run under that key, and
|
|
65
65
|
republishes that result. CI computes, CI verifies, CI decides.
|
|
66
66
|
|
|
67
|
+
## A repository that runs no CI
|
|
68
|
+
|
|
69
|
+
An owner can rule that a repository spends no CI minutes. There the local gate
|
|
70
|
+
is the gate, and the agent that drives the pull request runs it on every head
|
|
71
|
+
it asks to merge. The repository carries the gate as a `cde verify` manifest at
|
|
72
|
+
`.claude/local-gate.json`: tests with collection floors, and lint.
|
|
73
|
+
|
|
74
|
+
1. Check out the pull request head with a clean tree and fetch its base.
|
|
75
|
+
2. Run the manifest against the base commit the pull request names:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
python <cde>/scripts/local_verification/cli.py --manifest .claude/local-gate.json --repo . --base <base sha> --output <outside the repository>/report.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
3. Publish the report:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
python <cde>/scripts/local_report_publisher.py --token-environment GH_TOKEN --repository <owner>/<name> --pull-number <number> --local-repo . --manifest .claude/local-gate.json --report <outside the repository>/report.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The publisher checks the report against the manifest digest, the clean tree,
|
|
88
|
+
and the live head and base, then posts the `local-checks` commit status. A
|
|
89
|
+
report that fails any of those posts a failure or an error. The branch ruleset
|
|
90
|
+
lists `local-checks` as a required status check, so the host refuses a merge
|
|
91
|
+
until a passing report covers the head, and `agent_merge_check.py` prints
|
|
92
|
+
`HOLD` on the same requirement. A push moves the head and leaves the status
|
|
93
|
+
behind, so each new head runs the gate again.
|
|
94
|
+
|
|
67
95
|
## Sibling rules
|
|
68
96
|
|
|
69
97
|
| Rule | Role |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Memory Stores Durable Facts
|
|
2
|
+
|
|
3
|
+
**When this applies:** Before you write or update a file in an auto-memory
|
|
4
|
+
directory, and before you add its line to `MEMORY.md`.
|
|
5
|
+
|
|
6
|
+
## Rule
|
|
7
|
+
|
|
8
|
+
A memory must still help a fresh session, with no context, weeks later.
|
|
9
|
+
|
|
10
|
+
Run one test on the draft. Strip every date, pull request number, issue number,
|
|
11
|
+
commit, job path, session ID, and task number. When what remains is empty or
|
|
12
|
+
false, the draft is run state. Put it in the task tracker, the pull request, or
|
|
13
|
+
a handoff file beside the work, and save no memory.
|
|
14
|
+
|
|
15
|
+
## What never becomes a memory
|
|
16
|
+
|
|
17
|
+
| Shape | Example | Where it belongs |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Run or program state | "paused at", "resume from", "4 of 11 merged", a pointer to a handoff file | The task tracker or the handoff file |
|
|
20
|
+
| A pull request, issue, stack, or commit map | "#583, #605, #610 merged" | The epic issue or the pull request |
|
|
21
|
+
| A temporary condition | A concurrency schedule, credits out, a fleet that works this week, a fix "until X lands" | The current conversation |
|
|
22
|
+
| One session's topology | A session ID, a parallel session's worktree, a second-account dispatch setup | The current conversation |
|
|
23
|
+
| A workaround for a hook, gate, or verifier as it behaved one day | "the minter never fires, so skip the check" | A fix to the hook, per [`correction-lens.md`](correction-lens.md) |
|
|
24
|
+
| The story of one incident or bug fix | "the fold checkbox took five tries" | Git history and the pull request body |
|
|
25
|
+
| Where code lives, or a design recipe for one build phase | A module layout, a prompt recipe for one engine version | The repository and its inventories |
|
|
26
|
+
| A restatement of a rule file | "CI owns the gate" | The rule file already loaded |
|
|
27
|
+
|
|
28
|
+
## What a memory holds
|
|
29
|
+
|
|
30
|
+
- A preference or standing order the user stated, with the date the user set it.
|
|
31
|
+
- A trap in the local setup or the platform that stays true, such as a shared
|
|
32
|
+
`rr-cache` that replays one-sided resolutions without a marker.
|
|
33
|
+
- A pointer to an external resource: a dashboard, a repository, a tracker.
|
|
34
|
+
|
|
35
|
+
Write the fact first. A date belongs only on a rule the user set on that date.
|
|
36
|
+
|
|
37
|
+
## Keeping the folder current
|
|
38
|
+
|
|
39
|
+
When a memory goes stale, delete it and its `MEMORY.md` line in the same run.
|
|
40
|
+
A "superseded" note on a stale memory keeps a false fact loading into every
|
|
41
|
+
session.
|
|
42
|
+
|
|
43
|
+
## Why
|
|
44
|
+
|
|
45
|
+
One project's memory folder held 104 index entries. A cleanup pass removed 57
|
|
46
|
+
of them, and each one failed the test above. Handoffs outlived their work,
|
|
47
|
+
pull request maps named merged branches, and a capacity schedule and a
|
|
48
|
+
dispatch topology contradicted newer standing orders. Every stale memory loads
|
|
49
|
+
into each session and reads as current.
|
|
50
|
+
|
|
51
|
+
## Sibling rules
|
|
52
|
+
|
|
53
|
+
| Rule | Role |
|
|
54
|
+
|---|---|
|
|
55
|
+
| [`correction-lens.md`](correction-lens.md) | A memory records a decision, and the control holds the behavior |
|
|
56
|
+
| [`verify-before-asking.md`](verify-before-asking.md) | A recalled fact is a claim to re-check |
|
|
57
|
+
| [`cleanup-temp-files.md`](cleanup-temp-files.md) | Scratch output leaves with the task |
|
|
@@ -25,6 +25,7 @@ It prints `CLOSED` and exits 0 when every finding on the head is answered. It pr
|
|
|
25
25
|
| A thread the driving account opened | Itself |
|
|
26
26
|
| A blocking `Claude Approvals` row | A push, which moves the head the check reports on |
|
|
27
27
|
| A top-level comment on the pull request | A later top-level comment from the driving account |
|
|
28
|
+
| A bot notice: a review-skipped note, a pointer to an updated summary, a Graphite verdict mirror, a Qodo change summary, a Qodo in-progress placeholder, or a Qodo review that found no issues | Itself |
|
|
28
29
|
|
|
29
30
|
A review bot rewrites its summary comment on each pass. Its edit leaves an answered summary closed, because each new finding it has arrives as a review thread or a new comment. An edit from a person reopens the comment.
|
|
30
31
|
|
|
@@ -87,12 +87,16 @@ ROLE_BUGTEAM: str = "bugteam"
|
|
|
87
87
|
ROLE_CLEAN_CODER: str = "clean-coder"
|
|
88
88
|
"""Compatibility role for FIX and standards-fix using poteto-agent."""
|
|
89
89
|
|
|
90
|
+
ROLE_TRANSLATOR: str = "translator"
|
|
91
|
+
"""Role the shared_utils translation chain dispatches, served by poteto-agent."""
|
|
92
|
+
|
|
90
93
|
DEFAULT_ROLE: str = ROLE_BUGTEAM
|
|
91
94
|
"""Role applied when the caller does not pass ``--role``."""
|
|
92
95
|
|
|
93
96
|
ALL_AGENT_FILENAMES_BY_ROLE: dict[str, tuple[str, ...]] = {
|
|
94
97
|
ROLE_BUGTEAM: ("poteto-agent.md",),
|
|
95
98
|
ROLE_CLEAN_CODER: ("poteto-agent.md",),
|
|
99
|
+
ROLE_TRANSLATOR: ("poteto-agent.md",),
|
|
96
100
|
}
|
|
97
101
|
"""Agent definition filenames required under ``agents/`` for each known role."""
|
|
98
102
|
|
|
@@ -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:
|