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
@@ -79,3 +79,14 @@ test('the top-level run guards an unusable input into a structured blocker befor
79
79
  assert.match(guardBlock, /converged: false/);
80
80
  assert.match(guardBlock, /return/);
81
81
  });
82
+
83
+ test('classifyRunInput passes copilotDisabled through alongside the coordinates', () => {
84
+ const classified = classifyRunInput({ ...VALID_COORDINATES, copilotDisabled: true });
85
+ assert.equal(classified.blocker, null);
86
+ assert.equal(classified.input.copilotDisabled, true);
87
+ });
88
+
89
+ test('classifyRunInput leaves copilotDisabled undefined when the input omits it', () => {
90
+ const classified = classifyRunInput(VALID_COORDINATES);
91
+ assert.equal(classified.input.copilotDisabled, undefined);
92
+ });
@@ -102,6 +102,28 @@ function classifyMultiInput(rawArgs) {
102
102
  return { input: candidate, blocker: null }
103
103
  }
104
104
 
105
+ /**
106
+ * Build the converge.mjs child-run args for one validated PR entry.
107
+ *
108
+ * Every per-run opt-out the child converge.mjs reads is forwarded here so the
109
+ * fan-out honors it: bugbotDisabled skips the Bugbot phase, and copilotDisabled
110
+ * short-circuits the Copilot quota gate when the account is out of premium
111
+ * requests. An entry that omits an opt-out defaults it to false so the child
112
+ * runs that phase.
113
+ * @param {object} prEntry one validated element of the args.prs array
114
+ * @returns {object} the args object passed to the converge.mjs child run
115
+ */
116
+ function childRunInput(prEntry) {
117
+ return {
118
+ owner: prEntry.owner,
119
+ repo: prEntry.repo,
120
+ prNumber: prEntry.prNumber,
121
+ repoPath: prEntry.repoPath,
122
+ bugbotDisabled: Boolean(prEntry.bugbotDisabled),
123
+ copilotDisabled: Boolean(prEntry.copilotDisabled),
124
+ }
125
+ }
126
+
105
127
  const multiInput = classifyMultiInput(args)
106
128
  if (multiInput.blocker) {
107
129
  return { converged: false, prCount: 0, convergedCount: 0, results: [], blocker: multiInput.blocker }
@@ -115,13 +137,7 @@ const childResults = await parallel(
115
137
  input.prs.map((eachPr) => async () => {
116
138
  const childOutcome = await workflow(
117
139
  { scriptPath: input.convergeScriptPath },
118
- {
119
- owner: eachPr.owner,
120
- repo: eachPr.repo,
121
- prNumber: eachPr.prNumber,
122
- repoPath: eachPr.repoPath,
123
- bugbotDisabled: Boolean(eachPr.bugbotDisabled),
124
- },
140
+ childRunInput(eachPr),
125
141
  )
126
142
  return {
127
143
  owner: eachPr.owner,
@@ -17,9 +17,9 @@ function sourceSliceBetween(startNeedle, endNeedle) {
17
17
 
18
18
  const productionModule = new Function(
19
19
  `${sourceSliceBetween('function normalizeMultiInput(', '\nconst multiInput =')}\n` +
20
- 'return { normalizeMultiInput, isUsablePrEntry, classifyMultiInput };',
20
+ 'return { normalizeMultiInput, isUsablePrEntry, classifyMultiInput, childRunInput };',
21
21
  )();
22
- const { normalizeMultiInput, classifyMultiInput } = productionModule;
22
+ const { normalizeMultiInput, classifyMultiInput, childRunInput } = productionModule;
23
23
 
24
24
  const SCRIPT_PATH = '/abs/skills/autoconverge/workflow/converge.mjs';
25
25
 
@@ -98,3 +98,29 @@ test('a null payload is blocked', () => {
98
98
  assert.equal(classified.input, null);
99
99
  assert.match(classified.blocker, /did not parse/);
100
100
  });
101
+
102
+ test('childRunInput forwards the PR coordinates to the child run', () => {
103
+ const childArgs = childRunInput(validEntry(398));
104
+ assert.equal(childArgs.owner, 'JonEcho');
105
+ assert.equal(childArgs.repo, 'python-automation');
106
+ assert.equal(childArgs.prNumber, 398);
107
+ assert.equal(childArgs.repoPath, '/worktrees/pr-398');
108
+ });
109
+
110
+ test('childRunInput forwards copilotDisabled true when the entry opts out', () => {
111
+ const optedOutEntry = { ...validEntry(398), copilotDisabled: true };
112
+ assert.equal(childRunInput(optedOutEntry).copilotDisabled, true);
113
+ });
114
+
115
+ test('childRunInput defaults copilotDisabled to false when the entry omits it', () => {
116
+ assert.equal(childRunInput(validEntry(398)).copilotDisabled, false);
117
+ });
118
+
119
+ test('childRunInput forwards bugbotDisabled true when the entry opts out', () => {
120
+ const optedOutEntry = { ...validEntry(398), bugbotDisabled: true };
121
+ assert.equal(childRunInput(optedOutEntry).bugbotDisabled, true);
122
+ });
123
+
124
+ test('childRunInput defaults bugbotDisabled to false when the entry omits it', () => {
125
+ assert.equal(childRunInput(validEntry(398)).bugbotDisabled, false);
126
+ });
@@ -43,6 +43,26 @@ working directory routes into the PR's repo for local work and returns to
43
43
  the session worktree before teardown. See
44
44
  [`reference/per-tick.md` § Step 1.5](reference/per-tick.md).
45
45
 
46
+ ## Copilot quota pre-check (start of run)
47
+
48
+ On the first tick, before Step 4, run the Copilot quota pre-check once:
49
+ `python "$HOME/.claude/_shared/pr-loop/scripts/copilot_quota.py"`. It reads the
50
+ account's remaining Copilot premium-request quota via `gh api
51
+ copilot_internal/user` and prints one line — log that line. Exit 0 means Copilot
52
+ has quota, so leave `copilot_down` false. Any non-zero exit means skip Copilot
53
+ for the whole run — the account is out of quota, the quota API or account access
54
+ is down, or no account is set — so set `copilot_down = true` in
55
+ `pr-converge-state.json`. The account comes from the `COPILOT_QUOTA_ACCOUNT`
56
+ environment variable or a git-ignored `.env` file, and the no-account line names
57
+ the exact `.env` path and key to set.
58
+
59
+ Run the pre-check once, not per tick. Every tick reads `copilot_down` from
60
+ state. When `copilot_down` is true, skip the Copilot gate outright — no fetch,
61
+ no request, no poll, no agent — and export `CLAUDE_REVIEWS_DISABLED="copilot"`
62
+ in that tick's shell before the convergence check, so `check_convergence.py`
63
+ bypasses the Copilot review gate and the pending-requested-reviews gate and the
64
+ run still marks ready on the remaining signals.
65
+
46
66
  ## Budget-aware tick boundaries
47
67
 
48
68
  Before starting any tick, estimate whether the remaining session/usage
@@ -73,8 +93,8 @@ so the next tick resumes with accurate state.
73
93
 
74
94
  Fields: `phase`, `tick_count`, `bugbot_clean_at`, `code_review_clean_at`,
75
95
  `bugteam_clean_at`, `copilot_clean_at`, `current_head`,
76
- `bugbot_acknowledged_at`, `bugbot_down`, `bugteam_skill_invoked_at_head`,
77
- `bugteam_skill_invoked_at_tick`.
96
+ `bugbot_acknowledged_at`, `bugbot_down`, `copilot_down`,
97
+ `bugteam_skill_invoked_at_head`, `bugteam_skill_invoked_at_tick`.
78
98
 
79
99
  ## Gotchas
80
100
 
@@ -300,6 +320,7 @@ round as converged. This rule holds every tick, every loop, every PR.
300
320
  - [ ] Any unresolved? → For each: verify concern against current HEAD;
301
321
  if still applies → Fix (spawn `clean-coder`) → reply → resolve;
302
322
  if no longer applies → reply-with-note → resolve. Push if any code changed → return to Step 4
323
+ - [ ] When `copilot_down == true` (start-of-run quota pre-check), skip the Copilot fetch below — no request, no poll, no agent — and continue to gate (b); the Copilot gate is bypassed for the whole run.
303
324
  - [ ] Fetch Copilot review on `current_head` (top-level review state — uses get_reviews, identifies by reviewer):
304
325
  ```
305
326
  python ~/.claude/skills/pr-converge/scripts/fetch_copilot_reviews.py --owner <O> --repo <R> --pr-number <N>
@@ -316,6 +337,7 @@ round as converged. This rule holds every tick, every loop, every PR.
316
337
  - [ ] not mergeable → rebase → push → return to Step 1
317
338
 
318
339
  **(c) Request Copilot review**
340
+ - [ ] When `copilot_down == true`, skip the request and do not enter COPILOT_WAIT — continue to gate (d); the run marks ready on the remaining signals.
319
341
  - [ ] Check for pending Copilot review:
320
342
  ```
321
343
  python ~/.claude/skills/pr-converge/scripts/check_pending_reviews.py --owner <O> --repo <R> --pr-number <N>
@@ -341,6 +363,7 @@ round as converged. This rule holds every tick, every loop, every PR.
341
363
  Push if code changed → return to Step 4
342
364
 
343
365
  **(e) Mark ready**
366
+ - [ ] When `copilot_down == true`, export `CLAUDE_REVIEWS_DISABLED="copilot"` in this tick's shell before the check below, so it bypasses the Copilot review gate and the pending-requested-reviews gate.
344
367
  - [ ] Run automated convergence check:
345
368
  ```
346
369
  python $HOME/.claude/skills/pr-converge/scripts/check_convergence.py \
@@ -353,6 +376,9 @@ round as converged. This rule holds every tick, every loop, every PR.
353
376
  - [ ] **Step 7a: COPILOT_WAIT — fetch Copilot, decide**
354
377
  See: [`reference/per-tick.md` § Step 2 COPILOT_WAIT](reference/per-tick.md)
355
378
 
379
+ This step does not run when `copilot_down == true`: gate (c) skips the
380
+ Copilot request, so the loop never enters COPILOT_WAIT.
381
+
356
382
  Fetch Copilot reviews + inline comments on `current_head`.
357
383
 
358
384
  - [ ] **clean (no findings)** →
@@ -25,6 +25,12 @@ python ~/.claude/skills/pr-converge/scripts/fetch_copilot_inline_comments.py --o
25
25
  → unaddressed inline threads on the latest Copilot review at current_head
26
26
  ```
27
27
 
28
+ When `copilot_down == true` (start-of-run quota pre-check), skip this gate
29
+ entirely — no Copilot fetch, no request, no poll, no agent. Record evidence
30
+ "Copilot bypassed (quota pre-check non-zero) at <SHA>" and continue to gate (b); the bypass
31
+ holds for the whole run and the quota API is not re-queried per tick. Otherwise
32
+ decide among the four branches below.
33
+
28
34
  Decide (four branches; match first whose predicate holds):
29
35
 
30
36
  - **`classification == "dirty"` with non-empty inline comments matching
@@ -117,6 +123,12 @@ Persist `mergeable_state` into `merge_state_status`. Decide:
117
123
 
118
124
  ## (d) Post-convergence Copilot review request
119
125
 
126
+ When `copilot_down == true` (start-of-run quota pre-check), skip this gate: do
127
+ not request a Copilot review and do not enter `COPILOT_WAIT`. Export
128
+ `CLAUDE_REVIEWS_DISABLED="copilot"` before gate (f)'s convergence check so
129
+ `check_convergence.py` bypasses the Copilot review and pending-review gates.
130
+ Continue to gate (e).
131
+
120
132
  Once gates (a), (b), and (c) all pass (Copilot clean at `current_head` *or* no
121
133
  Copilot review yet, AND Claude clean or absent at `current_head`, AND
122
134
  `mergeable_state == "clean"`), request Copilot review:
@@ -190,7 +202,7 @@ evidence from gates (a)–(e) above. All seven must be confirmed:
190
202
 
191
203
  - [ ] `bugbot_clean_at == current_head` (from per-tick.md Step 2 BUGBOT §c)
192
204
  - [ ] bugteam `convergence (zero findings)` at `current_head` (from per-tick.md Step 2 BUGTEAM §d)
193
- - [ ] `copilot_clean_at == current_head` (from gate (a) or gate (d))
205
+ - [ ] `copilot_clean_at == current_head` (from gate (a) or gate (d)), or the Copilot gate bypassed for the run via `copilot_down`
194
206
  - [ ] Claude `APPROVED` or absent at `current_head` (from gate (b))
195
207
  - [ ] `mergeable_state == "clean"` AND `mergeable == true` (from gate (c))
196
208
  - [ ] Zero unresolved review threads anywhere on the PR (from gate (e))
@@ -22,6 +22,17 @@ live ONLY in the single-PR `$CLAUDE_JOB_DIR/pr-converge-state.json` file
22
22
  with no Copilot review surfaced at `current_head`. Escalate as hard blocker
23
23
  at `>= 3`. Reset to `0` when a Copilot review surfaces at `current_head`
24
24
  (APPROVED or dirty) or on any non-COPILOT_WAIT branch.
25
+ - `copilot_down`: boolean, init `false`. Set `true` at the start of the run
26
+ when the Copilot quota pre-check
27
+ (`_shared/pr-loop/scripts/copilot_quota.py`) exits non-zero — the account is
28
+ out of premium-request quota, the quota API or account access is down, or no
29
+ account is set. Read once from the start-of-run pre-check, not re-queried per
30
+ tick. While `true`, every tick skips the Copilot gate outright (no fetch, no
31
+ request, no poll, no agent) and exports `CLAUDE_REVIEWS_DISABLED="copilot"`
32
+ before the convergence check, so `check_convergence.py` bypasses the Copilot
33
+ review gate and the pending-requested-reviews gate and the run marks ready on
34
+ the remaining signals. Unlike `bugbot_down`, it is not reset on push — a
35
+ quota outage holds for the whole run.
25
36
  - `inline_lag_streak`: integer, init `0`. Consecutive ticks where review
26
37
  body shows findings against `current_head` but inline API returns zero
27
38
  matching. Reset to `0` on any other branch outcome.