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.
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/reference/third-party-bind.md +2 -2
- 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/astra_advisor_constants.py +8 -8
- 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/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
|
@@ -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,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"
|