claude-dev-env 1.95.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -1,235 +1,279 @@
1
- """Review and Bugbot IO leaves for the convergence gate checks.
2
-
3
- Each function issues one ``gh`` REST call and returns a ``(passed, detail)``
4
- pair the convergence checker turns into a PASS/FAIL line, or a raw
5
- ``(returncode, stdout)`` pair for the low-level callers. The checker re-imports
6
- these names, so a test patches them on the checker module.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import json
12
- import re
13
- import subprocess
14
- import sys
15
-
16
- import _pr_converge_path_setup # noqa: F401
17
- from pr_converge_skill_constants.constants import (
18
- ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS,
19
- BUGBOT_CHECK_RUN_NAME_SUBSTRING,
20
- BUGBOT_DIRTY_BODY_REGEX,
21
- CHECK_RUNS_PER_PAGE,
22
- CURSOR_LOGIN_FILTER_SUBSTRING,
23
- EXIT_CODE_GH_ERROR,
24
- GH_CHECK_RUNS_PATH_TEMPLATE,
25
- GH_PR_OBJECT_PATH_TEMPLATE,
26
- GH_REVIEWS_PATH_TEMPLATE,
27
- REVIEWS_PER_PAGE,
28
- )
29
- from pr_converge_scripts_constants.convergence_gate_constants import SHORT_SHA_LENGTH
30
-
31
- JsonObject = dict[str, object]
32
- ReviewStateGroup = tuple[str, ...]
33
-
34
-
35
- def _run_gh_command(all_arguments: list[str]) -> tuple[int, str]:
36
- completed_process = subprocess.run(
37
- all_arguments,
38
- capture_output=True,
39
- text=True,
40
- encoding="utf-8",
41
- errors="replace",
42
- check=False,
43
- )
44
- return completed_process.returncode, completed_process.stdout
45
-
46
-
47
- def _gh_api(endpoint_path: str) -> tuple[int, str]:
48
- return _run_gh_command(["gh", "api", endpoint_path])
49
-
50
-
51
- def _gh_api_paginated(endpoint_path: str) -> tuple[int, str]:
52
- return _run_gh_command(["gh", "api", endpoint_path, "--paginate", "--slurp"])
53
-
54
-
55
- def _short_sha(commit_id: str) -> str:
56
- return commit_id[:SHORT_SHA_LENGTH]
57
-
58
-
59
- def _get_pr_head_sha(*, owner: str, repo: str, number: int) -> str:
60
- """Return the PR HEAD SHA, exiting with the gh-error code when the call fails."""
61
- endpoint = GH_PR_OBJECT_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
62
- returncode, stdout = _gh_api(endpoint)
63
- if returncode != 0:
64
- sys.stderr.write(f"gh api error fetching PR object: {stdout}\n")
65
- raise SystemExit(EXIT_CODE_GH_ERROR)
66
- pr_object = json.loads(stdout)
67
- head_sha: object = pr_object.get("head", {}).get("sha")
68
- if not isinstance(head_sha, str):
69
- raise SystemExit(EXIT_CODE_GH_ERROR)
70
- return head_sha
71
-
72
-
73
- def _get_mergeable(*, owner: str, repo: str, number: int) -> tuple[bool, str]:
74
- """Return whether the PR is cleanly mergeable, with the mergeable-state detail."""
75
- endpoint = GH_PR_OBJECT_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
76
- returncode, stdout = _gh_api(endpoint)
77
- if returncode != 0:
78
- return False, f"gh api error: {stdout}"
79
- pr_object = json.loads(stdout)
80
- mergeable: object = pr_object.get("mergeable")
81
- mergeable_state: object = pr_object.get("mergeable_state", "unknown")
82
- state_text = str(mergeable_state)
83
- if mergeable is True and state_text == "clean":
84
- return True, "clean"
85
- return False, state_text
86
-
87
-
88
- def _bugbot_check_runs(
89
- *, owner: str, repo: str, sha: str
90
- ) -> tuple[int, list[JsonObject]]:
91
- """Fetch the check runs for a SHA and return the returncode and run dicts."""
92
- endpoint = GH_CHECK_RUNS_PATH_TEMPLATE.format(owner=owner, repo=repo, sha=sha)
93
- returncode, stdout = _gh_api(f"{endpoint}?per_page={CHECK_RUNS_PER_PAGE}")
94
- if returncode != 0:
95
- return returncode, []
96
- try:
97
- payload = json.loads(stdout)
98
- except json.JSONDecodeError:
99
- return returncode, []
100
- raw_runs = payload.get("check_runs") if isinstance(payload, dict) else None
101
- if not isinstance(raw_runs, list):
102
- return returncode, []
103
- return returncode, [each_run for each_run in raw_runs if isinstance(each_run, dict)]
104
-
105
-
106
- def _bugbot_run_conclusion_detail(check_run: JsonObject) -> tuple[bool, str]:
107
- """Return the pass flag and detail for one bugbot check run."""
108
- conclusion = check_run.get("conclusion", "")
109
- if conclusion in ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS:
110
- check_id = check_run.get("id", "?")
111
- detail_url = check_run.get("html_url", "")
112
- details_suffix = f" ({detail_url})" if detail_url else ""
113
- return True, f"check run {check_id}, conclusion: {conclusion}{details_suffix}"
114
- return (
115
- False,
116
- f"check run conclusion is '{conclusion}', expected {ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS}",
117
- )
118
-
119
-
120
- def _check_bugbot(*, owner: str, repo: str, sha: str) -> tuple[bool, str]:
121
- """Return whether the Cursor Bugbot check run on a SHA completed successfully."""
122
- returncode, check_runs = _bugbot_check_runs(owner=owner, repo=repo, sha=sha)
123
- if returncode != 0:
124
- return False, "gh api error fetching check runs"
125
- for each_check_run in check_runs:
126
- each_name = each_check_run.get("name", "")
127
- if not isinstance(each_name, str):
128
- continue
129
- if BUGBOT_CHECK_RUN_NAME_SUBSTRING.lower() not in each_name.lower():
130
- continue
131
- return _bugbot_run_conclusion_detail(each_check_run)
132
- return False, "no bugbot check run found"
133
-
134
-
135
- def _flatten_paginated_reviews(stdout: str) -> list[JsonObject] | None:
136
- """Flatten a slurped, paginated reviews payload into a newest-first list."""
137
- try:
138
- reviews_payload = json.loads(stdout)
139
- except json.JSONDecodeError:
140
- return None
141
- if not isinstance(reviews_payload, list):
142
- return None
143
- all_flat: list[JsonObject] = [
144
- each_entry
145
- for each_page in reviews_payload
146
- if isinstance(each_page, list)
147
- for each_entry in each_page
148
- if isinstance(each_entry, dict)
149
- ]
150
- all_flat.sort(
151
- key=lambda each_review: str(each_review.get("submitted_at", "")), reverse=True
152
- )
153
- return all_flat
154
-
155
-
156
- def _review_matches_login(review: JsonObject, login_substring: str) -> bool:
157
- user_object = review.get("user")
158
- if not isinstance(user_object, dict):
159
- return False
160
- login = user_object.get("login", "")
161
- return isinstance(login, str) and login_substring in login.lower()
162
-
163
-
164
- def _review_commit_starts_with(review: JsonObject, head_sha: str) -> bool:
165
- commit_id = review.get("commit_id", "")
166
- return isinstance(commit_id, str) and commit_id.startswith(head_sha)
167
-
168
-
169
- def _check_bugbot_not_dirty(
170
- *, owner: str, repo: str, number: int, head_sha: str
171
- ) -> tuple[bool, str]:
172
- """Return whether the newest Cursor Bugbot review on HEAD reports no findings."""
173
- endpoint = GH_REVIEWS_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
174
- returncode, stdout = _gh_api_paginated(f"{endpoint}?per_page={REVIEWS_PER_PAGE}")
175
- if returncode != 0:
176
- return True, "bugbot reviews unavailable (non-fatal)"
177
- all_flat = _flatten_paginated_reviews(stdout)
178
- if all_flat is None:
179
- return True, "bugbot reviews not valid JSON (non-fatal)"
180
- dirty_pattern = re.compile(BUGBOT_DIRTY_BODY_REGEX, re.IGNORECASE)
181
- for each_review in all_flat:
182
- if not _review_matches_login(each_review, CURSOR_LOGIN_FILTER_SUBSTRING):
183
- continue
184
- if not _review_commit_starts_with(each_review, head_sha):
185
- continue
186
- body = each_review.get("body", "")
187
- if isinstance(body, str) and dirty_pattern.search(body):
188
- return False, "bugbot review body reports findings"
189
- return True, "clean"
190
- return True, "no bugbot review at HEAD"
191
-
192
-
193
- def _bot_review_state_detail(
194
- review: JsonObject,
195
- clean_states: ReviewStateGroup,
196
- dirty_states: ReviewStateGroup,
197
- ) -> tuple[bool, str]:
198
- """Return the pass flag and detail for one bot review's state."""
199
- review_state = review.get("state", "")
200
- commit_id = review.get("commit_id", "")
201
- short_commit = _short_sha(commit_id) if isinstance(commit_id, str) else "?"
202
- if review_state in clean_states:
203
- review_id = review.get("id", "?")
204
- return True, f"review {review_id}, state: {review_state}, commit {short_commit}"
205
- if review_state in dirty_states:
206
- return False, f"review state is '{review_state}' (dirty), commit {short_commit}"
207
- return False, f"review state is '{review_state}', commit {short_commit}"
208
-
209
-
210
- def _check_bot_review(
211
- *,
212
- owner: str,
213
- repo: str,
214
- number: int,
215
- head_sha: str,
216
- login_substring: str,
217
- clean_states: ReviewStateGroup,
218
- dirty_states: ReviewStateGroup,
219
- label: str,
220
- ) -> tuple[bool, str]:
221
- """Return whether the newest matching bot review on HEAD is in a clean state."""
222
- endpoint = GH_REVIEWS_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
223
- returncode, stdout = _gh_api_paginated(f"{endpoint}?per_page={REVIEWS_PER_PAGE}")
224
- if returncode != 0:
225
- return False, f"gh api error: {stdout}"
226
- all_flat = _flatten_paginated_reviews(stdout)
227
- if all_flat is None:
228
- return False, f"no {label} review found"
229
- for each_review in all_flat:
230
- if not _review_matches_login(each_review, login_substring):
231
- continue
232
- if not _review_commit_starts_with(each_review, head_sha):
233
- continue
234
- return _bot_review_state_detail(each_review, clean_states, dirty_states)
235
- return False, f"no {label} review found on {_short_sha(head_sha)}"
1
+ """Review and Bugbot IO leaves for the convergence gate checks.
2
+
3
+ Each function issues one ``gh`` REST call and returns a ``(passed, detail)``
4
+ pair the convergence checker turns into a PASS/FAIL line, or a raw
5
+ ``(returncode, stdout)`` pair for the low-level callers. The checker re-imports
6
+ these names, so a test patches them on the checker module.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ import subprocess
14
+ import sys
15
+
16
+ import _pr_converge_path_setup # noqa: F401
17
+ from pr_converge_skill_constants.constants import (
18
+ ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS,
19
+ BUGBOT_CHECK_RUN_NAME_SUBSTRING,
20
+ BUGBOT_DIRTY_BODY_REGEX,
21
+ CHECK_RUNS_PER_PAGE,
22
+ CURSOR_LOGIN_FILTER_SUBSTRING,
23
+ EXIT_CODE_GH_ERROR,
24
+ GH_CHECK_RUNS_PATH_TEMPLATE,
25
+ GH_PR_OBJECT_PATH_TEMPLATE,
26
+ GH_REVIEWS_PATH_TEMPLATE,
27
+ REVIEWS_PER_PAGE,
28
+ )
29
+ from pr_converge_scripts_constants.convergence_gate_constants import (
30
+ MERGEABLE_STATE_CLEAN,
31
+ REVIEW_SUBMITTED_AT_KEY,
32
+ SHORT_SHA_LENGTH,
33
+ )
34
+
35
+ JsonObject = dict[str, object]
36
+ ReviewStateGroup = tuple[str, ...]
37
+
38
+
39
+ def _run_gh_command(all_arguments: list[str]) -> tuple[int, str]:
40
+ completed_process = subprocess.run(
41
+ all_arguments,
42
+ capture_output=True,
43
+ text=True,
44
+ encoding="utf-8",
45
+ errors="replace",
46
+ check=False,
47
+ )
48
+ return completed_process.returncode, completed_process.stdout
49
+
50
+
51
+ def _gh_api(endpoint_path: str) -> tuple[int, str]:
52
+ return _run_gh_command(["gh", "api", endpoint_path])
53
+
54
+
55
+ def _gh_api_paginated(endpoint_path: str) -> tuple[int, str]:
56
+ return _run_gh_command(["gh", "api", endpoint_path, "--paginate", "--slurp"])
57
+
58
+
59
+ def _short_sha(commit_id: str) -> str:
60
+ return commit_id[:SHORT_SHA_LENGTH]
61
+
62
+
63
+ def _get_pr_head_sha(*, owner: str, repo: str, number: int) -> str:
64
+ """Return the PR HEAD SHA, exiting with the gh-error code when the call fails."""
65
+ endpoint = GH_PR_OBJECT_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
66
+ returncode, stdout = _gh_api(endpoint)
67
+ if returncode != 0:
68
+ sys.stderr.write(f"gh api error fetching PR object: {stdout}\n")
69
+ raise SystemExit(EXIT_CODE_GH_ERROR)
70
+ try:
71
+ pr_object = json.loads(stdout)
72
+ except json.JSONDecodeError as decode_error:
73
+ sys.stderr.write(f"gh api PR object not valid JSON: {stdout}\n")
74
+ raise SystemExit(EXIT_CODE_GH_ERROR) from decode_error
75
+ head_object = pr_object.get("head") if isinstance(pr_object, dict) else None
76
+ head_sha: object = head_object.get("sha") if isinstance(head_object, dict) else None
77
+ if not isinstance(head_sha, str):
78
+ raise SystemExit(EXIT_CODE_GH_ERROR)
79
+ return head_sha
80
+
81
+
82
+ def _evaluate_mergeable_from_pr_object(pr_object: JsonObject) -> tuple[bool, str]:
83
+ """Return whether a PR object dict is cleanly mergeable.
84
+
85
+ Pure evaluator: no network IO. Live and fixture paths share this contract.
86
+
87
+ Args:
88
+ pr_object: Pull request JSON object with mergeable and mergeable_state fields.
89
+
90
+ Returns:
91
+ (True, clean) when mergeable is true and state is clean; otherwise (False, state).
92
+ """
93
+ mergeable: object = pr_object.get("mergeable")
94
+ mergeable_state: object = pr_object.get("mergeable_state", "unknown")
95
+ state_text = str(mergeable_state)
96
+ if mergeable is True and state_text == MERGEABLE_STATE_CLEAN:
97
+ return True, MERGEABLE_STATE_CLEAN
98
+ return False, state_text
99
+
100
+
101
+ def _get_mergeable(*, owner: str, repo: str, number: int) -> tuple[bool, str]:
102
+ """Return whether the PR is cleanly mergeable, with the mergeable-state detail."""
103
+ endpoint = GH_PR_OBJECT_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
104
+ returncode, stdout = _gh_api(endpoint)
105
+ if returncode != 0:
106
+ return False, f"gh api error: {stdout}"
107
+ try:
108
+ pr_object = json.loads(stdout)
109
+ except json.JSONDecodeError:
110
+ return False, "gh api PR object not valid JSON"
111
+ if not isinstance(pr_object, dict):
112
+ return False, "unknown"
113
+ return _evaluate_mergeable_from_pr_object(pr_object)
114
+
115
+
116
+ def _bugbot_check_runs(
117
+ *, owner: str, repo: str, sha: str
118
+ ) -> tuple[int, list[JsonObject]]:
119
+ """Fetch the check runs for a SHA and return the returncode and run dicts."""
120
+ endpoint = GH_CHECK_RUNS_PATH_TEMPLATE.format(owner=owner, repo=repo, sha=sha)
121
+ returncode, stdout = _gh_api(f"{endpoint}?per_page={CHECK_RUNS_PER_PAGE}")
122
+ if returncode != 0:
123
+ return returncode, []
124
+ try:
125
+ payload = json.loads(stdout)
126
+ except json.JSONDecodeError:
127
+ return returncode, []
128
+ raw_runs = payload.get("check_runs") if isinstance(payload, dict) else None
129
+ if not isinstance(raw_runs, list):
130
+ return returncode, []
131
+ return returncode, [each_run for each_run in raw_runs if isinstance(each_run, dict)]
132
+
133
+
134
+ def _bugbot_run_conclusion_detail(check_run: JsonObject) -> tuple[bool, str]:
135
+ """Return the pass flag and detail for one bugbot check run."""
136
+ conclusion = check_run.get("conclusion", "")
137
+ if conclusion in ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS:
138
+ check_id = check_run.get("id", "?")
139
+ detail_url = check_run.get("html_url", "")
140
+ details_suffix = f" ({detail_url})" if detail_url else ""
141
+ return True, f"check run {check_id}, conclusion: {conclusion}{details_suffix}"
142
+ return (
143
+ False,
144
+ f"check run conclusion is '{conclusion}', expected {ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS}",
145
+ )
146
+
147
+
148
+ def _check_bugbot(*, owner: str, repo: str, sha: str) -> tuple[bool, str]:
149
+ """Return whether the Cursor Bugbot check run on a SHA completed successfully."""
150
+ returncode, check_runs = _bugbot_check_runs(owner=owner, repo=repo, sha=sha)
151
+ if returncode != 0:
152
+ return False, "gh api error fetching check runs"
153
+ for each_check_run in check_runs:
154
+ each_name = each_check_run.get("name", "")
155
+ if not isinstance(each_name, str):
156
+ continue
157
+ if BUGBOT_CHECK_RUN_NAME_SUBSTRING.lower() not in each_name.lower():
158
+ continue
159
+ return _bugbot_run_conclusion_detail(each_check_run)
160
+ return False, "no bugbot check run found"
161
+
162
+
163
+ def _sort_reviews_newest_first(all_reviews: list[JsonObject]) -> list[JsonObject]:
164
+ """Return the reviews ordered newest-first by submission time.
165
+
166
+ The live gh path and the offline fixture path both feed the same
167
+ newest-first evaluators, so both order their reviews through here.
168
+
169
+ Args:
170
+ all_reviews: Review objects in any order.
171
+
172
+ Returns:
173
+ A new list ordered newest-first; a review with no submitted_at sorts last.
174
+ """
175
+ return sorted(
176
+ all_reviews,
177
+ key=lambda each_review: str(each_review.get(REVIEW_SUBMITTED_AT_KEY, "")),
178
+ reverse=True,
179
+ )
180
+
181
+
182
+ def _flatten_paginated_reviews(stdout: str) -> list[JsonObject] | None:
183
+ """Flatten a slurped, paginated reviews payload into a newest-first list."""
184
+ try:
185
+ reviews_payload = json.loads(stdout)
186
+ except json.JSONDecodeError:
187
+ return None
188
+ if not isinstance(reviews_payload, list):
189
+ return None
190
+ all_flat: list[JsonObject] = [
191
+ each_entry
192
+ for each_page in reviews_payload
193
+ if isinstance(each_page, list)
194
+ for each_entry in each_page
195
+ if isinstance(each_entry, dict)
196
+ ]
197
+ return _sort_reviews_newest_first(all_flat)
198
+
199
+
200
+ def _review_matches_login(review: JsonObject, login_substring: str) -> bool:
201
+ user_object = review.get("user")
202
+ if not isinstance(user_object, dict):
203
+ return False
204
+ login = user_object.get("login", "")
205
+ return isinstance(login, str) and login_substring in login.lower()
206
+
207
+
208
+ def _review_commit_starts_with(review: JsonObject, head_sha: str) -> bool:
209
+ commit_id = review.get("commit_id", "")
210
+ return isinstance(commit_id, str) and commit_id.startswith(head_sha)
211
+
212
+
213
+ def _check_bugbot_not_dirty(
214
+ *, owner: str, repo: str, number: int, head_sha: str
215
+ ) -> tuple[bool, str]:
216
+ """Return whether the newest Cursor Bugbot review on HEAD reports no findings."""
217
+ endpoint = GH_REVIEWS_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
218
+ returncode, stdout = _gh_api_paginated(f"{endpoint}?per_page={REVIEWS_PER_PAGE}")
219
+ if returncode != 0:
220
+ return True, "bugbot reviews unavailable (non-fatal)"
221
+ all_flat = _flatten_paginated_reviews(stdout)
222
+ if all_flat is None:
223
+ return True, "bugbot reviews not valid JSON (non-fatal)"
224
+ dirty_pattern = re.compile(BUGBOT_DIRTY_BODY_REGEX, re.IGNORECASE)
225
+ for each_review in all_flat:
226
+ if not _review_matches_login(each_review, CURSOR_LOGIN_FILTER_SUBSTRING):
227
+ continue
228
+ if not _review_commit_starts_with(each_review, head_sha):
229
+ continue
230
+ body = each_review.get("body", "")
231
+ if isinstance(body, str) and dirty_pattern.search(body):
232
+ return False, "bugbot review body reports findings"
233
+ return True, "clean"
234
+ return True, "no bugbot review at HEAD"
235
+
236
+
237
+ def _bot_review_state_detail(
238
+ review: JsonObject,
239
+ clean_states: ReviewStateGroup,
240
+ dirty_states: ReviewStateGroup,
241
+ ) -> tuple[bool, str]:
242
+ """Return the pass flag and detail for one bot review's state."""
243
+ review_state = review.get("state", "")
244
+ commit_id = review.get("commit_id", "")
245
+ short_commit = _short_sha(commit_id) if isinstance(commit_id, str) else "?"
246
+ if review_state in clean_states:
247
+ review_id = review.get("id", "?")
248
+ return True, f"review {review_id}, state: {review_state}, commit {short_commit}"
249
+ if review_state in dirty_states:
250
+ return False, f"review state is '{review_state}' (dirty), commit {short_commit}"
251
+ return False, f"review state is '{review_state}', commit {short_commit}"
252
+
253
+
254
+ def _check_bot_review(
255
+ *,
256
+ owner: str,
257
+ repo: str,
258
+ number: int,
259
+ head_sha: str,
260
+ login_substring: str,
261
+ clean_states: ReviewStateGroup,
262
+ dirty_states: ReviewStateGroup,
263
+ label: str,
264
+ ) -> tuple[bool, str]:
265
+ """Return whether the newest matching bot review on HEAD is in a clean state."""
266
+ endpoint = GH_REVIEWS_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
267
+ returncode, stdout = _gh_api_paginated(f"{endpoint}?per_page={REVIEWS_PER_PAGE}")
268
+ if returncode != 0:
269
+ return False, f"gh api error: {stdout}"
270
+ all_flat = _flatten_paginated_reviews(stdout)
271
+ if all_flat is None:
272
+ return False, f"no {label} review found"
273
+ for each_review in all_flat:
274
+ if not _review_matches_login(each_review, login_substring):
275
+ continue
276
+ if not _review_commit_starts_with(each_review, head_sha):
277
+ continue
278
+ return _bot_review_state_detail(each_review, clean_states, dirty_states)
279
+ return False, f"no {label} review found on {_short_sha(head_sha)}"
@@ -207,7 +207,7 @@ def _check_no_pending_reviews(
207
207
  try:
208
208
  pending_payload = json.loads(stdout)
209
209
  except json.JSONDecodeError:
210
- return True, "no pending (empty response)"
210
+ return False, "pending-reviewers response not valid JSON"
211
211
  copilot_pending = _pending_copilot_logins(
212
212
  _requested_reviewer_users(pending_payload)
213
213
  )
@@ -1,11 +1,14 @@
1
1
  """Named constants for the convergence gate IO leaves.
2
2
 
3
- Holds the short-SHA length, the join separators for gate detail strings, and
4
- the GraphQL query that lists a PR's review threads.
3
+ Holds the short-SHA length, the review field key the newest-first sort reads,
4
+ the join separators for gate detail strings, the fixture payload keys, and the
5
+ GraphQL query that lists a PR's review threads.
5
6
  """
6
7
 
7
8
  SHORT_SHA_LENGTH: int = 7
8
9
 
10
+ REVIEW_SUBMITTED_AT_KEY: str = "submitted_at"
11
+
9
12
  THREAD_PATH_JOIN_SEPARATOR: str = "; "
10
13
 
11
14
  PENDING_REVIEWER_JOIN_SEPARATOR: str = ", "
@@ -34,3 +37,34 @@ query($owner: String!, $repo: String!, $number: Int!, $first: Int!, $cursor: Str
34
37
  }
35
38
  }
36
39
  """
40
+
41
+ FIXTURE_KEY_HEAD_SHA: str = "head_sha"
42
+ FIXTURE_KEY_PR_OBJECT: str = "pr_object"
43
+ FIXTURE_KEY_REVIEWS: str = "reviews"
44
+ FIXTURE_KEY_UNRESOLVED_BOT_THREADS_PASSED: str = "unresolved_bot_threads_passed"
45
+ FIXTURE_KEY_UNRESOLVED_BOT_THREADS_DETAIL: str = "unresolved_bot_threads_detail"
46
+ FIXTURE_KEY_PENDING_REVIEWS_PASSED: str = "pending_reviews_passed"
47
+ FIXTURE_KEY_PENDING_REVIEWS_DETAIL: str = "pending_reviews_detail"
48
+ FIXTURE_KEY_CODEX_PERCENT_LEFT: str = "codex_percent_left"
49
+ FIXTURE_KEY_CODEX_CLEAN_AT: str = "codex_clean_at"
50
+ FIXTURE_DEFAULT_THREADS_DETAIL: str = "0 unresolved"
51
+ FIXTURE_DEFAULT_PENDING_DETAIL: str = "none pending"
52
+ MERGEABLE_STATE_CLEAN: str = "clean"
53
+
54
+ COPILOT_DOWN_BYPASS_NOTE: str = "copilot_down"
55
+ BUGBOT_DOWN_BYPASS_NOTE: str = "bugbot_down"
56
+ REVIEWER_UNAVAILABLE_NOTE_TEMPLATE: str = "{token} unavailable: {message}"
57
+ SETTINGS_DISK_FALLBACK_LOG_TEMPLATE: str = (
58
+ "reviewer-availability: settings.json unreadable; using process env for %s and %s"
59
+ )
60
+
61
+ CODEX_GATE_LABEL: str = "codex_clean_at == current_head"
62
+ CODEX_BYPASS_DETAIL: str = "bypassed (codex_down)"
63
+ CODEX_SKIPPED_USAGE_DETAIL: str = "skipped (codex review not required)"
64
+ CODEX_CLEAN_DETAIL_TEMPLATE: str = "clean at %s"
65
+ CODEX_MISSING_CLEAN_DETAIL_TEMPLATE: str = "no codex clean on %s"
66
+ CLAUDE_JOB_DIR_ENV_VAR_NAME: str = "CLAUDE_JOB_DIR"
67
+ PR_CONVERGE_STATE_FILENAME: str = "pr-converge-state.json"
68
+ CODEX_CLEAN_AT_STATE_KEY: str = "codex_clean_at"
69
+ CODEX_DOWN_STATE_KEY: str = "codex_down"
70
+ MINIMUM_ABBREVIATED_SHA_LENGTH: int = 7
@@ -22,5 +22,9 @@ def test_path_setup_registers_skill_and_shared_directories() -> None:
22
22
  shared_directory = (
23
23
  _SCRIPTS_DIRECTORY.parent.parent.parent / "_shared" / "pr-loop" / "scripts"
24
24
  )
25
+ codex_scripts_directory = (
26
+ _SCRIPTS_DIRECTORY.parent.parent / "codex-review" / "scripts"
27
+ )
25
28
  assert str(skill_directory) in sys.path
26
29
  assert str(shared_directory) in sys.path
30
+ assert str(codex_scripts_directory) in sys.path