claude-dev-env 8.12.0 → 8.14.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/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills/team-advisor/SKILL.md +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/advisor-protocol.md +19 -19
- package/_shared/advisor/reference/advisor-block.md +1 -1
- package/_shared/advisor/reference/astra-rung.md +11 -11
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +2 -2
- package/_shared/advisor/reference/spawn-walk-log.md +4 -4
- package/_shared/advisor/reference/third-party-bind.md +7 -7
- package/_shared/advisor/reference/warm-up.md +3 -3
- package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
- package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +2 -2
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -3
- package/_shared/advisor/scripts/model_tier_run_validator.py +8 -9
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +91 -91
- package/_shared/pr-loop/scripts/check_convergence.py +54 -49
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
- package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
- package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
- package/bin/git_hooks_installer.test.mjs +1 -1
- package/bin/install-constants.mjs +1 -6
- package/bin/install.cursor-rules.test.mjs +24 -51
- package/bin/install.mjs +16 -70
- package/docs/codex-accounts.md +79 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
- package/hooks/blocking/test_pii_scanner.py +1 -1
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
- package/hooks/git-hooks/conftest.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +1 -1
- package/hooks/git-hooks/test_pre_commit.py +1 -1
- package/hooks/git-hooks/test_pre_push.py +2 -2
- package/hooks/git-hooks/test_verification_start.py +1 -1
- package/hooks/git-hooks/verification_notice.py +2 -2
- package/hooks/git-hooks/verification_notice_context.py +21 -2
- package/hooks/git-hooks/verification_start.py +2 -3
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +4 -3
- package/scripts/claude_account_profile.py +21 -9
- package/scripts/codex_account_choice.py +396 -0
- package/scripts/codex_account_meters.py +372 -0
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
- package/scripts/private_term_scan.py +6 -2
- package/scripts/repository_checks/config/constants.py +1 -0
- package/scripts/repository_checks/tracked_private_terms.py +7 -0
- package/scripts/test_claude_account_profile.py +12 -0
- package/scripts/test_codex_account_choice.py +276 -0
- package/scripts/test_codex_account_meters.py +205 -0
- package/scripts/tests/test_automatic_advisory.py +3 -3
- package/scripts/tests/test_automatic_advisory_polling.py +1 -1
- package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
- package/scripts/tests/test_closed_pr_label.py +1 -1
- package/scripts/tests/test_private_term_scan.py +28 -0
- package/scripts/tests/test_review_closure.py +2 -2
- package/scripts/tests/test_review_closure_model.py +2 -2
- package/scripts/tests/test_rules.py +2 -83
- package/scripts/tests/test_tracked_private_terms.py +15 -0
- package/scripts/tests/test_verification_notice.py +29 -13
- package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
- package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
- package/scripts/sync_to_cursor/AGENTS.md +0 -1
- package/scripts/sync_to_cursor/CLAUDE.md +0 -1
- package/scripts/sync_to_cursor/__init__.py +0 -13
- package/scripts/sync_to_cursor/canonical_docs.py +0 -66
- package/scripts/sync_to_cursor/config/__init__.py +0 -8
- package/scripts/sync_to_cursor/engine.py +0 -219
- package/scripts/sync_to_cursor/hashing.py +0 -7
- package/scripts/sync_to_cursor/paths.py +0 -18
- package/scripts/sync_to_cursor/rules.py +0 -494
- package/scripts/sync_to_cursor.py +0 -22
- package/scripts/tests/test_engine.py +0 -101
- package/scripts/tests/test_sync_to_cursor.py +0 -315
|
@@ -45,7 +45,7 @@ FIXTURE_KEY_UNRESOLVED_BOT_THREADS_PASSED: str = "unresolved_bot_threads_passed"
|
|
|
45
45
|
FIXTURE_KEY_UNRESOLVED_BOT_THREADS_DETAIL: str = "unresolved_bot_threads_detail"
|
|
46
46
|
FIXTURE_KEY_PENDING_REVIEWS_PASSED: str = "pending_reviews_passed"
|
|
47
47
|
FIXTURE_KEY_PENDING_REVIEWS_DETAIL: str = "pending_reviews_detail"
|
|
48
|
-
|
|
48
|
+
FIXTURE_KEY_CODEX_TIER: str = "codex_tier"
|
|
49
49
|
FIXTURE_KEY_CODEX_CLEAN_AT: str = "codex_clean_at"
|
|
50
50
|
FIXTURE_DEFAULT_THREADS_DETAIL: str = "0 unresolved"
|
|
51
51
|
FIXTURE_DEFAULT_PENDING_DETAIL: str = "none pending"
|
|
@@ -68,3 +68,12 @@ PR_CONVERGE_STATE_FILENAME: str = "pr-converge-state.json"
|
|
|
68
68
|
CODEX_CLEAN_AT_STATE_KEY: str = "codex_clean_at"
|
|
69
69
|
CODEX_DOWN_STATE_KEY: str = "codex_down"
|
|
70
70
|
MINIMUM_ABBREVIATED_SHA_LENGTH: int = 7
|
|
71
|
+
SHARED_PACKAGE_ROOT_PARENT_INDEX: int = 3
|
|
72
|
+
ALL_CODEX_ACCOUNT_PICKER_RELATIVE_PARTS: tuple[str, ...] = (
|
|
73
|
+
"scripts",
|
|
74
|
+
"codex_account_choice.py",
|
|
75
|
+
)
|
|
76
|
+
CODEX_ACCOUNT_PICKER_CHOOSE_COMMAND: str = "choose"
|
|
77
|
+
CODEX_ACCOUNT_PICK_TIMEOUT_SECONDS: float = 150.0
|
|
78
|
+
CODEX_TIER_KEY: str = "tier"
|
|
79
|
+
CODEX_TIER_NORMAL: str = "normal"
|
|
@@ -44,14 +44,9 @@ from process_tree_kill import ( # noqa: E402
|
|
|
44
44
|
terminate_process_tree,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
|
|
48
|
-
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
49
|
-
WINDOWS_COMMAND_SHELL,
|
|
50
|
-
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
51
|
-
WINDOWS_OS_NAME,
|
|
52
|
-
)
|
|
53
47
|
from codex_review_scripts_constants.run_constants import ( # noqa: E402
|
|
54
48
|
ALL_SHAPE_PROBE_REQUIRED_FLAGS,
|
|
49
|
+
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
55
50
|
BASE_TARGET_FLAG,
|
|
56
51
|
CAPTURE_STREAMS_KEYWORD,
|
|
57
52
|
CHECK_KEYWORD,
|
|
@@ -88,6 +83,9 @@ from codex_review_scripts_constants.run_constants import ( # noqa: E402
|
|
|
88
83
|
UTF8_ENCODING,
|
|
89
84
|
VERSION_FLAG,
|
|
90
85
|
VERSION_PROBE_PATTERN,
|
|
86
|
+
WINDOWS_COMMAND_SHELL,
|
|
87
|
+
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
88
|
+
WINDOWS_OS_NAME,
|
|
91
89
|
)
|
|
92
90
|
|
|
93
91
|
from subprocess_window_access import hidden_window_creation_flags
|
|
@@ -107,14 +107,14 @@ def _raise_if_called(gate_label: str) -> Callable[..., tuple[bool, str]]:
|
|
|
107
107
|
return _stub
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
def
|
|
110
|
+
def _unread_codex_tier() -> str | None:
|
|
111
111
|
return None
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
def _patch_all_gates_passing(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
115
115
|
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _passing_head_sha)
|
|
116
116
|
monkeypatch.setattr(
|
|
117
|
-
check_convergence, "
|
|
117
|
+
check_convergence, "_read_codex_tier", _unread_codex_tier
|
|
118
118
|
)
|
|
119
119
|
for each_gate_name in _ALL_LEAF_GATE_NAMES:
|
|
120
120
|
monkeypatch.setattr(check_convergence, each_gate_name, _passing_gate)
|
|
@@ -135,7 +135,7 @@ def should_pass_when_clean_bugteam_review_present_on_current_head(
|
|
|
135
135
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
136
136
|
)
|
|
137
137
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
138
|
-
owner="
|
|
138
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
139
139
|
)
|
|
140
140
|
assert passed is True
|
|
141
141
|
assert "clean bugteam audit" in detail
|
|
@@ -157,7 +157,7 @@ def should_fail_when_dirty_bugteam_review_present_on_current_head(
|
|
|
157
157
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
158
158
|
)
|
|
159
159
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
160
|
-
owner="
|
|
160
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
161
161
|
)
|
|
162
162
|
assert passed is False
|
|
163
163
|
assert "dirty bugteam audit" in detail
|
|
@@ -185,7 +185,7 @@ def should_fail_when_no_bugteam_review_present_on_current_head(
|
|
|
185
185
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
186
186
|
)
|
|
187
187
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
188
|
-
owner="
|
|
188
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
189
189
|
)
|
|
190
190
|
assert passed is False
|
|
191
191
|
assert "no bugteam review found" in detail
|
|
@@ -204,7 +204,7 @@ def should_fail_with_shape_detail_when_gh_returns_non_list_payload(
|
|
|
204
204
|
check_convergence, "_gh_api_paginated", stub_gh_api_paginated_returning_object
|
|
205
205
|
)
|
|
206
206
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
207
|
-
owner="
|
|
207
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
208
208
|
)
|
|
209
209
|
assert passed is False
|
|
210
210
|
assert "unexpected gh api response shape" in detail
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Fixture-driven tests for the conditional Codex convergence gate.
|
|
2
2
|
|
|
3
|
-
Covers: required-and-clean, required-and-dirty, skipped-by-
|
|
4
|
-
skipped-by-token, skipped-by-down
|
|
5
|
-
|
|
3
|
+
Covers: required-and-clean, required-and-dirty, skipped-by-tier,
|
|
4
|
+
skipped-by-token, skipped-by-down, and the read of the Codex account picker's
|
|
5
|
+
``choose`` answer. The required tier comes from ``CODEX_TIER_NORMAL``.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -16,10 +16,8 @@ from types import ModuleType
|
|
|
16
16
|
import pytest
|
|
17
17
|
|
|
18
18
|
import _pr_converge_path_setup # noqa: F401
|
|
19
|
-
from codex_review_scripts_constants.codex_usage_probe_constants import (
|
|
20
|
-
WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
|
|
21
|
-
)
|
|
22
19
|
from pr_converge_scripts_constants.convergence_gate_constants import (
|
|
20
|
+
CODEX_TIER_NORMAL,
|
|
23
21
|
MINIMUM_ABBREVIATED_SHA_LENGTH,
|
|
24
22
|
)
|
|
25
23
|
|
|
@@ -33,8 +31,7 @@ CLEAN_BODY = (
|
|
|
33
31
|
)
|
|
34
32
|
HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
35
33
|
OTHER_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
36
|
-
|
|
37
|
-
PERCENT_AT_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT)
|
|
34
|
+
ALL_TIERS_WITHOUT_ROOM_FOR_REVIEW = ("luna", "wait")
|
|
38
35
|
|
|
39
36
|
|
|
40
37
|
def _load_module() -> ModuleType:
|
|
@@ -64,7 +61,7 @@ check_convergence = _load_module()
|
|
|
64
61
|
def _write_fixture(
|
|
65
62
|
tmp_path: Path,
|
|
66
63
|
*,
|
|
67
|
-
|
|
64
|
+
codex_tier: str | None,
|
|
68
65
|
codex_clean_at: str | None,
|
|
69
66
|
filename: str = "codex-gate.json",
|
|
70
67
|
) -> Path:
|
|
@@ -83,7 +80,7 @@ def _write_fixture(
|
|
|
83
80
|
"unresolved_bot_threads_detail": "0 unresolved",
|
|
84
81
|
"pending_reviews_passed": True,
|
|
85
82
|
"pending_reviews_detail": "none pending",
|
|
86
|
-
"
|
|
83
|
+
"codex_tier": codex_tier,
|
|
87
84
|
"codex_clean_at": codex_clean_at,
|
|
88
85
|
}
|
|
89
86
|
fixture_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
@@ -95,7 +92,7 @@ def should_pass_when_codex_required_and_clean(
|
|
|
95
92
|
) -> None:
|
|
96
93
|
fixture_path = _write_fixture(
|
|
97
94
|
tmp_path,
|
|
98
|
-
|
|
95
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
99
96
|
codex_clean_at=HEAD_SHA,
|
|
100
97
|
filename="required-clean.json",
|
|
101
98
|
)
|
|
@@ -120,20 +117,18 @@ def should_pass_when_codex_required_and_clean(
|
|
|
120
117
|
assert "All pre-conditions met" in captured
|
|
121
118
|
|
|
122
119
|
|
|
123
|
-
def
|
|
120
|
+
def should_pass_without_running_the_picker_when_the_clean_stamp_is_on_head(
|
|
124
121
|
monkeypatch: pytest.MonkeyPatch,
|
|
125
122
|
) -> None:
|
|
126
|
-
def
|
|
123
|
+
def _picker_that_must_not_run() -> str | None:
|
|
127
124
|
raise AssertionError(
|
|
128
|
-
"the
|
|
125
|
+
"the Codex account picker must not run when codex_clean_at is on HEAD"
|
|
129
126
|
)
|
|
130
127
|
|
|
131
|
-
monkeypatch.setattr(
|
|
132
|
-
check_convergence, "_probe_codex_percent_left", _probe_that_must_not_run
|
|
133
|
-
)
|
|
128
|
+
monkeypatch.setattr(check_convergence, "_read_codex_tier", _picker_that_must_not_run)
|
|
134
129
|
|
|
135
130
|
is_passed, detail = check_convergence._evaluate_codex_clean(
|
|
136
|
-
|
|
131
|
+
read_codex_tier=check_convergence._read_codex_tier,
|
|
137
132
|
codex_clean_at=HEAD_SHA,
|
|
138
133
|
head_sha=HEAD_SHA,
|
|
139
134
|
)
|
|
@@ -151,7 +146,7 @@ def should_fail_when_codex_clean_stamp_is_shorter_than_an_abbreviated_sha(
|
|
|
151
146
|
) -> None:
|
|
152
147
|
fixture_path = _write_fixture(
|
|
153
148
|
tmp_path,
|
|
154
|
-
|
|
149
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
155
150
|
codex_clean_at=HEAD_SHA[:1],
|
|
156
151
|
filename="required-short-stamp.json",
|
|
157
152
|
)
|
|
@@ -179,7 +174,7 @@ def should_pass_when_codex_clean_stamp_is_an_abbreviated_head_sha(
|
|
|
179
174
|
) -> None:
|
|
180
175
|
fixture_path = _write_fixture(
|
|
181
176
|
tmp_path,
|
|
182
|
-
|
|
177
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
183
178
|
codex_clean_at=HEAD_SHA[:MINIMUM_ABBREVIATED_SHA_LENGTH],
|
|
184
179
|
filename="required-abbreviated-stamp.json",
|
|
185
180
|
)
|
|
@@ -207,7 +202,7 @@ def should_fail_when_codex_required_and_dirty(
|
|
|
207
202
|
) -> None:
|
|
208
203
|
fixture_path = _write_fixture(
|
|
209
204
|
tmp_path,
|
|
210
|
-
|
|
205
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
211
206
|
codex_clean_at=OTHER_SHA,
|
|
212
207
|
filename="required-dirty.json",
|
|
213
208
|
)
|
|
@@ -232,14 +227,15 @@ def should_fail_when_codex_required_and_dirty(
|
|
|
232
227
|
assert "do not mark ready" in captured
|
|
233
228
|
|
|
234
229
|
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
@pytest.mark.parametrize("tier_without_room", ALL_TIERS_WITHOUT_ROOM_FOR_REVIEW)
|
|
231
|
+
def should_skip_when_the_picker_names_no_account_with_room_for_review(
|
|
232
|
+
tier_without_room: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
237
233
|
) -> None:
|
|
238
234
|
fixture_path = _write_fixture(
|
|
239
235
|
tmp_path,
|
|
240
|
-
|
|
236
|
+
codex_tier=tier_without_room,
|
|
241
237
|
codex_clean_at=None,
|
|
242
|
-
filename="skipped-
|
|
238
|
+
filename="skipped-tier.json",
|
|
243
239
|
)
|
|
244
240
|
exit_code = check_convergence.main(
|
|
245
241
|
[
|
|
@@ -261,12 +257,12 @@ def should_skip_when_usage_at_or_below_threshold(
|
|
|
261
257
|
assert "skipped (codex review not required)" in captured
|
|
262
258
|
|
|
263
259
|
|
|
264
|
-
def
|
|
260
|
+
def should_skip_when_codex_tier_is_null(
|
|
265
261
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
266
262
|
) -> None:
|
|
267
263
|
fixture_path = _write_fixture(
|
|
268
264
|
tmp_path,
|
|
269
|
-
|
|
265
|
+
codex_tier=None,
|
|
270
266
|
codex_clean_at=None,
|
|
271
267
|
filename="skipped-null.json",
|
|
272
268
|
)
|
|
@@ -297,7 +293,7 @@ def should_bypass_when_codex_token_disables_reviewer(
|
|
|
297
293
|
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
|
|
298
294
|
fixture_path = _write_fixture(
|
|
299
295
|
tmp_path,
|
|
300
|
-
|
|
296
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
301
297
|
codex_clean_at=None,
|
|
302
298
|
filename="skipped-token.json",
|
|
303
299
|
)
|
|
@@ -326,7 +322,7 @@ def should_bypass_when_codex_down_flag_set(
|
|
|
326
322
|
) -> None:
|
|
327
323
|
fixture_path = _write_fixture(
|
|
328
324
|
tmp_path,
|
|
329
|
-
|
|
325
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
330
326
|
codex_clean_at=None,
|
|
331
327
|
filename="skipped-down.json",
|
|
332
328
|
)
|
|
@@ -433,7 +429,7 @@ def should_bypass_when_job_state_codex_down_is_true(
|
|
|
433
429
|
)
|
|
434
430
|
fixture_path = _write_fixture(
|
|
435
431
|
tmp_path,
|
|
436
|
-
|
|
432
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
437
433
|
codex_clean_at=None,
|
|
438
434
|
filename="job-state-codex-down.json",
|
|
439
435
|
)
|
|
@@ -470,7 +466,7 @@ def should_fail_when_job_state_lacks_codex_down_and_clean_stamp(
|
|
|
470
466
|
)
|
|
471
467
|
fixture_path = _write_fixture(
|
|
472
468
|
tmp_path,
|
|
473
|
-
|
|
469
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
474
470
|
codex_clean_at=None,
|
|
475
471
|
filename="job-state-no-down.json",
|
|
476
472
|
)
|
|
@@ -515,3 +511,49 @@ def should_accept_codex_clean_at_flag_in_parsed_arguments() -> None:
|
|
|
515
511
|
]
|
|
516
512
|
)
|
|
517
513
|
assert arguments.codex_clean_at == HEAD_SHA
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _install_fake_picker(
|
|
517
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, picker_source: str
|
|
518
|
+
) -> None:
|
|
519
|
+
picker_path = tmp_path / "codex_account_choice.py"
|
|
520
|
+
picker_path.write_text(picker_source, encoding="utf-8")
|
|
521
|
+
monkeypatch.setattr(check_convergence, "_codex_account_picker_path", lambda: picker_path)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def should_read_the_tier_the_picker_chooses(
|
|
525
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
526
|
+
) -> None:
|
|
527
|
+
_install_fake_picker(
|
|
528
|
+
tmp_path,
|
|
529
|
+
monkeypatch,
|
|
530
|
+
"import json, sys\n"
|
|
531
|
+
"assert sys.argv[1:] == ['choose']\n"
|
|
532
|
+
"print(json.dumps({'tier': 'normal', 'account': 'codex-2'}))\n",
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
assert check_convergence._read_codex_tier() == CODEX_TIER_NORMAL
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@pytest.mark.parametrize(
|
|
539
|
+
"picker_source",
|
|
540
|
+
[
|
|
541
|
+
"import sys\nprint('{\"tier\": \"normal\"}')\nsys.exit(1)\n",
|
|
542
|
+
"print('not json')\n",
|
|
543
|
+
"print('[\"normal\"]')\n",
|
|
544
|
+
"print('{\"tier\": 1}')\n",
|
|
545
|
+
],
|
|
546
|
+
)
|
|
547
|
+
def should_read_no_tier_when_the_picker_fails_or_answers_out_of_shape(
|
|
548
|
+
picker_source: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
549
|
+
) -> None:
|
|
550
|
+
_install_fake_picker(tmp_path, monkeypatch, picker_source)
|
|
551
|
+
|
|
552
|
+
assert check_convergence._read_codex_tier() is None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def should_locate_the_packaged_codex_account_picker() -> None:
|
|
556
|
+
picker_path = check_convergence._codex_account_picker_path()
|
|
557
|
+
|
|
558
|
+
assert picker_path.name == "codex_account_choice.py"
|
|
559
|
+
assert picker_path.is_file()
|
|
@@ -118,14 +118,14 @@ def _refuse_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
|
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
def
|
|
121
|
+
def _unread_codex_tier() -> str | None:
|
|
122
122
|
return None
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def _patch_gates_clean(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
126
126
|
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _clean_head)
|
|
127
127
|
monkeypatch.setattr(
|
|
128
|
-
check_convergence, "
|
|
128
|
+
check_convergence, "_read_codex_tier", _unread_codex_tier
|
|
129
129
|
)
|
|
130
130
|
for each_gate_name in _ALL_LEAF_GATE_NAMES:
|
|
131
131
|
monkeypatch.setattr(check_convergence, each_gate_name, _clean_gate)
|
|
@@ -127,10 +127,10 @@ def test_path_contains_glob_metacharacters_accepts_windows_paths_with_parens() -
|
|
|
127
127
|
`(`, `)`, and `,` are not glob metacharacters in Claude Code's permission
|
|
128
128
|
rule matching. Including them in the metacharacter set causes
|
|
129
129
|
get_current_project_path to raise ValueError for any user whose home
|
|
130
|
-
directory contains parentheses (e.g. `C:/Users/
|
|
130
|
+
directory contains parentheses (e.g. `C:/Users/Example (Admin)/...`).
|
|
131
131
|
"""
|
|
132
132
|
assert common.path_contains_glob_metacharacters("C:/Program Files (x86)/app") is False
|
|
133
|
-
assert common.path_contains_glob_metacharacters("C:/Users/
|
|
133
|
+
assert common.path_contains_glob_metacharacters("C:/Users/Example (Admin)/project") is False
|
|
134
134
|
assert common.path_contains_glob_metacharacters("C:/Projects/a,b/file.py") is False
|
|
135
135
|
|
|
136
136
|
|
|
@@ -227,7 +227,7 @@ test('generated advisory shims keep temporary Git commit and push available afte
|
|
|
227
227
|
const { temporaryRoot, gitHooksDirectory } = makeTemporaryGitHooksDirectory();
|
|
228
228
|
const repositoryRoot = join(temporaryRoot, 'repository');
|
|
229
229
|
const bareRepositoryRoot = join(temporaryRoot, 'remote.git');
|
|
230
|
-
const targetRemote = 'https://github.com/
|
|
230
|
+
const targetRemote = 'https://github.com/example-owner/example-repo.git';
|
|
231
231
|
try {
|
|
232
232
|
writeFileSync(
|
|
233
233
|
join(gitHooksDirectory, 'broken_advisory.py'),
|
|
@@ -159,15 +159,10 @@ export const CODEX_RULES_SHIPPED_FILE_NAME = 'claude-dev-env.rules';
|
|
|
159
159
|
export const DEFAULT_CURSOR_DIRECTORY_NAME = '.cursor';
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
* Directory name under the Cursor home
|
|
162
|
+
* Directory name for rule files, under the Cursor home and under the agents home.
|
|
163
163
|
*/
|
|
164
164
|
export const CURSOR_RULES_DIRECTORY_NAME = 'rules';
|
|
165
165
|
|
|
166
|
-
/**
|
|
167
|
-
* Installed script that writes Cursor `.mdc` files from Claude rules.
|
|
168
|
-
*/
|
|
169
|
-
export const CURSOR_SYNC_SCRIPT_FILE_NAME = 'sync_to_cursor.py';
|
|
170
|
-
|
|
171
166
|
/**
|
|
172
167
|
* Windows Python launcher command the installer may bake into hook settings.
|
|
173
168
|
*/
|
|
@@ -15,7 +15,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
import { execFileSync } from 'node:child_process';
|
|
16
16
|
import {
|
|
17
17
|
resolveInstallRoot,
|
|
18
|
-
isAllowedInstallDestination,
|
|
19
18
|
} from './resolve-install-root.mjs';
|
|
20
19
|
import {
|
|
21
20
|
DEFAULT_CURSOR_DIRECTORY_NAME,
|
|
@@ -43,78 +42,54 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
|
|
|
43
42
|
});
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
test('
|
|
47
|
-
const homeDirectory = join(tmpdir(), 'cdev-cursor-rules-home');
|
|
48
|
-
const resolution = resolveInstallRoot({
|
|
49
|
-
homeDirectory,
|
|
50
|
-
environment: {},
|
|
51
|
-
explicitTarget: null,
|
|
52
|
-
});
|
|
53
|
-
const expectedDirectory = join(
|
|
54
|
-
homeDirectory,
|
|
55
|
-
DEFAULT_CURSOR_DIRECTORY_NAME,
|
|
56
|
-
CURSOR_RULES_DIRECTORY_NAME,
|
|
57
|
-
);
|
|
58
|
-
assert.equal(resolution.cursorRulesInstallDirectory, expectedDirectory);
|
|
59
|
-
assert.equal(
|
|
60
|
-
isAllowedInstallDestination(join(expectedDirectory, 'asd-ste100-language.mdc'), resolution),
|
|
61
|
-
true,
|
|
62
|
-
);
|
|
63
|
-
assert.equal(
|
|
64
|
-
isAllowedInstallDestination(join(homeDirectory, '.ssh', 'id_rsa'), resolution),
|
|
65
|
-
false,
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('a full install writes stem-named Cursor rules and leaves a local extra mdc in place', () => {
|
|
45
|
+
test('a reinstall moves the Cursor rules an older install generated and keeps a local mdc', () => {
|
|
70
46
|
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-install-'));
|
|
71
47
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
78
|
-
mkdirSync(
|
|
79
|
-
writeFileSync(
|
|
48
|
+
runInstaller(homeDirectory, []);
|
|
49
|
+
const cursorDirectory = join(homeDirectory, DEFAULT_CURSOR_DIRECTORY_NAME);
|
|
50
|
+
const rulesDirectory = join(cursorDirectory, CURSOR_RULES_DIRECTORY_NAME);
|
|
51
|
+
const generatedRulePath = join(rulesDirectory, 'asd-ste100-language.mdc');
|
|
52
|
+
const syncManifestPath = join(cursorDirectory, '.sync-manifest.json');
|
|
53
|
+
const localRulePath = join(rulesDirectory, 'user-local.mdc');
|
|
54
|
+
mkdirSync(rulesDirectory, { recursive: true });
|
|
55
|
+
writeFileSync(generatedRulePath, 'generated\n');
|
|
56
|
+
writeFileSync(syncManifestPath, '{}\n');
|
|
57
|
+
writeFileSync(localRulePath, 'keep-me\n');
|
|
58
|
+
const manifestPath = join(homeDirectory, '.claude', '.claude-dev-env-manifest.json');
|
|
59
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
60
|
+
manifest.files.push(generatedRulePath, syncManifestPath);
|
|
61
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
80
62
|
|
|
81
63
|
runInstaller(homeDirectory, []);
|
|
82
64
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
89
|
-
assert.equal(existsSync(generatedPath), true);
|
|
90
|
-
const generatedText = readFileSync(generatedPath, 'utf8');
|
|
91
|
-
assert.equal(generatedText.includes('alwaysApply: true'), true);
|
|
92
|
-
assert.equal(readFileSync(extraRulePath, 'utf8'), 'keep-me\n');
|
|
65
|
+
assert.equal(existsSync(generatedRulePath), false);
|
|
66
|
+
assert.equal(existsSync(syncManifestPath), false);
|
|
67
|
+
assert.equal(readFileSync(localRulePath, 'utf8'), 'keep-me\n');
|
|
68
|
+
const reinstalledManifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
69
|
+
assert.equal(reinstalledManifest.files.includes(generatedRulePath), false);
|
|
93
70
|
} finally {
|
|
94
71
|
rmSync(homeDirectory, { recursive: true, force: true });
|
|
95
72
|
}
|
|
96
73
|
});
|
|
97
74
|
|
|
98
|
-
test('--only journal is rejected before
|
|
75
|
+
test('--only journal is rejected before the policy is seeded; --only core seeds it and writes no Cursor rules', () => {
|
|
99
76
|
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-groups-'));
|
|
100
77
|
try {
|
|
101
|
-
const
|
|
78
|
+
const cursorRulesDirectory = join(
|
|
102
79
|
homeDirectory,
|
|
103
80
|
DEFAULT_CURSOR_DIRECTORY_NAME,
|
|
104
81
|
CURSOR_RULES_DIRECTORY_NAME,
|
|
105
|
-
'asd-ste100-language.mdc',
|
|
106
82
|
);
|
|
107
83
|
const policyPath = join(homeDirectory, '.agents', 'rules', 'subagent-model-policy.json');
|
|
108
84
|
assert.throws(
|
|
109
85
|
() => runInstaller(homeDirectory, ['--only', 'journal']),
|
|
110
86
|
error => error.status === 1 && /Unknown group\(s\): journal/.test(error.stderr),
|
|
111
87
|
);
|
|
112
|
-
assert.equal(existsSync(generatedPath), false);
|
|
113
88
|
assert.equal(existsSync(policyPath), false);
|
|
114
89
|
|
|
115
90
|
runInstaller(homeDirectory, ['--only', 'core']);
|
|
116
|
-
assert.equal(existsSync(generatedPath), true);
|
|
117
91
|
assert.equal(existsSync(policyPath), true);
|
|
92
|
+
assert.equal(existsSync(cursorRulesDirectory), false);
|
|
118
93
|
} finally {
|
|
119
94
|
rmSync(homeDirectory, { recursive: true, force: true });
|
|
120
95
|
}
|
|
@@ -427,7 +402,7 @@ test('uninstalling one profile keeps routing for another installed profile', ()
|
|
|
427
402
|
}
|
|
428
403
|
});
|
|
429
404
|
|
|
430
|
-
test('a blocked shared rule destination
|
|
405
|
+
test('a blocked shared rule destination fails the install and keeps the existing file', () => {
|
|
431
406
|
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-rollback-'));
|
|
432
407
|
try {
|
|
433
408
|
const sharedRulesPath = join(homeDirectory, '.agents', 'rules');
|
|
@@ -435,8 +410,6 @@ test('a blocked shared rule destination rolls back generated Cursor files', () =
|
|
|
435
410
|
writeFileSync(sharedRulesPath, 'keep-existing-file');
|
|
436
411
|
assert.throws(() => runInstaller(homeDirectory, ['--only', 'core']));
|
|
437
412
|
assert.equal(readFileSync(sharedRulesPath, 'utf8'), 'keep-existing-file');
|
|
438
|
-
assert.equal(existsSync(join(homeDirectory, '.cursor', 'rules', 'pstack-models.mdc')), false);
|
|
439
|
-
assert.equal(existsSync(join(homeDirectory, '.cursor', '.sync-manifest.json')), false);
|
|
440
413
|
} finally {
|
|
441
414
|
rmSync(homeDirectory, { recursive: true, force: true });
|
|
442
415
|
}
|
package/bin/install.mjs
CHANGED
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
CODEX_HOME_ENVIRONMENT_VARIABLE,
|
|
34
34
|
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
35
35
|
CODEX_RULES_PACKAGE_DIRECTORY_NAME,
|
|
36
|
-
CURSOR_SYNC_SCRIPT_FILE_NAME,
|
|
37
36
|
CURSOR_RULES_DIRECTORY_NAME,
|
|
38
37
|
SUBAGENT_MODEL_POLICY_FILE_NAME,
|
|
39
38
|
WINDOWS_PYTHON_LAUNCHER_COMMAND,
|
|
@@ -378,58 +377,6 @@ export function pythonFileAndPrefixArguments(pythonCommand) {
|
|
|
378
377
|
return { file: unquoted, prefixArguments: [] };
|
|
379
378
|
}
|
|
380
379
|
|
|
381
|
-
/**
|
|
382
|
-
* Read generated Cursor paths from the sync manifest under a Cursor home.
|
|
383
|
-
*
|
|
384
|
-
* Args:
|
|
385
|
-
* cursorRoot: Absolute `.cursor` directory.
|
|
386
|
-
*
|
|
387
|
-
* Returns:
|
|
388
|
-
* Absolute paths the installer may record, including the sync manifest.
|
|
389
|
-
*/
|
|
390
|
-
export function collectManagedCursorSyncPaths(cursorRoot) {
|
|
391
|
-
const manifestPath = join(cursorRoot, '.sync-manifest.json');
|
|
392
|
-
if (!existsSync(manifestPath)) return [];
|
|
393
|
-
const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
394
|
-
const generatedPaths = [manifestPath];
|
|
395
|
-
const allEntries = { ...(parsed.entries || {}), ...(parsed.docs_entries || {}) };
|
|
396
|
-
for (const eachRelativePath of Object.keys(allEntries)) {
|
|
397
|
-
generatedPaths.push(join(cursorRoot, eachRelativePath));
|
|
398
|
-
}
|
|
399
|
-
return generatedPaths;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Run the installed Cursor rule generator against Claude and Cursor roots.
|
|
404
|
-
*
|
|
405
|
-
* Args:
|
|
406
|
-
* pythonCommand: Interpreter command from install preflight.
|
|
407
|
-
* scriptPath: Absolute `sync_to_cursor.py` path.
|
|
408
|
-
* claudeRoot: Managed Claude root that holds `rules/` and `docs/`.
|
|
409
|
-
* cursorRoot: Cursor home that receives `rules/*.mdc`.
|
|
410
|
-
*
|
|
411
|
-
* Returns:
|
|
412
|
-
* void
|
|
413
|
-
*/
|
|
414
|
-
export function runCursorRuleSync(pythonCommand, scriptPath, claudeRoot, cursorRoot) {
|
|
415
|
-
mkdirSync(cursorRoot, { recursive: true });
|
|
416
|
-
const { file, prefixArguments } = pythonFileAndPrefixArguments(pythonCommand);
|
|
417
|
-
execFileSync(
|
|
418
|
-
file,
|
|
419
|
-
[
|
|
420
|
-
...prefixArguments,
|
|
421
|
-
scriptPath,
|
|
422
|
-
'--force',
|
|
423
|
-
'--quiet',
|
|
424
|
-
'--claude-root',
|
|
425
|
-
claudeRoot,
|
|
426
|
-
'--cursor-root',
|
|
427
|
-
cursorRoot,
|
|
428
|
-
],
|
|
429
|
-
{ stdio: 'inherit' },
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
380
|
/**
|
|
434
381
|
* Install the pstack plugin from its marketplace into the hosts this run serves.
|
|
435
382
|
*
|
|
@@ -2371,6 +2318,19 @@ function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalled
|
|
|
2371
2318
|
skillsPrunedCount += rootOutcome.prunedCount;
|
|
2372
2319
|
}
|
|
2373
2320
|
}
|
|
2321
|
+
const cursorInstallDirectory = INSTALL_ROOT_RESOLUTION.cursorInstallDirectory;
|
|
2322
|
+
const retiredCursorSyncManifestPath = join(cursorInstallDirectory, '.sync-manifest.json');
|
|
2323
|
+
const isRetiredCursorRuleFile = priorFile => resolve(priorFile) === retiredCursorSyncManifestPath
|
|
2324
|
+
|| isInsideDirectory(resolve(priorFile), INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory);
|
|
2325
|
+
const retiredCursorRulesOutcome = pruneStaleInstalledFiles(
|
|
2326
|
+
priorInstalledFiles?.filter(isRetiredCursorRuleFile) ?? null,
|
|
2327
|
+
currentInstalledFiles,
|
|
2328
|
+
cursorInstallDirectory,
|
|
2329
|
+
join(backupRoot, 'cursor'),
|
|
2330
|
+
{ managedHomeDirectory: cursorInstallDirectory },
|
|
2331
|
+
);
|
|
2332
|
+
prunedCount += retiredCursorRulesOutcome.prunedCount;
|
|
2333
|
+
failedPaths.push(...retiredCursorRulesOutcome.failedPaths);
|
|
2374
2334
|
const retiredClaudeGuidancePath = join(CLAUDE_HOME, 'AGENTS.md');
|
|
2375
2335
|
const retiredGuidanceOutcome = pruneStaleInstalledFiles(
|
|
2376
2336
|
priorInstalledFiles?.filter(priorFile => resolve(priorFile) === retiredClaudeGuidancePath) ?? null,
|
|
@@ -2714,19 +2674,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2714
2674
|
allInstalledFiles.push(...stats.paths);
|
|
2715
2675
|
}
|
|
2716
2676
|
}
|
|
2717
|
-
const
|
|
2677
|
+
const shouldSeedSubagentModelPolicy = !selectedGroups
|
|
2718
2678
|
|| activeGroups.some((eachGroup) => (eachGroup.includeDirectories || []).includes('rules'));
|
|
2719
|
-
if (
|
|
2720
|
-
const scriptPath = join(CLAUDE_HOME, 'scripts', CURSOR_SYNC_SCRIPT_FILE_NAME);
|
|
2721
|
-
if (!existsSync(scriptPath)) {
|
|
2722
|
-
throw new Error(`cursor rule sync script missing: ${scriptPath}`);
|
|
2723
|
-
}
|
|
2724
|
-
const cursorRoot = dirname(INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory);
|
|
2725
|
-
runCursorRuleSync(pythonCommand, scriptPath, CLAUDE_HOME, cursorRoot);
|
|
2726
|
-
const generatedCursorPaths = collectManagedCursorSyncPaths(cursorRoot);
|
|
2727
|
-
allInstalledFiles.push(...generatedCursorPaths);
|
|
2728
|
-
summary.cursorRules = { created: generatedCursorPaths.length, updated: 0, paths: generatedCursorPaths };
|
|
2729
|
-
syncWrittenPaths(allInstalledFiles);
|
|
2679
|
+
if (shouldSeedSubagentModelPolicy) {
|
|
2730
2680
|
const seededPolicyPath = seedSubagentModelPolicy();
|
|
2731
2681
|
if (seededPolicyPath) {
|
|
2732
2682
|
allInstalledFiles.push(seededPolicyPath);
|
|
@@ -2988,10 +2938,6 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2988
2938
|
const { created, updated } = summary.codexRules;
|
|
2989
2939
|
console.log(` ${CODEX_RULES_PACKAGE_DIRECTORY_NAME}: ${created + updated} files (${created} new, ${updated} updated)`);
|
|
2990
2940
|
}
|
|
2991
|
-
if (summary.cursorRules) {
|
|
2992
|
-
const { created } = summary.cursorRules;
|
|
2993
|
-
console.log(` cursor-rules: ${created} generated files`);
|
|
2994
|
-
}
|
|
2995
2941
|
if (summary[MANAGED_AGENTS_DIRECTORY_NAME]) {
|
|
2996
2942
|
const { created, updated } = summary[MANAGED_AGENTS_DIRECTORY_NAME];
|
|
2997
2943
|
console.log(` ${MANAGED_AGENTS_DIRECTORY_NAME}: ${created + updated} files (${created} new, ${updated} updated)`);
|
|
@@ -3336,7 +3282,7 @@ Install location: ~/.claude/ by default; CLAUDE_CONFIG_DIR or --target selects a
|
|
|
3336
3282
|
Skills and agents live under ~/.agents (sibling of ~/.claude). ~/.claude/skills and ~/.claude/agents are directory pointers to that home.
|
|
3337
3283
|
Named profiles resolve under LLM_SETTINGS_PROFILES_ROOT or ~/.claude-profiles/<directoryName>.
|
|
3338
3284
|
Codex exec-policy files copy into ~/.codex/rules, or CODEX_HOME/rules when CODEX_HOME is set.
|
|
3339
|
-
Cursor rule files
|
|
3285
|
+
A reinstall moves the Cursor rule files older installs generated under ~/.cursor/rules into the run backup.
|
|
3340
3286
|
|
|
3341
3287
|
The installer reads only flags. A bare path argument carries no meaning, so
|
|
3342
3288
|
npx ${PACKAGE_NAME} . lands in the same root as a run with no argument.
|