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.
Files changed (85) hide show
  1. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  2. package/.agents/skills/team-advisor/SKILL.md +1 -1
  3. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  4. package/_shared/advisor/advisor-protocol.md +19 -19
  5. package/_shared/advisor/reference/advisor-block.md +1 -1
  6. package/_shared/advisor/reference/astra-rung.md +11 -11
  7. package/_shared/advisor/reference/identity.md +4 -4
  8. package/_shared/advisor/reference/lifecycle.md +2 -2
  9. package/_shared/advisor/reference/spawn-walk-log.md +4 -4
  10. package/_shared/advisor/reference/third-party-bind.md +7 -7
  11. package/_shared/advisor/reference/warm-up.md +3 -3
  12. package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
  13. package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +2 -2
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
  16. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -3
  17. package/_shared/advisor/scripts/model_tier_run_validator.py +8 -9
  18. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  19. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  20. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +91 -91
  21. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  22. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  23. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  24. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  25. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  26. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  27. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  28. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  29. package/bin/git_hooks_installer.test.mjs +1 -1
  30. package/bin/install-constants.mjs +1 -6
  31. package/bin/install.cursor-rules.test.mjs +24 -51
  32. package/bin/install.mjs +16 -70
  33. package/docs/codex-accounts.md +79 -0
  34. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  35. package/hooks/blocking/test_pii_scanner.py +1 -1
  36. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  37. package/hooks/git-hooks/conftest.py +16 -0
  38. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  39. package/hooks/git-hooks/test_post_commit.py +1 -1
  40. package/hooks/git-hooks/test_pre_commit.py +1 -1
  41. package/hooks/git-hooks/test_pre_push.py +2 -2
  42. package/hooks/git-hooks/test_verification_start.py +1 -1
  43. package/hooks/git-hooks/verification_notice.py +2 -2
  44. package/hooks/git-hooks/verification_notice_context.py +21 -2
  45. package/hooks/git-hooks/verification_start.py +2 -3
  46. package/package.json +1 -1
  47. package/rules/durable-post-artifacts.md +4 -3
  48. package/scripts/claude_account_profile.py +21 -9
  49. package/scripts/codex_account_choice.py +396 -0
  50. package/scripts/codex_account_meters.py +372 -0
  51. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  52. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  53. package/scripts/private_term_scan.py +6 -2
  54. package/scripts/repository_checks/config/constants.py +1 -0
  55. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  56. package/scripts/test_claude_account_profile.py +12 -0
  57. package/scripts/test_codex_account_choice.py +276 -0
  58. package/scripts/test_codex_account_meters.py +205 -0
  59. package/scripts/tests/test_automatic_advisory.py +3 -3
  60. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  61. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  62. package/scripts/tests/test_closed_pr_label.py +1 -1
  63. package/scripts/tests/test_private_term_scan.py +28 -0
  64. package/scripts/tests/test_review_closure.py +2 -2
  65. package/scripts/tests/test_review_closure_model.py +2 -2
  66. package/scripts/tests/test_rules.py +2 -83
  67. package/scripts/tests/test_tracked_private_terms.py +15 -0
  68. package/scripts/tests/test_verification_notice.py +29 -13
  69. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  70. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  71. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  72. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  73. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  74. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  75. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  76. package/scripts/sync_to_cursor/__init__.py +0 -13
  77. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  78. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  79. package/scripts/sync_to_cursor/engine.py +0 -219
  80. package/scripts/sync_to_cursor/hashing.py +0 -7
  81. package/scripts/sync_to_cursor/paths.py +0 -18
  82. package/scripts/sync_to_cursor/rules.py +0 -494
  83. package/scripts/sync_to_cursor.py +0 -22
  84. package/scripts/tests/test_engine.py +0 -101
  85. 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
- FIXTURE_KEY_CODEX_PERCENT_LEFT: str = "codex_percent_left"
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 _null_codex_percent_left() -> float | None:
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, "_probe_codex_percent_left", _null_codex_percent_left
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="JonEcho", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
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="JonEcho", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
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="JonEcho", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
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="JonEcho", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
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-threshold,
4
- skipped-by-token, skipped-by-down. Threshold comes from the probe constant
5
- via ``is_codex_review_required`` — this module never inlines the percent.
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
- PERCENT_ABOVE_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT) + 1.0
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
- codex_percent_left: float | None,
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
- "codex_percent_left": codex_percent_left,
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
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
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 should_pass_without_reading_usage_when_the_clean_stamp_is_on_head(
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 _probe_that_must_not_run() -> float | None:
123
+ def _picker_that_must_not_run() -> str | None:
127
124
  raise AssertionError(
128
- "the weekly usage probe must not run when codex_clean_at is on HEAD"
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
- read_percent_left=check_convergence._probe_codex_percent_left,
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
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
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
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
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
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
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
- def should_skip_when_usage_at_or_below_threshold(
236
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
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
- codex_percent_left=PERCENT_AT_THRESHOLD,
236
+ codex_tier=tier_without_room,
241
237
  codex_clean_at=None,
242
- filename="skipped-threshold.json",
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 should_skip_when_codex_percent_left_is_null(
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
- codex_percent_left=None,
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
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
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
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
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
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
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
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
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 _null_codex_percent_left() -> float | None:
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, "_probe_codex_percent_left", _null_codex_percent_left
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/Jon (Admin)/...`).
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/Jon (Admin)/project") is False
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/JonEcho/python-automation.git';
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 that holds generated `.mdc` rule files.
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('resolveInstallRoot names ~/.cursor/rules and allows generated mdc files under it', () => {
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
- const extraRulePath = join(
73
- homeDirectory,
74
- DEFAULT_CURSOR_DIRECTORY_NAME,
75
- CURSOR_RULES_DIRECTORY_NAME,
76
- 'user-local.mdc',
77
- );
78
- mkdirSync(dirname(extraRulePath), { recursive: true });
79
- writeFileSync(extraRulePath, 'keep-me\n');
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
- const generatedPath = join(
84
- homeDirectory,
85
- DEFAULT_CURSOR_DIRECTORY_NAME,
86
- CURSOR_RULES_DIRECTORY_NAME,
87
- 'asd-ste100-language.mdc',
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 Cursor rules change; --only core writes them', () => {
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 generatedPath = join(
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 rolls back generated Cursor files', () => {
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 shouldInstallCursorRules = !selectedGroups
2677
+ const shouldSeedSubagentModelPolicy = !selectedGroups
2718
2678
  || activeGroups.some((eachGroup) => (eachGroup.includeDirectories || []).includes('rules'));
2719
- if (shouldInstallCursorRules) {
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 generate into ~/.cursor/rules as stem-named mdc files, one per Claude rule.
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.