claude-dev-env 1.78.0 → 1.80.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 (63) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  5. package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
  6. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  7. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  8. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  9. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
  10. package/bin/install.mjs +1 -0
  11. package/bin/install.test.mjs +3 -2
  12. package/hooks/blocking/CLAUDE.md +3 -2
  13. package/hooks/blocking/code_rules_docstrings.py +609 -16
  14. package/hooks/blocking/code_rules_enforcer.py +41 -0
  15. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  16. package/hooks/blocking/code_rules_naming_collection.py +76 -1
  17. package/hooks/blocking/code_rules_paired_test.py +240 -22
  18. package/hooks/blocking/code_rules_test_assertions.py +159 -1
  19. package/hooks/blocking/code_rules_unused_imports.py +7 -63
  20. package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
  21. package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
  22. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
  23. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
  24. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
  25. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
  26. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  27. package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
  28. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
  29. package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
  31. package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
  32. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  33. package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
  34. package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
  36. package/hooks/hooks_constants/CLAUDE.md +1 -0
  37. package/hooks/hooks_constants/blocking_check_limits.py +79 -0
  38. package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
  39. package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
  40. package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
  41. package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  43. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  44. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  45. package/package.json +1 -1
  46. package/rules/CLAUDE.md +1 -0
  47. package/rules/docstring-prose-matches-implementation.md +57 -54
  48. package/rules/env-var-table-code-drift.md +24 -0
  49. package/rules/package-inventory-stale-entry.md +4 -4
  50. package/rules/paired-test-coverage.md +12 -5
  51. package/skills/autoconverge/SKILL.md +26 -5
  52. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  53. package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
  54. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
  55. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  56. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  57. package/skills/autoconverge/workflow/converge.mjs +110 -228
  58. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  59. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  60. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  61. package/skills/pr-converge/SKILL.md +28 -2
  62. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  63. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -13,6 +13,7 @@ Python scripts invoked at runtime by the PR-loop skills. Each script is a standa
13
13
  | `revoke_project_claude_permissions.py` | Removes the allow-rules and entries that `grant_project_claude_permissions.py` wrote; safe to run when no prior grant exists |
14
14
  | `code_rules_gate.py` | Pre-commit gate that runs `code_rules_enforcer` checks on staged Python files before a fix commit lands |
15
15
  | `reviews_disabled.py` | Shared helper for the `CLAUDE_REVIEWS_DISABLED` opt-out gate; parses the env-var token to find which reviewer types are suppressed |
16
+ | `copilot_quota.py` | Copilot premium-request quota pre-check: resolves a configured GitHub account, reads its remaining `premium_interactions` quota via `gh api copilot_internal/user`, and exits 0 (run Copilot) or non-zero (skip: out of quota, API down, or no account configured) |
16
17
  | `fix_hookspath.py` | Repairs a malformed `core.hooksPath` global git config entry |
17
18
  | `_claude_permissions_common.py` | Internal helpers shared by the grant/revoke scripts: atomic settings.json writes, list mutation, path helpers |
18
19
 
@@ -0,0 +1,360 @@
1
+ """Copilot premium-interaction quota pre-check.
2
+
3
+ Run this before any Copilot review step spawns. It reads a configured GitHub
4
+ account's remaining premium-interaction quota and decides whether Copilot has
5
+ quota to run. An account already out of quota skips the Copilot step instead of
6
+ wasting a whole review cycle on it.
7
+
8
+ Account resolution order: the ``--account`` flag, then the
9
+ ``COPILOT_QUOTA_ACCOUNT`` environment variable, then that same key parsed from a
10
+ git-ignored ``.env`` file. The account's ``gh`` token is resolved with
11
+ ``gh auth token -u <account>`` and the quota is read from
12
+ ``gh api copilot_internal/user``. The config file names the account only and
13
+ stores no secret.
14
+
15
+ The exit code tells the caller what to do. Exit 0 means run Copilot. A non-zero
16
+ exit means skip it: the account is out of quota (scenario A), the quota API or
17
+ account access is down (scenario B), or no account is configured (scenario C).
18
+ Every path prints one line: the run line to stdout, each skip line to stderr.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import subprocess
27
+ import sys
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+
31
+ from pr_loop_shared_constants.copilot_quota_constants import (
32
+ COPILOT_INTERNAL_USER_API_PATH,
33
+ COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME,
34
+ COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH,
35
+ EXIT_CODE_NO_ACCOUNT_CONFIGURED,
36
+ EXIT_CODE_OUT_OF_QUOTA,
37
+ EXIT_CODE_QUOTA_API_DOWN,
38
+ EXIT_CODE_QUOTA_AVAILABLE,
39
+ GH_TOKEN_ENV_VAR_NAME,
40
+ PREMIUM_ENTITLEMENT_FIELD_NAME,
41
+ PREMIUM_INTERACTIONS_FIELD_NAME,
42
+ PREMIUM_OVERAGE_PERMITTED_FIELD_NAME,
43
+ PREMIUM_PERCENT_REMAINING_FIELD_NAME,
44
+ PREMIUM_REMAINING_FIELD_NAME,
45
+ PREMIUM_UNLIMITED_FIELD_NAME,
46
+ QUOTA_SNAPSHOTS_FIELD_NAME,
47
+ )
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class QuotaDecision:
52
+ """One quota pre-check outcome: the exit code and the single log line.
53
+
54
+ The exit code tells the caller what to do, where 0 runs Copilot and any
55
+ non-zero skips it. The CLI prints the message on one line, to stdout on a
56
+ run and to stderr on a skip.
57
+ """
58
+
59
+ exit_code: int
60
+ message: str
61
+
62
+
63
+ def _run_gh(
64
+ all_command_arguments: list[str],
65
+ all_environment_overrides: dict[str, str] | None = None,
66
+ ) -> tuple[int, str]:
67
+ """Run a ``gh`` subprocess and return its exit code and stdout.
68
+
69
+ Args:
70
+ all_command_arguments: Arguments after the ``gh`` program name, for
71
+ example ``["api", "copilot_internal/user"]``.
72
+ all_environment_overrides: Environment entries to overlay on the current
73
+ environment for this call, or None to inherit it unchanged.
74
+
75
+ Returns:
76
+ The subprocess exit code paired with its captured stdout.
77
+ """
78
+ process_environment = dict(os.environ)
79
+ if all_environment_overrides:
80
+ process_environment.update(all_environment_overrides)
81
+ completed_process = subprocess.run(
82
+ ["gh", *all_command_arguments],
83
+ capture_output=True,
84
+ text=True,
85
+ encoding="utf-8",
86
+ errors="replace",
87
+ check=False,
88
+ env=process_environment,
89
+ )
90
+ return completed_process.returncode, completed_process.stdout
91
+
92
+
93
+ def _read_account_from_env_file(env_file_path: Path) -> str | None:
94
+ """Read the configured account from a git-ignored ``.env`` file.
95
+
96
+ Args:
97
+ env_file_path: Path to the ``.env`` file that may carry a
98
+ ``COPILOT_QUOTA_ACCOUNT=<login>`` line.
99
+
100
+ Returns:
101
+ The configured account login, or None when the file is absent,
102
+ unreadable, or carries no non-empty account line.
103
+ """
104
+ try:
105
+ file_text = env_file_path.read_text(encoding="utf-8")
106
+ except (OSError, UnicodeDecodeError):
107
+ return None
108
+ for each_line in file_text.splitlines():
109
+ stripped_line = each_line.strip()
110
+ if not stripped_line or stripped_line.startswith("#"):
111
+ continue
112
+ key_text, separator, raw_account = stripped_line.partition("=")
113
+ if not separator or key_text.strip() != COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME:
114
+ continue
115
+ resolved_login = raw_account.strip().strip('"').strip("'")
116
+ if resolved_login:
117
+ return resolved_login
118
+ return None
119
+
120
+
121
+ def _resolve_account(cli_account: str | None, env_file_path: Path) -> str | None:
122
+ """Resolve the account whose Copilot quota to check.
123
+
124
+ Args:
125
+ cli_account: The ``--account`` value, or None when the flag is absent.
126
+ env_file_path: The ``.env`` file consulted when neither the flag nor
127
+ the environment variable names an account.
128
+
129
+ Returns:
130
+ The resolved account login from the flag, then the
131
+ ``COPILOT_QUOTA_ACCOUNT`` environment variable, then the ``.env``
132
+ file, or None when none of the three names an account.
133
+ """
134
+ if cli_account and cli_account.strip():
135
+ return cli_account.strip()
136
+ environment_account = os.environ.get(COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME, "").strip()
137
+ if environment_account:
138
+ return environment_account
139
+ return _read_account_from_env_file(env_file_path)
140
+
141
+
142
+ def _resolve_gh_token(account_login: str) -> str | None:
143
+ """Resolve the account's gh token via ``gh auth token -u <account>``.
144
+
145
+ Args:
146
+ account_login: The GitHub login to resolve a token for.
147
+
148
+ Returns:
149
+ The token string, or None when ``gh`` exits non-zero or prints no
150
+ token, so an account that is not ``gh auth login``-ed reads as down.
151
+ """
152
+ returncode, stdout = _run_gh(["auth", "token", "-u", account_login])
153
+ if returncode != 0:
154
+ return None
155
+ token = stdout.strip()
156
+ return token or None
157
+
158
+
159
+ def _fetch_copilot_user(token: str) -> dict[str, object] | None:
160
+ """Read the ``copilot_internal/user`` payload for a resolved token.
161
+
162
+ Args:
163
+ token: A gh token authorized for the account under check.
164
+
165
+ Returns:
166
+ The parsed response object, or None when ``gh`` exits non-zero or
167
+ returns a body that is not a JSON object.
168
+ """
169
+ returncode, stdout = _run_gh(
170
+ ["api", COPILOT_INTERNAL_USER_API_PATH],
171
+ {GH_TOKEN_ENV_VAR_NAME: token},
172
+ )
173
+ if returncode != 0:
174
+ return None
175
+ try:
176
+ parsed_payload = json.loads(stdout)
177
+ except json.JSONDecodeError:
178
+ return None
179
+ if not isinstance(parsed_payload, dict):
180
+ return None
181
+ return parsed_payload
182
+
183
+
184
+ def _extract_premium_snapshot(
185
+ all_user_fields: dict[str, object],
186
+ ) -> dict[str, object] | None:
187
+ """Pull the ``premium_interactions`` quota snapshot from the user payload.
188
+
189
+ Args:
190
+ all_user_fields: The parsed ``copilot_internal/user`` response object.
191
+
192
+ Returns:
193
+ The ``quota_snapshots.premium_interactions`` object, or None when
194
+ either level is missing or is not an object.
195
+ """
196
+ quota_snapshots = all_user_fields.get(QUOTA_SNAPSHOTS_FIELD_NAME)
197
+ if not isinstance(quota_snapshots, dict):
198
+ return None
199
+ premium_snapshot = quota_snapshots.get(PREMIUM_INTERACTIONS_FIELD_NAME)
200
+ if not isinstance(premium_snapshot, dict):
201
+ return None
202
+ return premium_snapshot
203
+
204
+
205
+ def _is_premium_quota_exhausted(all_premium_fields: dict[str, object]) -> bool:
206
+ """Report whether the premium-interaction quota is exhausted.
207
+
208
+ The quota counts as exhausted when it is not unlimited, its remaining count
209
+ has reached zero, and overage is not permitted. That is the one state in
210
+ which a Copilot review request cannot be served. Any missing or malformed
211
+ gating field leaves the quota reported as not exhausted, so the caller runs
212
+ Copilot rather than skipping on an unreadable snapshot.
213
+
214
+ Args:
215
+ all_premium_fields: The ``premium_interactions`` quota snapshot object.
216
+
217
+ Returns:
218
+ True when unlimited is False, remaining is at or below zero, and overage
219
+ is not permitted.
220
+ """
221
+ is_unlimited = all_premium_fields.get(PREMIUM_UNLIMITED_FIELD_NAME)
222
+ is_overage_permitted = all_premium_fields.get(PREMIUM_OVERAGE_PERMITTED_FIELD_NAME)
223
+ remaining = all_premium_fields.get(PREMIUM_REMAINING_FIELD_NAME)
224
+ if not isinstance(is_unlimited, bool) or not isinstance(is_overage_permitted, bool):
225
+ return False
226
+ if isinstance(remaining, bool) or not isinstance(remaining, (int, float)):
227
+ return False
228
+ return is_unlimited is False and remaining <= 0 and is_overage_permitted is False
229
+
230
+
231
+ def _describe_quota_amount(all_premium_fields: dict[str, object]) -> str:
232
+ """Render the remaining/entitlement/percent fragment for a snapshot.
233
+
234
+ Args:
235
+ all_premium_fields: The ``premium_interactions`` quota snapshot object.
236
+
237
+ Returns:
238
+ A fragment naming the remaining count over the entitlement with the
239
+ percent remaining, built from the snapshot's remaining, entitlement,
240
+ and percent-remaining fields.
241
+ """
242
+ remaining = all_premium_fields.get(PREMIUM_REMAINING_FIELD_NAME)
243
+ entitlement = all_premium_fields.get(PREMIUM_ENTITLEMENT_FIELD_NAME)
244
+ percent_remaining = all_premium_fields.get(PREMIUM_PERCENT_REMAINING_FIELD_NAME)
245
+ return f"{remaining}/{entitlement} remaining ({percent_remaining}%)"
246
+
247
+
248
+ def evaluate_copilot_quota(
249
+ cli_account: str | None, env_file_path: Path
250
+ ) -> QuotaDecision:
251
+ """Decide whether Copilot has premium quota to run for a configured account.
252
+
253
+ Resolves the account, reads its remaining ``premium_interactions`` quota via
254
+ ``gh``, and maps the result to one of four outcomes: premium quota available
255
+ (run Copilot), out of quota, quota API or account access down, and no
256
+ account configured.
257
+
258
+ Args:
259
+ cli_account: The ``--account`` value, or None when the flag is absent.
260
+ env_file_path: The ``.env`` file consulted for the account when neither
261
+ the flag nor the environment variable names one.
262
+
263
+ Returns:
264
+ A QuotaDecision carrying the exit code and the single log line for the
265
+ resolved outcome.
266
+ """
267
+ account_login = _resolve_account(cli_account, env_file_path)
268
+ if account_login is None:
269
+ return QuotaDecision(
270
+ EXIT_CODE_NO_ACCOUNT_CONFIGURED,
271
+ f"copilot-quota: no account configured — set "
272
+ f"{COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME}=<login> in {env_file_path} "
273
+ f"(or pass --account). Skipping Copilot (scenario C).",
274
+ )
275
+ token = _resolve_gh_token(account_login)
276
+ if token is None:
277
+ return QuotaDecision(
278
+ EXIT_CODE_QUOTA_API_DOWN,
279
+ f"copilot-quota: could not resolve a gh token for '{account_login}' "
280
+ f"(is it gh auth login-ed?) — skipping Copilot (scenario B).",
281
+ )
282
+ all_user_fields = _fetch_copilot_user(token)
283
+ if all_user_fields is None:
284
+ return QuotaDecision(
285
+ EXIT_CODE_QUOTA_API_DOWN,
286
+ f"copilot-quota: gh api {COPILOT_INTERNAL_USER_API_PATH} failed or "
287
+ f"returned non-JSON for '{account_login}' — skipping Copilot "
288
+ f"(scenario B).",
289
+ )
290
+ premium_snapshot = _extract_premium_snapshot(all_user_fields)
291
+ if premium_snapshot is None:
292
+ return QuotaDecision(
293
+ EXIT_CODE_QUOTA_API_DOWN,
294
+ f"copilot-quota: the {COPILOT_INTERNAL_USER_API_PATH} response for "
295
+ f"'{account_login}' carried no "
296
+ f"{QUOTA_SNAPSHOTS_FIELD_NAME}.{PREMIUM_INTERACTIONS_FIELD_NAME} — "
297
+ f"skipping Copilot (scenario B).",
298
+ )
299
+ if _is_premium_quota_exhausted(premium_snapshot):
300
+ return QuotaDecision(
301
+ EXIT_CODE_OUT_OF_QUOTA,
302
+ f"copilot-quota: {account_login} is out of premium-interaction "
303
+ f"quota — {_describe_quota_amount(premium_snapshot)}, overage not "
304
+ f"permitted — skipping Copilot (scenario A).",
305
+ )
306
+ return QuotaDecision(
307
+ EXIT_CODE_QUOTA_AVAILABLE,
308
+ f"copilot-quota: {account_login} — premium interactions "
309
+ f"{_describe_quota_amount(premium_snapshot)} — running Copilot.",
310
+ )
311
+
312
+
313
+ def _parse_arguments(all_argv: list[str]) -> argparse.Namespace:
314
+ """Parse command-line arguments for the quota pre-check.
315
+
316
+ Args:
317
+ all_argv: Argument list excluding the program name, typically
318
+ ``sys.argv[1:]``.
319
+
320
+ Returns:
321
+ Namespace exposing an ``account`` attribute holding the ``--account``
322
+ value, or None when the flag is absent.
323
+ """
324
+ parser = argparse.ArgumentParser(description=__doc__)
325
+ parser.add_argument(
326
+ "--account",
327
+ default=None,
328
+ help=(
329
+ "GitHub login whose Copilot premium-interaction quota to check; "
330
+ "overrides COPILOT_QUOTA_ACCOUNT and the .env file"
331
+ ),
332
+ )
333
+ return parser.parse_args(all_argv)
334
+
335
+
336
+ def main(all_arguments: list[str]) -> int:
337
+ """Run the quota pre-check end-to-end and print its one-line decision.
338
+
339
+ Args:
340
+ all_arguments: Argument list excluding the program name.
341
+
342
+ Returns:
343
+ The pre-check exit code: 0 to run Copilot, or a non-zero skip code for
344
+ out-of-quota (scenario A), quota API or access down (scenario B), and no
345
+ account configured (scenario C).
346
+ """
347
+ arguments = _parse_arguments(all_arguments)
348
+ decision = evaluate_copilot_quota(
349
+ cli_account=arguments.account,
350
+ env_file_path=COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH,
351
+ )
352
+ message_stream = (
353
+ sys.stdout if decision.exit_code == EXIT_CODE_QUOTA_AVAILABLE else sys.stderr
354
+ )
355
+ print(decision.message, file=message_stream)
356
+ return decision.exit_code
357
+
358
+
359
+ if __name__ == "__main__":
360
+ raise SystemExit(main(sys.argv[1:]))
@@ -15,6 +15,7 @@ Named constants for every script in `_shared/pr-loop/scripts/`. Each module owns
15
15
  | `preflight_constants.py` | `preflight.py` — env-var names, git subcommands, pytest exit codes, test discovery patterns |
16
16
  | `preflight_self_heal_constants.py` | `preflight_self_heal.py` — git config keys and local-scope detection strings |
17
17
  | `reviews_disabled_constants.py` | `reviews_disabled.py` — `CLAUDE_REVIEWS_DISABLED` token taxonomy |
18
+ | `copilot_quota_constants.py` | `copilot_quota.py` — `COPILOT_QUOTA_ACCOUNT` env-var name, `copilot_internal/user` API path, the `premium_interactions` gating field names, the four skip/run exit codes, and the default `.env` path |
18
19
  | `__init__.py` | Empty package marker |
19
20
 
20
21
  ## Convention
@@ -0,0 +1,24 @@
1
+ """Configuration constants for the Copilot premium-interaction quota pre-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME: str = "COPILOT_QUOTA_ACCOUNT"
8
+ GH_TOKEN_ENV_VAR_NAME: str = "GH_TOKEN"
9
+ COPILOT_INTERNAL_USER_API_PATH: str = "copilot_internal/user"
10
+
11
+ QUOTA_SNAPSHOTS_FIELD_NAME: str = "quota_snapshots"
12
+ PREMIUM_INTERACTIONS_FIELD_NAME: str = "premium_interactions"
13
+ PREMIUM_UNLIMITED_FIELD_NAME: str = "unlimited"
14
+ PREMIUM_REMAINING_FIELD_NAME: str = "remaining"
15
+ PREMIUM_OVERAGE_PERMITTED_FIELD_NAME: str = "overage_permitted"
16
+ PREMIUM_ENTITLEMENT_FIELD_NAME: str = "entitlement"
17
+ PREMIUM_PERCENT_REMAINING_FIELD_NAME: str = "percent_remaining"
18
+
19
+ EXIT_CODE_QUOTA_AVAILABLE: int = 0
20
+ EXIT_CODE_OUT_OF_QUOTA: int = 1
21
+ EXIT_CODE_QUOTA_API_DOWN: int = 2
22
+ EXIT_CODE_NO_ACCOUNT_CONFIGURED: int = 3
23
+
24
+ COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH: Path = Path(__file__).resolve().parents[4] / ".env"
@@ -21,10 +21,17 @@ pytest suite for the scripts and constants in `_shared/pr-loop/scripts/`. Each t
21
21
  | `test_preflight_constants.py` | `pr_loop_shared_constants/preflight_constants.py` |
22
22
  | `test_preflight_self_heal.py` | `preflight_self_heal.py` hooks-path repair |
23
23
  | `test_reviews_disabled.py` | `reviews_disabled.py` opt-out gate parsing |
24
+ | `test_copilot_quota.py` | `copilot_quota.py` end-to-end: account resolution, premium-quota classification, exit codes, and skip logging |
25
+ | `test_copilot_quota_constants.py` | `pr_loop_shared_constants/copilot_quota_constants.py` |
24
26
  | `test_revoke_project_claude_permissions.py` | `revoke_project_claude_permissions.py` end-to-end |
25
27
  | `test_agent_config_carveout.py` | Agent-config deny-rule carve-out logic |
26
28
  | `conftest.py` | Shared pytest fixtures |
27
29
 
30
+ ## Fixtures
31
+
32
+ `fixtures/copilot_internal_user_jonecho.json` — a captured `gh api
33
+ copilot_internal/user` response driving `test_copilot_quota.py`.
34
+
28
35
  ## Running
29
36
 
30
37
  ```bash
@@ -0,0 +1,76 @@
1
+ {
2
+ "login": "jonecho",
3
+ "access_type_sku": "plus_monthly_subscriber_quota",
4
+ "assigned_date": "2026-05-09T14:08:29-04:00",
5
+ "can_signup_for_limited": false,
6
+ "chat_enabled": true,
7
+ "cli_enabled": true,
8
+ "copilotignore_enabled": false,
9
+ "copilot_plan": "individual_pro",
10
+ "editor_preview_features_enabled": true,
11
+ "is_mcp_enabled": true,
12
+ "is_staff": false,
13
+ "organization_login_list": [],
14
+ "organization_list": [],
15
+ "restricted_telemetry": false,
16
+ "cli_remote_control_enabled": true,
17
+ "cloud_session_storage_enabled": true,
18
+ "endpoints": {
19
+ "api": "https://api.individual.githubcopilot.com",
20
+ "origin-tracker": "https://origin-tracker.individual.githubcopilot.com",
21
+ "proxy": "https://proxy.individual.githubcopilot.com",
22
+ "telemetry": "https://telemetry.individual.githubcopilot.com"
23
+ },
24
+ "can_upgrade_plan": true,
25
+ "codex_agent_enabled": true,
26
+ "quota_reset_date": "2026-08-01",
27
+ "quota_snapshots": {
28
+ "chat": {
29
+ "overage_count": 0,
30
+ "overage_entitlement": 0,
31
+ "overage_permitted": false,
32
+ "percent_remaining": 100.0,
33
+ "quota_id": "chat",
34
+ "quota_remaining": 0.0,
35
+ "unlimited": true,
36
+ "timestamp_utc": "2026-07-01T11:05:17.669Z",
37
+ "has_quota": true,
38
+ "quota_reset_at": 0,
39
+ "token_based_billing": true,
40
+ "remaining": 0,
41
+ "entitlement": 0
42
+ },
43
+ "completions": {
44
+ "overage_count": 0,
45
+ "overage_entitlement": 0,
46
+ "overage_permitted": false,
47
+ "percent_remaining": 100.0,
48
+ "quota_id": "completions",
49
+ "quota_remaining": 0.0,
50
+ "unlimited": true,
51
+ "timestamp_utc": "2026-07-01T11:05:17.669Z",
52
+ "has_quota": true,
53
+ "quota_reset_at": 0,
54
+ "token_based_billing": true,
55
+ "remaining": 0,
56
+ "entitlement": 0
57
+ },
58
+ "premium_interactions": {
59
+ "overage_count": 0,
60
+ "overage_entitlement": 0,
61
+ "overage_permitted": false,
62
+ "percent_remaining": 97.3,
63
+ "quota_id": "premium_interactions",
64
+ "quota_remaining": 6817.0,
65
+ "unlimited": false,
66
+ "timestamp_utc": "2026-07-01T11:05:17.669Z",
67
+ "has_quota": true,
68
+ "quota_reset_at": 0,
69
+ "token_based_billing": true,
70
+ "remaining": 6817,
71
+ "entitlement": 7000
72
+ }
73
+ },
74
+ "quota_reset_date_utc": "2026-08-01T00:00:00.000Z",
75
+ "token_based_billing": true
76
+ }