claude-dev-env 2.14.1 → 2.15.1
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/_shared/advisor/scripts/tier_model_ids.py +6 -2
- package/bin/AGENTS.md +6 -4
- package/bin/install-constants.mjs +51 -0
- package/bin/install.codex-rules.test.mjs +173 -0
- package/bin/install.cursor-rules.test.mjs +103 -0
- package/bin/install.mjs +165 -51
- package/bin/install.profile-root.test.mjs +8 -0
- package/bin/install.prune.test.mjs +2 -1
- package/bin/install.test.mjs +42 -8
- package/bin/install.transaction.test.mjs +1 -0
- package/bin/install.uninstall-transaction.test.mjs +1 -0
- package/bin/resolve-install-root.mjs +43 -10
- package/codex-rules/claude-dev-env.rules +12 -0
- package/package.json +3 -2
- package/scripts/AGENTS.md +0 -7
- package/scripts/claude_chain_runner.py +49 -2
- package/scripts/invoke_code_review.py +48 -51
- package/scripts/resolve_worker_spawn.py +42 -47
- package/scripts/sync_to_cursor/AGENTS.md +3 -3
- package/scripts/sync_to_cursor/canonical_docs.py +11 -11
- package/scripts/sync_to_cursor/config/__init__.py +8 -0
- package/scripts/sync_to_cursor/engine.py +26 -1
- package/scripts/sync_to_cursor/rules.py +76 -5
- package/scripts/test_claude_chain_runner.py +28 -0
- package/scripts/test_dispatcher_profile_import.py +184 -0
- package/scripts/test_resolve_worker_spawn.py +119 -1
- package/scripts/test_validate_instruction_pairs.py +30 -0
- package/scripts/tests/AGENTS.md +2 -0
- package/scripts/tests/test_engine.py +102 -0
- package/scripts/tests/test_rules.py +79 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +0 -25
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +0 -60
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +0 -54
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +0 -64
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +0 -180
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +0 -288
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +0 -275
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +0 -221
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +0 -108
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +0 -147
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +0 -102
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +0 -210
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +0 -299
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +0 -127
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +0 -51
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +0 -77
- package/scripts/sync_to_cursor/config.py +0 -5
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Exercise the installed dispatcher layout in isolated profile trees."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
_DISPATCHER_NAMES = ("resolve_worker_spawn.py", "invoke_code_review.py")
|
|
14
|
+
_SHARED_DIRECTORY_NAMES = ("advisor", "process-tree")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _stage_profile_tree(
|
|
18
|
+
source_package_directory: Path, target_profile_directory: Path
|
|
19
|
+
) -> Path:
|
|
20
|
+
source_scripts_directory = source_package_directory / "scripts"
|
|
21
|
+
target_scripts_directory = target_profile_directory / "scripts"
|
|
22
|
+
shutil.copytree(source_scripts_directory, target_scripts_directory)
|
|
23
|
+
for each_shared_name in _SHARED_DIRECTORY_NAMES:
|
|
24
|
+
source_shared_directory = (
|
|
25
|
+
source_package_directory / "_shared" / each_shared_name
|
|
26
|
+
)
|
|
27
|
+
target_shared_directory = (
|
|
28
|
+
target_profile_directory / "_shared" / each_shared_name
|
|
29
|
+
)
|
|
30
|
+
shutil.copytree(source_shared_directory, target_shared_directory)
|
|
31
|
+
return target_scripts_directory
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _stage_shadow_import_root(
|
|
35
|
+
target_profile_directory: Path, dispatcher_module_name: str
|
|
36
|
+
) -> Path:
|
|
37
|
+
shadow_import_root = (
|
|
38
|
+
target_profile_directory / f"shadow-import-root-{dispatcher_module_name}"
|
|
39
|
+
)
|
|
40
|
+
shadow_import_root.mkdir()
|
|
41
|
+
(shadow_import_root / "tier_model_ids.py").write_text(
|
|
42
|
+
"raise RuntimeError('shadow tier_model_ids imported')\n",
|
|
43
|
+
encoding="utf-8",
|
|
44
|
+
)
|
|
45
|
+
(shadow_import_root / "claude_chain_runner.py").write_text(
|
|
46
|
+
"raise RuntimeError('shadow claude_chain_runner imported')\n",
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
)
|
|
49
|
+
shadow_constants_root = shadow_import_root / "advisor_scripts_constants"
|
|
50
|
+
shadow_constants_root.mkdir()
|
|
51
|
+
(shadow_constants_root / "__init__.py").write_text(
|
|
52
|
+
"raise RuntimeError('shadow advisor constants imported')\n",
|
|
53
|
+
encoding="utf-8",
|
|
54
|
+
)
|
|
55
|
+
shadow_scripts_constants_root = shadow_import_root / "dev_env_scripts_constants"
|
|
56
|
+
shadow_scripts_constants_root.mkdir()
|
|
57
|
+
(shadow_scripts_constants_root / "__init__.py").write_text(
|
|
58
|
+
"raise RuntimeError('shadow dev-env constants imported')\n",
|
|
59
|
+
encoding="utf-8",
|
|
60
|
+
)
|
|
61
|
+
return shadow_import_root
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.fixture(scope="module")
|
|
65
|
+
def installed_dispatcher_scripts(
|
|
66
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
67
|
+
) -> Path:
|
|
68
|
+
"""Stage one installed profile for both dispatcher import checks."""
|
|
69
|
+
source_package_directory = Path(__file__).resolve().parent.parent
|
|
70
|
+
target_profile_directory = tmp_path_factory.mktemp("dispatcher_profile") / ".claude"
|
|
71
|
+
return _stage_profile_tree(source_package_directory, target_profile_directory)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize("dispatcher_name", _DISPATCHER_NAMES)
|
|
75
|
+
def test_installed_dispatcher_help_imports_advisor_constants(
|
|
76
|
+
dispatcher_name: str, installed_dispatcher_scripts: Path
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Each deployed dispatcher imports and serves help from an isolated tree."""
|
|
79
|
+
target_scripts_directory = installed_dispatcher_scripts
|
|
80
|
+
target_profile_directory = target_scripts_directory.parent
|
|
81
|
+
|
|
82
|
+
completed_process = subprocess.run(
|
|
83
|
+
[
|
|
84
|
+
sys.executable,
|
|
85
|
+
"-S",
|
|
86
|
+
"-E",
|
|
87
|
+
str(target_scripts_directory / dispatcher_name),
|
|
88
|
+
"--help",
|
|
89
|
+
],
|
|
90
|
+
cwd=target_profile_directory,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
text=True,
|
|
93
|
+
check=False,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
assert completed_process.returncode == 0
|
|
97
|
+
assert completed_process.stderr == ""
|
|
98
|
+
assert "usage:" in completed_process.stdout
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize("dispatcher_name", _DISPATCHER_NAMES)
|
|
102
|
+
def test_imported_dispatcher_promotes_all_profile_owned_roots(
|
|
103
|
+
dispatcher_name: str, installed_dispatcher_scripts: Path
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Fresh imported-module loading selects every installed profile root."""
|
|
106
|
+
dispatcher_module_name = dispatcher_name.rsplit(".", maxsplit=1)[0]
|
|
107
|
+
target_profile_directory = installed_dispatcher_scripts.parent
|
|
108
|
+
shadow_import_root = _stage_shadow_import_root(
|
|
109
|
+
target_profile_directory, dispatcher_module_name
|
|
110
|
+
)
|
|
111
|
+
advisor_scripts_root = target_profile_directory / "_shared" / "advisor" / "scripts"
|
|
112
|
+
advisor_config_root = advisor_scripts_root / "config"
|
|
113
|
+
process_tree_scripts_root = (
|
|
114
|
+
target_profile_directory / "_shared" / "process-tree" / "scripts"
|
|
115
|
+
)
|
|
116
|
+
process_tree_config_root = process_tree_scripts_root / "config"
|
|
117
|
+
all_expected_roots = [
|
|
118
|
+
str(advisor_config_root),
|
|
119
|
+
str(advisor_scripts_root),
|
|
120
|
+
str(installed_dispatcher_scripts),
|
|
121
|
+
str(shadow_import_root),
|
|
122
|
+
]
|
|
123
|
+
if dispatcher_module_name == "resolve_worker_spawn":
|
|
124
|
+
all_expected_roots = [
|
|
125
|
+
str(process_tree_config_root),
|
|
126
|
+
str(process_tree_scripts_root),
|
|
127
|
+
*all_expected_roots,
|
|
128
|
+
]
|
|
129
|
+
child_code = "\n".join(
|
|
130
|
+
(
|
|
131
|
+
"import importlib",
|
|
132
|
+
"import json",
|
|
133
|
+
"import sys",
|
|
134
|
+
"from pathlib import Path",
|
|
135
|
+
f"dispatcher_module_name = {dispatcher_module_name!r}",
|
|
136
|
+
"shadow_import_root, scripts_root, advisor_scripts_root, advisor_config_root = (",
|
|
137
|
+
f" {str(shadow_import_root)!r},",
|
|
138
|
+
f" {str(installed_dispatcher_scripts)!r},",
|
|
139
|
+
f" {str(advisor_scripts_root)!r},",
|
|
140
|
+
f" {str(advisor_config_root)!r},",
|
|
141
|
+
")",
|
|
142
|
+
"sys.path[:0] = [",
|
|
143
|
+
" shadow_import_root,",
|
|
144
|
+
" scripts_root,",
|
|
145
|
+
" advisor_scripts_root,",
|
|
146
|
+
" advisor_config_root,",
|
|
147
|
+
"]",
|
|
148
|
+
"assert all(each_name not in sys.modules for each_name in (",
|
|
149
|
+
" dispatcher_module_name,",
|
|
150
|
+
" 'tier_model_ids',",
|
|
151
|
+
" 'advisor_scripts_constants',",
|
|
152
|
+
"))",
|
|
153
|
+
"dispatcher = importlib.import_module(dispatcher_module_name)",
|
|
154
|
+
"tier_model_ids = importlib.import_module('tier_model_ids')",
|
|
155
|
+
"advisor_scripts_constants = importlib.import_module('advisor_scripts_constants')",
|
|
156
|
+
"print(json.dumps({",
|
|
157
|
+
" 'dispatcher': str(Path(dispatcher.__file__).resolve()),",
|
|
158
|
+
" 'tier_model_ids': str(Path(tier_model_ids.__file__).resolve()),",
|
|
159
|
+
" 'advisor_scripts_constants': str(Path(advisor_scripts_constants.__file__).resolve()),",
|
|
160
|
+
" 'roots': sys.path[:6],",
|
|
161
|
+
"}))",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
completed_process = subprocess.run(
|
|
165
|
+
[sys.executable, "-S", "-E", "-c", child_code],
|
|
166
|
+
cwd=target_profile_directory,
|
|
167
|
+
capture_output=True,
|
|
168
|
+
text=True,
|
|
169
|
+
check=False,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert completed_process.returncode == 0, completed_process.stderr
|
|
173
|
+
assert completed_process.stderr == ""
|
|
174
|
+
all_import_results = json.loads(completed_process.stdout)
|
|
175
|
+
assert all_import_results["dispatcher"] == str(
|
|
176
|
+
installed_dispatcher_scripts / dispatcher_name
|
|
177
|
+
)
|
|
178
|
+
assert all_import_results["tier_model_ids"] == str(
|
|
179
|
+
advisor_scripts_root / "tier_model_ids.py"
|
|
180
|
+
)
|
|
181
|
+
assert all_import_results["advisor_scripts_constants"] == str(
|
|
182
|
+
advisor_config_root / "advisor_scripts_constants" / "__init__.py"
|
|
183
|
+
)
|
|
184
|
+
assert all_import_results["roots"][: len(all_expected_roots)] == all_expected_roots
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import io
|
|
5
6
|
import json
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
@@ -9,6 +10,7 @@ import threading
|
|
|
9
10
|
from collections.abc import Sequence
|
|
10
11
|
from dataclasses import dataclass, field
|
|
11
12
|
from pathlib import Path
|
|
13
|
+
from typing import Self
|
|
12
14
|
|
|
13
15
|
import pytest
|
|
14
16
|
|
|
@@ -17,6 +19,7 @@ if str(_SCRIPTS_DIR) not in sys.path:
|
|
|
17
19
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
18
20
|
|
|
19
21
|
import claude_chain_runner as chain_runner # noqa: E402
|
|
22
|
+
import invoke_code_review as invoker
|
|
20
23
|
import resolve_worker_spawn as dispatcher # noqa: E402
|
|
21
24
|
from claude_chain_runner import ( # noqa: E402
|
|
22
25
|
ChainAttempt,
|
|
@@ -87,6 +90,7 @@ FIXTURE_ROLE = "code-quality-agent"
|
|
|
87
90
|
MIN_WORKER_TIMEOUT_SECONDS_CONSTANT_NAME = "MIN_WORKER_TIMEOUT_SECONDS"
|
|
88
91
|
LARGE_PROMPT_CHARACTER_COUNT = 40000
|
|
89
92
|
WINDOWS_SAFE_ARGV_ELEMENT_CEILING = 8192
|
|
93
|
+
LOCK_WAIT_TIMEOUT_SECONDS = 1
|
|
90
94
|
EXPECTED_PRIMARY_AGENT_FOR_DEFAULT_ROLE = Path(
|
|
91
95
|
ALL_AGENT_FILENAMES_BY_ROLE[DEFAULT_ROLE][0]
|
|
92
96
|
).stem
|
|
@@ -1085,7 +1089,7 @@ def test_headless_chain_runner_lock_serializes_distinct_cwds(
|
|
|
1085
1089
|
claude_outcome=_claude_served(),
|
|
1086
1090
|
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
1087
1091
|
)
|
|
1088
|
-
all_errors: list[
|
|
1092
|
+
all_errors: list[Exception] = []
|
|
1089
1093
|
barrier = threading.Barrier(2)
|
|
1090
1094
|
|
|
1091
1095
|
def _run_with_working_directory(working_directory: Path) -> None:
|
|
@@ -1122,6 +1126,120 @@ def test_headless_chain_runner_lock_serializes_distinct_cwds(
|
|
|
1122
1126
|
assert chain_runner.chain_subprocess_runner is not None
|
|
1123
1127
|
|
|
1124
1128
|
|
|
1129
|
+
def test_shared_chain_runner_lock_serializes_cross_dispatcher_cwds(
|
|
1130
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1131
|
+
) -> None:
|
|
1132
|
+
first_working_directory = tmp_path / "project-a"
|
|
1133
|
+
second_working_directory = tmp_path / "project-b"
|
|
1134
|
+
first_working_directory.mkdir()
|
|
1135
|
+
second_working_directory.mkdir()
|
|
1136
|
+
observed_working_directories: list[Path] = []
|
|
1137
|
+
all_errors: list[BaseException] = []
|
|
1138
|
+
|
|
1139
|
+
class CoordinatedLock:
|
|
1140
|
+
def __init__(self) -> None:
|
|
1141
|
+
self._lock = threading.Lock()
|
|
1142
|
+
self.first_acquired = threading.Event()
|
|
1143
|
+
self.second_attempted = threading.Event()
|
|
1144
|
+
self.enter_count = 0
|
|
1145
|
+
|
|
1146
|
+
def __enter__(self) -> Self:
|
|
1147
|
+
self.enter_count += 1
|
|
1148
|
+
if self.enter_count == 1:
|
|
1149
|
+
self.first_acquired.set()
|
|
1150
|
+
elif self.enter_count == 2:
|
|
1151
|
+
self.second_attempted.set()
|
|
1152
|
+
self._lock.acquire()
|
|
1153
|
+
return self
|
|
1154
|
+
|
|
1155
|
+
def __exit__(self, *all_positionals: object) -> None:
|
|
1156
|
+
del all_positionals
|
|
1157
|
+
self._lock.release()
|
|
1158
|
+
|
|
1159
|
+
coordinated_lock = CoordinatedLock()
|
|
1160
|
+
|
|
1161
|
+
def tracking_subprocess_runner(
|
|
1162
|
+
all_invocation_tokens: Sequence[str],
|
|
1163
|
+
*all_positionals: object,
|
|
1164
|
+
**all_keywords: object,
|
|
1165
|
+
) -> subprocess.CompletedProcess[str]:
|
|
1166
|
+
del all_invocation_tokens, all_positionals
|
|
1167
|
+
observed_working_directories.append(Path(str(all_keywords["cwd"])))
|
|
1168
|
+
return subprocess.CompletedProcess(
|
|
1169
|
+
args=["claude"], returncode=0, stdout="{}", stderr=""
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
def run_review_runner(
|
|
1173
|
+
all_claude_arguments: list[str], *, timeout_seconds: int
|
|
1174
|
+
) -> ChainInvocationOutcome:
|
|
1175
|
+
del all_claude_arguments, timeout_seconds
|
|
1176
|
+
if not coordinated_lock.second_attempted.wait(
|
|
1177
|
+
timeout=LOCK_WAIT_TIMEOUT_SECONDS
|
|
1178
|
+
):
|
|
1179
|
+
all_errors.append(AssertionError("shared lock was never contended"))
|
|
1180
|
+
return _claude_served()
|
|
1181
|
+
chain_runner.chain_subprocess_runner(
|
|
1182
|
+
["claude"], capture_output=True, text=True, timeout=1, check=False
|
|
1183
|
+
)
|
|
1184
|
+
return _claude_served()
|
|
1185
|
+
|
|
1186
|
+
def run_spawn_runner(
|
|
1187
|
+
all_claude_arguments: list[str], *, timeout_seconds: int
|
|
1188
|
+
) -> ChainInvocationOutcome:
|
|
1189
|
+
del all_claude_arguments, timeout_seconds
|
|
1190
|
+
chain_runner.chain_subprocess_runner(
|
|
1191
|
+
["claude"], capture_output=True, text=True, timeout=1, check=False
|
|
1192
|
+
)
|
|
1193
|
+
return _claude_served()
|
|
1194
|
+
|
|
1195
|
+
monkeypatch.setattr(
|
|
1196
|
+
chain_runner, "chain_subprocess_runner", tracking_subprocess_runner
|
|
1197
|
+
)
|
|
1198
|
+
monkeypatch.setattr(
|
|
1199
|
+
chain_runner, "chain_subprocess_runner_lock", lambda: coordinated_lock,
|
|
1200
|
+
raising=False,
|
|
1201
|
+
)
|
|
1202
|
+
monkeypatch.setattr(invoker, "review_claude_runner", run_review_runner)
|
|
1203
|
+
monkeypatch.setattr(dispatcher, "spawn_claude_runner", run_spawn_runner)
|
|
1204
|
+
|
|
1205
|
+
def run_review() -> None:
|
|
1206
|
+
try:
|
|
1207
|
+
invoker._run_claude_with_empty_stdin(
|
|
1208
|
+
["-p", "review"],
|
|
1209
|
+
timeout_seconds=1,
|
|
1210
|
+
working_directory=first_working_directory,
|
|
1211
|
+
)
|
|
1212
|
+
except Exception as raised_error: # noqa: BLE001
|
|
1213
|
+
all_errors.append(raised_error)
|
|
1214
|
+
|
|
1215
|
+
def run_spawn() -> None:
|
|
1216
|
+
try:
|
|
1217
|
+
dispatcher._run_claude_with_headless_overrides(
|
|
1218
|
+
["-p", "spawn"],
|
|
1219
|
+
timeout_seconds=1,
|
|
1220
|
+
working_directory=second_working_directory,
|
|
1221
|
+
prompt_stdin=io.StringIO(FIXTURE_PROMPT_TEXT),
|
|
1222
|
+
)
|
|
1223
|
+
except Exception as raised_error: # noqa: BLE001
|
|
1224
|
+
all_errors.append(raised_error)
|
|
1225
|
+
|
|
1226
|
+
review_thread = threading.Thread(target=run_review)
|
|
1227
|
+
spawn_thread = threading.Thread(target=run_spawn)
|
|
1228
|
+
review_thread.start()
|
|
1229
|
+
coordinated_lock.first_acquired.wait(timeout=LOCK_WAIT_TIMEOUT_SECONDS)
|
|
1230
|
+
spawn_thread.start()
|
|
1231
|
+
review_thread.join(timeout=10)
|
|
1232
|
+
spawn_thread.join(timeout=10)
|
|
1233
|
+
|
|
1234
|
+
assert all_errors == []
|
|
1235
|
+
assert coordinated_lock.enter_count == 2
|
|
1236
|
+
assert observed_working_directories == [
|
|
1237
|
+
first_working_directory,
|
|
1238
|
+
second_working_directory,
|
|
1239
|
+
]
|
|
1240
|
+
assert chain_runner.chain_subprocess_runner is tracking_subprocess_runner
|
|
1241
|
+
|
|
1242
|
+
|
|
1125
1243
|
def test_usage_limit_fallover_delivers_full_prompt_to_each_binary(
|
|
1126
1244
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1127
1245
|
) -> None:
|
|
@@ -4,6 +4,16 @@ from pathlib import Path
|
|
|
4
4
|
from validate_instruction_pairs import validate_repository
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
def read_workflow(workflow_filename: str) -> str:
|
|
8
|
+
workflow_path = (
|
|
9
|
+
Path(__file__).resolve().parents[3]
|
|
10
|
+
/ ".github"
|
|
11
|
+
/ "workflows"
|
|
12
|
+
/ workflow_filename
|
|
13
|
+
)
|
|
14
|
+
return workflow_path.read_text(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
7
17
|
def initialize_repository(repository_root: Path) -> None:
|
|
8
18
|
subprocess.run(["git", "init", "--quiet"], cwd=repository_root, check=True)
|
|
9
19
|
subprocess.run(
|
|
@@ -88,3 +98,23 @@ def test_untracked_import_fails_tracking_check(tmp_path: Path) -> None:
|
|
|
88
98
|
all_errors = validate_repository(tmp_path)
|
|
89
99
|
|
|
90
100
|
assert any("Git mode 100644" in each_error for each_error in all_errors)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_pull_request_trigger_is_unconditional() -> None:
|
|
104
|
+
workflow_text = read_workflow("validate-instruction-pairs.yml")
|
|
105
|
+
|
|
106
|
+
assert " pull_request:\n push:\n" in workflow_text
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_instruction_pairs_status_context_stays_exact() -> None:
|
|
110
|
+
workflow_text = read_workflow("validate-instruction-pairs.yml")
|
|
111
|
+
reusable_workflow_text = read_workflow("instruction-pairs-reusable.yml")
|
|
112
|
+
|
|
113
|
+
assert (
|
|
114
|
+
" instruction-pairs:\n"
|
|
115
|
+
" uses: ./.github/workflows/instruction-pairs-reusable.yml\n"
|
|
116
|
+
" with:\n"
|
|
117
|
+
" validator-ref: ${{ github.event.pull_request.head.sha || github.sha }}\n"
|
|
118
|
+
in workflow_text
|
|
119
|
+
)
|
|
120
|
+
assert " instruction-pairs:\n name: instruction-pairs\n" in reusable_workflow_text
|
package/scripts/tests/AGENTS.md
CHANGED
|
@@ -10,6 +10,8 @@ pytest suite for the Python scripts and Pester suite for the PowerShell scripts
|
|
|
10
10
|
| `test_setup_project_paths_config.py` | Configuration constants used by `setup_project_paths.py` |
|
|
11
11
|
| `test_sweep_empty_dirs.py` | `sweep_empty_dirs.py` — age check, one-shot mode, and continuous-watch behavior |
|
|
12
12
|
| `test_sync_to_cursor.py` | `sync_to_cursor/` package — mapping, hashing, manifest, and path resolution |
|
|
13
|
+
| `test_rules.py` | Discovered Claude `rules/*.md` mappings to stem-named Cursor `.mdc` files |
|
|
14
|
+
| `test_engine.py` | `sync_to_cursor` engine `--claude-root` / `--cursor-root` layout flags |
|
|
13
15
|
| `test_grok_worker_constants.py` | `grok_worker_constants.py` — the accepted batch worker-key set stays in step with the worker key constants, and the unknown-key message names both its placeholders |
|
|
14
16
|
|
|
15
17
|
## PowerShell test files
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for sync_to_cursor engine CLI roots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
11
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
13
|
+
|
|
14
|
+
from sync_to_cursor.engine import run as run_sync_to_cursor
|
|
15
|
+
|
|
16
|
+
_CODE_STANDARDS_SECTION_ORDER = (
|
|
17
|
+
"COMMENT PRESERVATION",
|
|
18
|
+
"CORE PRINCIPLES",
|
|
19
|
+
"⚡ HOOK-ENFORCED RULES",
|
|
20
|
+
"3. REUSE CONSTANTS / 4. CONFIG LOCATIONS",
|
|
21
|
+
"5. NO ABBREVIATIONS",
|
|
22
|
+
"6. COMPLETE TYPE HINTS",
|
|
23
|
+
"9. SELF-CONTAINED COMPONENTS",
|
|
24
|
+
)
|
|
25
|
+
_TEST_QUALITY_SECTION_ORDER = (
|
|
26
|
+
"Delete Useless Tests",
|
|
27
|
+
"Test Dependencies MUST FAIL",
|
|
28
|
+
"Core Testing Principles",
|
|
29
|
+
"React Testing Patterns",
|
|
30
|
+
"Test File Organization",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _write_minimal_curated_rules(rules_directory: Path) -> None:
|
|
35
|
+
rules_directory.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
(rules_directory / "code-standards.md").write_text(
|
|
37
|
+
"# Code standards stub\n", encoding="utf-8"
|
|
38
|
+
)
|
|
39
|
+
(rules_directory / "tasklings-preferences.md").write_text(
|
|
40
|
+
'---\npaths:\n - "Y:/x/**"\n---\n\n# Tasklings\n',
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
(rules_directory / "bdd.md").write_text("# BDD\n", encoding="utf-8")
|
|
44
|
+
(rules_directory / "testing.md").write_text(
|
|
45
|
+
'---\npaths:\n - "**/test_*.py"\n---\n\n# Testing\n',
|
|
46
|
+
encoding="utf-8",
|
|
47
|
+
)
|
|
48
|
+
(rules_directory / "research-mode.md").write_text("# RM\n", encoding="utf-8")
|
|
49
|
+
(rules_directory / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
|
|
50
|
+
(rules_directory / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _write_minimal_docs(docs_directory: Path) -> None:
|
|
54
|
+
docs_directory.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
(docs_directory / "CODE_RULES.md").write_text(
|
|
56
|
+
"\n\n".join(f"## {title}\n\nalpha" for title in _CODE_STANDARDS_SECTION_ORDER)
|
|
57
|
+
+ "\n",
|
|
58
|
+
encoding="utf-8",
|
|
59
|
+
)
|
|
60
|
+
(docs_directory / "TEST_QUALITY.md").write_text(
|
|
61
|
+
"\n\n".join(f"## {title}\n\nbeta" for title in _TEST_QUALITY_SECTION_ORDER)
|
|
62
|
+
+ "\n",
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_explicit_roots_write_stem_named_mdc(
|
|
68
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
69
|
+
) -> None:
|
|
70
|
+
claude = tmp_path / "claude-home"
|
|
71
|
+
cursor = tmp_path / "cursor-home"
|
|
72
|
+
_write_minimal_curated_rules(claude / "rules")
|
|
73
|
+
_write_minimal_docs(claude / "docs")
|
|
74
|
+
(claude / "rules" / "plain-language.md").write_text(
|
|
75
|
+
"# Plain language\n\nUse short sentences.\n",
|
|
76
|
+
encoding="utf-8",
|
|
77
|
+
)
|
|
78
|
+
(claude / "rules" / "CLAUDE.md").write_text("# Inventory\n", encoding="utf-8")
|
|
79
|
+
monkeypatch.delenv("LLM_SETTINGS_ROOT", raising=False)
|
|
80
|
+
assert cursor.exists() is False
|
|
81
|
+
assert (
|
|
82
|
+
run_sync_to_cursor(
|
|
83
|
+
[
|
|
84
|
+
"--force",
|
|
85
|
+
"--claude-root",
|
|
86
|
+
str(claude),
|
|
87
|
+
"--cursor-root",
|
|
88
|
+
str(cursor),
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
== 0
|
|
92
|
+
)
|
|
93
|
+
generated = (cursor / "rules" / "plain-language.mdc").read_text(encoding="utf-8")
|
|
94
|
+
assert 'description: "Plain language"' in generated
|
|
95
|
+
assert "alwaysApply: true" in generated
|
|
96
|
+
assert "Use short sentences." in generated
|
|
97
|
+
assert not (cursor / "rules" / "CLAUDE.mdc").is_file()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_explicit_roots_require_both_flags(tmp_path: Path) -> None:
|
|
101
|
+
with pytest.raises(SystemExit):
|
|
102
|
+
run_sync_to_cursor(["--force", "--claude-root", str(tmp_path)])
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tests for discovered Claude-to-Cursor rule mappings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
9
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
11
|
+
|
|
12
|
+
from sync_to_cursor.rules import build_mappings
|
|
13
|
+
|
|
14
|
+
_PACKAGE_ROOT = _SCRIPTS_DIR.parent
|
|
15
|
+
_SKIPPED_RULE_FILE_NAMES = frozenset({"CLAUDE.md", "AGENTS.md"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _write_minimal_curated_rules(rules_directory: Path) -> None:
|
|
19
|
+
rules_directory.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
(rules_directory / "code-standards.md").write_text(
|
|
21
|
+
"# Code standards stub\n", encoding="utf-8"
|
|
22
|
+
)
|
|
23
|
+
(rules_directory / "tasklings-preferences.md").write_text(
|
|
24
|
+
'---\npaths:\n - "Y:/x/**"\n---\n\n# Tasklings\n',
|
|
25
|
+
encoding="utf-8",
|
|
26
|
+
)
|
|
27
|
+
(rules_directory / "bdd.md").write_text("# BDD\n", encoding="utf-8")
|
|
28
|
+
(rules_directory / "testing.md").write_text(
|
|
29
|
+
'---\npaths:\n - "**/test_*.py"\n---\n\n# Testing\n',
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
(rules_directory / "research-mode.md").write_text("# RM\n", encoding="utf-8")
|
|
33
|
+
(rules_directory / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
|
|
34
|
+
(rules_directory / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_build_mappings_emits_stem_mdc_for_remaining_claude_rules(
|
|
38
|
+
tmp_path: Path,
|
|
39
|
+
) -> None:
|
|
40
|
+
claude = tmp_path / ".claude"
|
|
41
|
+
_write_minimal_curated_rules(claude / "rules")
|
|
42
|
+
(claude / "docs").mkdir(parents=True, exist_ok=True)
|
|
43
|
+
(claude / "rules" / "plain-language.md").write_text(
|
|
44
|
+
"# Plain language\n\nBe brief.\n",
|
|
45
|
+
encoding="utf-8",
|
|
46
|
+
)
|
|
47
|
+
(claude / "rules" / "CLAUDE.md").write_text(
|
|
48
|
+
"# Package inventory\n", encoding="utf-8"
|
|
49
|
+
)
|
|
50
|
+
(claude / "rules" / "AGENTS.md").write_text("# Agent inventory\n", encoding="utf-8")
|
|
51
|
+
mappings = build_mappings(claude)
|
|
52
|
+
output_by_key = {each_mapping.key: each_mapping for each_mapping in mappings}
|
|
53
|
+
discovered = output_by_key["plain-language"]
|
|
54
|
+
assert discovered.output_name == "plain-language.mdc"
|
|
55
|
+
assert discovered.always_apply is True
|
|
56
|
+
assert discovered.description == "Plain language"
|
|
57
|
+
assert "CLAUDE.md" not in {
|
|
58
|
+
each_source.name
|
|
59
|
+
for each_mapping in mappings
|
|
60
|
+
for each_source in each_mapping.sources
|
|
61
|
+
}
|
|
62
|
+
assert "AGENTS.md" not in {
|
|
63
|
+
each_source.name
|
|
64
|
+
for each_mapping in mappings
|
|
65
|
+
for each_source in each_mapping.sources
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_every_shipped_claude_rule_maps_to_an_mdc() -> None:
|
|
70
|
+
mappings = build_mappings(_PACKAGE_ROOT)
|
|
71
|
+
output_name_by_rule_file = {}
|
|
72
|
+
for each_mapping in mappings:
|
|
73
|
+
for each_source in each_mapping.sources:
|
|
74
|
+
if each_source.parent.name == "rules" and each_source.suffix == ".md":
|
|
75
|
+
output_name_by_rule_file[each_source.name] = each_mapping.output_name
|
|
76
|
+
for each_rule_file in sorted((_PACKAGE_ROOT / "rules").glob("*.md")):
|
|
77
|
+
if each_rule_file.name in _SKIPPED_RULE_FILE_NAMES:
|
|
78
|
+
continue
|
|
79
|
+
assert each_rule_file.name in output_name_by_rule_file, each_rule_file.name
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": 1,
|
|
3
|
-
"supportedActivationInterface": "claude-config-dir-mcp-json",
|
|
4
|
-
"mcpConfigFileName": "mcp.json",
|
|
5
|
-
"bundles": {
|
|
6
|
-
"lean": {
|
|
7
|
-
"id": "lean",
|
|
8
|
-
"allServerNames": ["filesystem-readonly"]
|
|
9
|
-
},
|
|
10
|
-
"full": {
|
|
11
|
-
"id": "full",
|
|
12
|
-
"allServerNames": ["filesystem-readonly", "github-readonly"]
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"serverByName": {
|
|
16
|
-
"filesystem-readonly": {
|
|
17
|
-
"command": "npx",
|
|
18
|
-
"args": ["-y", "@modelcontextprotocol/server-filesystem", "${PROFILE_ROOT}"]
|
|
19
|
-
},
|
|
20
|
-
"github-readonly": {
|
|
21
|
-
"command": "npx",
|
|
22
|
-
"args": ["-y", "@modelcontextprotocol/server-github"]
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIRECTORY_PATH = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {string} fileName
|
|
9
|
-
* @returns {unknown}
|
|
10
|
-
*/
|
|
11
|
-
function readJsonConfigFile(fileName) {
|
|
12
|
-
const absolutePath = join(CONFIG_DIRECTORY_PATH, fileName);
|
|
13
|
-
return JSON.parse(readFileSync(absolutePath, 'utf8'));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const PROFILES_MANIFEST_FILE_NAME = 'profiles.manifest.json';
|
|
17
|
-
export const SHARED_ALLOWLIST_FILE_NAME = 'shared-allowlist.json';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Sole authoritative profile-root environment variable for CLI isolation.
|
|
21
|
-
* CLAUDE_HOME is never honored as a profile root by this contract.
|
|
22
|
-
*/
|
|
23
|
-
export const CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE = 'CLAUDE_CONFIG_DIR';
|
|
24
|
-
export const CLAUDE_HOME_ENVIRONMENT_VARIABLE = 'CLAUDE_HOME';
|
|
25
|
-
export const CLAUDE_CODE_TMPDIR_ENVIRONMENT_VARIABLE = 'CLAUDE_CODE_TMPDIR';
|
|
26
|
-
export const CLAUDE_CODE_PLUGIN_SEED_DIR_ENVIRONMENT_VARIABLE = 'CLAUDE_CODE_PLUGIN_SEED_DIR';
|
|
27
|
-
|
|
28
|
-
export const PROFILES_ROOT_ENVIRONMENT_VARIABLE = 'LLM_SETTINGS_PROFILES_ROOT';
|
|
29
|
-
export const SHARED_SOURCE_ROOT_ENVIRONMENT_VARIABLE = 'LLM_SETTINGS_SHARED_SOURCE_ROOT';
|
|
30
|
-
export const PLUGIN_SEED_ROOT_ENVIRONMENT_VARIABLE = 'LLM_SETTINGS_PLUGIN_SEED_ROOT';
|
|
31
|
-
|
|
32
|
-
export const DEFAULT_PROFILES_ROOT_DIRECTORY_NAME = '.claude-profiles';
|
|
33
|
-
export const DEFAULT_SHARED_SOURCE_DIRECTORY_NAME = 'shared-source';
|
|
34
|
-
export const DEFAULT_PLUGIN_SEED_DIRECTORY_NAME = 'plugin-seed';
|
|
35
|
-
|
|
36
|
-
export const MIGRATION_MODE_CLEAN_LOCAL_RUNTIME = 'clean-local-runtime';
|
|
37
|
-
export const MIGRATION_MODE_MATERIALIZE_FROM_LEGACY = 'materialize-from-legacy';
|
|
38
|
-
|
|
39
|
-
export const MCP_BUNDLE_LEAN = 'lean';
|
|
40
|
-
export const MCP_BUNDLE_FULL = 'full';
|
|
41
|
-
|
|
42
|
-
export const LAUNCHER_SCHEMA_VERSION = 1;
|
|
43
|
-
export const PROFILE_ISOLATION_CONTRACT_OWNER = 'profile-isolation-contract';
|
|
44
|
-
export const PACKAGE_FILES_WHITELIST_SCRIPTS_ENTRY = 'scripts/';
|
|
45
|
-
export const LIVE_DEPLOYMENT_RESERVED_FOR = 'L1';
|
|
46
|
-
export const INSTALL_DESTINATION_ROOT_RELATIVE_PATH = 'scripts/profile-isolation-launchers';
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @returns {Record<string, unknown>}
|
|
50
|
-
*/
|
|
51
|
-
export function loadProfilesManifestDocument() {
|
|
52
|
-
return /** @type {Record<string, unknown>} */ (readJsonConfigFile(PROFILES_MANIFEST_FILE_NAME));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @returns {Record<string, unknown>}
|
|
57
|
-
*/
|
|
58
|
-
export function loadSharedAllowlistDocument() {
|
|
59
|
-
return /** @type {Record<string, unknown>} */ (readJsonConfigFile(SHARED_ALLOWLIST_FILE_NAME));
|
|
60
|
-
}
|