claude-dev-env 8.12.0 → 8.13.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 (74) hide show
  1. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  2. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  3. package/_shared/advisor/reference/astra-rung.md +7 -7
  4. package/_shared/advisor/reference/third-party-bind.md +2 -2
  5. package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
  6. package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
  7. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
  8. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  9. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  10. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  11. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  12. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  13. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  14. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  15. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  16. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  17. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  18. package/bin/git_hooks_installer.test.mjs +1 -1
  19. package/bin/install-constants.mjs +1 -6
  20. package/bin/install.cursor-rules.test.mjs +24 -51
  21. package/bin/install.mjs +16 -70
  22. package/docs/codex-accounts.md +79 -0
  23. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  24. package/hooks/blocking/test_pii_scanner.py +1 -1
  25. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  26. package/hooks/git-hooks/conftest.py +16 -0
  27. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  28. package/hooks/git-hooks/test_post_commit.py +1 -1
  29. package/hooks/git-hooks/test_pre_commit.py +1 -1
  30. package/hooks/git-hooks/test_pre_push.py +2 -2
  31. package/hooks/git-hooks/test_verification_start.py +1 -1
  32. package/hooks/git-hooks/verification_notice.py +2 -2
  33. package/hooks/git-hooks/verification_notice_context.py +21 -2
  34. package/hooks/git-hooks/verification_start.py +2 -3
  35. package/package.json +1 -1
  36. package/rules/durable-post-artifacts.md +4 -3
  37. package/scripts/claude_account_profile.py +21 -9
  38. package/scripts/codex_account_choice.py +396 -0
  39. package/scripts/codex_account_meters.py +372 -0
  40. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  41. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  42. package/scripts/private_term_scan.py +6 -2
  43. package/scripts/repository_checks/config/constants.py +1 -0
  44. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  45. package/scripts/test_claude_account_profile.py +12 -0
  46. package/scripts/test_codex_account_choice.py +276 -0
  47. package/scripts/test_codex_account_meters.py +205 -0
  48. package/scripts/tests/test_automatic_advisory.py +3 -3
  49. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  50. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  51. package/scripts/tests/test_closed_pr_label.py +1 -1
  52. package/scripts/tests/test_private_term_scan.py +28 -0
  53. package/scripts/tests/test_review_closure.py +2 -2
  54. package/scripts/tests/test_review_closure_model.py +2 -2
  55. package/scripts/tests/test_rules.py +2 -83
  56. package/scripts/tests/test_tracked_private_terms.py +15 -0
  57. package/scripts/tests/test_verification_notice.py +29 -13
  58. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  59. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  60. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  61. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  62. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  63. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  64. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  65. package/scripts/sync_to_cursor/__init__.py +0 -13
  66. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  67. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  68. package/scripts/sync_to_cursor/engine.py +0 -219
  69. package/scripts/sync_to_cursor/hashing.py +0 -7
  70. package/scripts/sync_to_cursor/paths.py +0 -18
  71. package/scripts/sync_to_cursor/rules.py +0 -494
  72. package/scripts/sync_to_cursor.py +0 -22
  73. package/scripts/tests/test_engine.py +0 -101
  74. package/scripts/tests/test_sync_to_cursor.py +0 -315
@@ -1,66 +0,0 @@
1
- """Copy and verify canonical docs under .cursor/docs/."""
2
-
3
- import shutil
4
- from pathlib import Path
5
-
6
- from sync_to_cursor.config import ALL_CANONICAL_DOC_FILES
7
- from sync_to_cursor.hashing import sha256_bytes
8
-
9
-
10
- def sync_canonical_docs(
11
- claude: Path,
12
- cursor: Path,
13
- dry_run: bool,
14
- quiet: bool,
15
- ) -> dict:
16
- docs_out = cursor / "docs"
17
- if not dry_run:
18
- docs_out.mkdir(parents=True, exist_ok=True)
19
- new_docs: dict = {}
20
- for each_name in ALL_CANONICAL_DOC_FILES:
21
- src = claude / "docs" / each_name
22
- dst = docs_out / each_name
23
- if not src.is_file():
24
- if dst.is_file():
25
- if not dry_run:
26
- dst.unlink()
27
- if not quiet:
28
- print(f"WARN docs/{each_name} (source removed — deleted stale copy at {dst})")
29
- elif not quiet:
30
- print(f"WARN docs/{each_name} (missing source: {src})")
31
- continue
32
- key = f"docs/{each_name}"
33
- src_hash = sha256_bytes(src.read_bytes())
34
- if dry_run:
35
- if dst.is_file():
36
- out_hash = sha256_bytes(dst.read_bytes())
37
- else:
38
- out_hash = ""
39
- else:
40
- shutil.copy2(src, dst)
41
- out_hash = sha256_bytes(dst.read_bytes())
42
- new_docs[key] = {"sources_hash": src_hash, "output_hash": out_hash}
43
- return new_docs
44
-
45
-
46
- def check_canonical_docs(claude: Path, cursor: Path, docs_entries: dict) -> bool:
47
- for each_name in ALL_CANONICAL_DOC_FILES:
48
- key = f"docs/{each_name}"
49
- src = claude / "docs" / each_name
50
- dst = cursor / "docs" / each_name
51
- if not src.is_file():
52
- if key in docs_entries:
53
- return False
54
- continue
55
- if not dst.is_file():
56
- return False
57
- src_hash = sha256_bytes(src.read_bytes())
58
- dst_hash = sha256_bytes(dst.read_bytes())
59
- prev = docs_entries.get(key)
60
- if not prev:
61
- return False
62
- if prev.get("sources_hash") != src_hash:
63
- return False
64
- if prev.get("output_hash") != dst_hash:
65
- return False
66
- return True
@@ -1,8 +0,0 @@
1
- """Shared configuration for the sync-to-cursor package."""
2
-
3
- GENERATOR_VERSION: str = "1.3.0"
4
- ALL_CANONICAL_DOC_FILES: tuple[str, ...] = ("CODE_RULES.md", "TEST_QUALITY.md")
5
- MAX_RULE_BODY_LINES: int = 50
6
- ALL_SKIPPED_RULE_FILE_NAMES: frozenset[str] = frozenset({"CLAUDE.md", "AGENTS.md"})
7
- MARKDOWN_SUFFIX: str = ".md"
8
- CLAUDE_RULES_DIRECTORY_NAME: str = "rules"
@@ -1,219 +0,0 @@
1
- """Sync Claude rules to Cursor .mdc files and manifest."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import sys
8
- from datetime import datetime, timezone
9
- from pathlib import Path
10
-
11
- from sync_to_cursor.canonical_docs import check_canonical_docs, sync_canonical_docs
12
- from sync_to_cursor.config import GENERATOR_VERSION
13
- from sync_to_cursor.hashing import sha256_bytes
14
- from sync_to_cursor.paths import llm_layout_paths
15
- from sync_to_cursor.rules import RuleMapping, _full_mdc, apply_transform, build_mappings
16
-
17
-
18
- def _load_manifest(manifest_path: Path) -> dict:
19
- if not manifest_path.is_file():
20
- return {}
21
- try:
22
- return json.loads(manifest_path.read_text(encoding="utf-8"))
23
- except json.JSONDecodeError:
24
- return {}
25
-
26
-
27
- def _sources_hash(paths: tuple[Path, ...]) -> str:
28
- return sha256_bytes(b"\x00".join(p.read_bytes() for p in paths))
29
-
30
-
31
- def _run_check(
32
- mappings: tuple[RuleMapping, ...],
33
- out_dir: Path,
34
- entries_meta: dict,
35
- claude: Path,
36
- cursor: Path,
37
- docs_entries_meta: dict,
38
- ) -> int:
39
- for each_mapping in mappings:
40
- key = f"rules/{each_mapping.output_name}"
41
- out_path = out_dir / each_mapping.output_name
42
- missing = [source for source in each_mapping.sources if not source.is_file()]
43
- if missing:
44
- if each_mapping.always_apply:
45
- return 1
46
- continue
47
- src_hash = _sources_hash(each_mapping.sources)
48
- prev = entries_meta.get(key, {})
49
- if src_hash != prev.get("sources_hash"):
50
- return 1
51
- prev_out = prev.get("output_hash", "")
52
- if out_path.is_file() and prev_out:
53
- if sha256_bytes(out_path.read_bytes()) != prev_out:
54
- return 1
55
- elif not out_path.is_file():
56
- return 1
57
- if out_dir.is_dir():
58
- for each_path in out_dir.glob("*.mdc"):
59
- if each_path.name not in {x.output_name for x in mappings}:
60
- return 1
61
- if not check_canonical_docs(claude, cursor, docs_entries_meta):
62
- return 1
63
- return 0
64
-
65
-
66
- def _sync_rules(
67
- mappings: tuple[RuleMapping, ...],
68
- out_dir: Path,
69
- entries_meta: dict,
70
- *,
71
- force: bool,
72
- dry_run: bool,
73
- quiet: bool,
74
- cursor: Path,
75
- ) -> tuple[dict, dict]:
76
- summary: dict = {"skip": 0, "update": 0, "tampered": 0, "warn": 0, "orphan": 0}
77
- new_entries: dict = {}
78
- write_allowed = not dry_run
79
-
80
- for each_mapping in mappings:
81
- key = f"rules/{each_mapping.output_name}"
82
- out_path = out_dir / each_mapping.output_name
83
- missing = [source for source in each_mapping.sources if not source.is_file()]
84
- if missing:
85
- summary["warn"] += 1
86
- if not quiet:
87
- print(f"WARN rules/{each_mapping.output_name} (missing source: {missing})")
88
- continue
89
-
90
- src_hash = _sources_hash(each_mapping.sources)
91
- prev = entries_meta.get(key, {})
92
- prev_src = prev.get("sources_hash", "")
93
- prev_out = prev.get("output_hash", "")
94
-
95
- if (
96
- not force
97
- and out_path.is_file()
98
- and prev_src == src_hash
99
- and prev_out
100
- and sha256_bytes(out_path.read_bytes()) == prev_out
101
- ):
102
- summary["skip"] += 1
103
- new_entries[key] = {"sources_hash": src_hash, "output_hash": prev_out}
104
- continue
105
-
106
- if (
107
- not force
108
- and out_path.is_file()
109
- and prev_src == src_hash
110
- and prev_out
111
- and sha256_bytes(out_path.read_bytes()) != prev_out
112
- ):
113
- summary["tampered"] += 1
114
- if not quiet:
115
- print(f"TAMPERED rules/{each_mapping.output_name} (manual edit; regenerating)")
116
-
117
- summary["update"] += 1
118
- if not quiet:
119
- print(f"UPDATE rules/{each_mapping.output_name}")
120
-
121
- body = apply_transform(
122
- each_mapping.transform,
123
- each_mapping.sources,
124
- strip_leading_frontmatter=each_mapping.strip_leading_frontmatter,
125
- )
126
- full = _full_mdc(each_mapping, body)
127
- out_hash = sha256_bytes(full.encode("utf-8"))
128
- new_entries[key] = {"sources_hash": src_hash, "output_hash": out_hash}
129
-
130
- if write_allowed:
131
- out_path.write_text(full, encoding="utf-8", newline="\n")
132
-
133
- expected = {each_mapping.output_name for each_mapping in mappings}
134
- for each_path in out_dir.glob("*.mdc"):
135
- if each_path.name not in expected:
136
- summary["orphan"] += 1
137
- if not quiet:
138
- print(f"WARN {each_path.relative_to(cursor)} (orphan — not generated by this tool)")
139
-
140
- return summary, new_entries
141
-
142
-
143
- def _layout_from_args(args: argparse.Namespace) -> tuple[Path, Path, Path, Path]:
144
- """Resolve Claude and Cursor roots from flags or the default layout.
145
-
146
- Args:
147
- args: Parsed CLI arguments.
148
-
149
- Returns:
150
- Claude root, Cursor root, rules output directory, and manifest path.
151
-
152
- Raises:
153
- SystemExit: When only one of `--claude-root` / `--cursor-root` is set.
154
- """
155
- claude_root = args.claude_root
156
- cursor_root = args.cursor_root
157
- if (claude_root is None) != (cursor_root is None):
158
- raise SystemExit("both --claude-root and --cursor-root are required")
159
- if claude_root is None:
160
- return llm_layout_paths()
161
- claude = Path(claude_root).expanduser().resolve()
162
- cursor = Path(cursor_root).expanduser().resolve()
163
- return claude, cursor, cursor / "rules", cursor / ".sync-manifest.json"
164
-
165
-
166
- def run(argv: list[str] | None = None) -> int:
167
- argument_parser = argparse.ArgumentParser(description="Sync Claude rules to Cursor .mdc files")
168
- argument_parser.add_argument("--force", action="store_true", help="Regenerate all outputs")
169
- argument_parser.add_argument("--dry-run", action="store_true", help="Print actions only")
170
- argument_parser.add_argument("--check", action="store_true", help="Exit 1 if anything stale")
171
- argument_parser.add_argument("--quiet", action="store_true", help="Minimal output when up to date")
172
- argument_parser.add_argument("--claude-root", help="Claude layout root holding rules/ and docs/")
173
- argument_parser.add_argument("--cursor-root", help="Cursor layout root receiving rules/ and docs/")
174
- args = argument_parser.parse_args(argv)
175
-
176
- claude, cursor, out_dir, manifest_path = _layout_from_args(args)
177
- mappings = build_mappings(claude)
178
- old_manifest = _load_manifest(manifest_path)
179
- entries_meta: dict = old_manifest.get("entries", {})
180
- docs_entries_meta: dict = old_manifest.get("docs_entries", {})
181
-
182
- if args.check:
183
- return _run_check(mappings, out_dir, entries_meta, claude, cursor, docs_entries_meta)
184
-
185
- if not args.dry_run:
186
- out_dir.mkdir(parents=True, exist_ok=True)
187
- new_docs_entries = sync_canonical_docs(claude, cursor, args.dry_run, args.quiet)
188
-
189
- summary, new_entries = _sync_rules(
190
- mappings,
191
- out_dir,
192
- entries_meta,
193
- force=args.force,
194
- dry_run=args.dry_run,
195
- quiet=args.quiet,
196
- cursor=cursor,
197
- )
198
-
199
- manifest_out = {
200
- "generated_at": datetime.now(timezone.utc).isoformat(),
201
- "generator_version": GENERATOR_VERSION,
202
- "entries": new_entries,
203
- "docs_entries": new_docs_entries,
204
- }
205
- if not args.dry_run:
206
- manifest_path.write_text(json.dumps(manifest_out, indent=2) + "\n", encoding="utf-8")
207
-
208
- if (
209
- not args.quiet
210
- and summary["skip"]
211
- and not any((summary["update"], summary["tampered"], summary["warn"], summary["orphan"]))
212
- ):
213
- print(f"SKIP all {summary['skip']} rule(s) unchanged")
214
-
215
- return 0
216
-
217
-
218
- def main() -> int:
219
- return run(sys.argv[1:])
@@ -1,7 +0,0 @@
1
- """Content hashing for manifest entries."""
2
-
3
- import hashlib
4
-
5
-
6
- def sha256_bytes(content_bytes: bytes) -> str:
7
- return hashlib.sha256(content_bytes).hexdigest()
@@ -1,18 +0,0 @@
1
- """Resolve Claude / Cursor layout paths (LLM_SETTINGS_ROOT or home)."""
2
-
3
- import os
4
- from pathlib import Path
5
-
6
-
7
- def llm_layout_paths() -> tuple[Path, Path, Path, Path]:
8
- """Return (claude_dir, cursor_dir, rules_out_dir, manifest_path)."""
9
- raw = os.environ.get("LLM_SETTINGS_ROOT", "").strip()
10
- if raw:
11
- base = Path(raw).expanduser().resolve()
12
- claude = base / ".claude"
13
- cursor = base / ".cursor"
14
- else:
15
- home = Path.home()
16
- claude = home / ".claude"
17
- cursor = home / ".cursor"
18
- return claude, cursor, cursor / "rules", cursor / ".sync-manifest.json"