claude-dev-env 8.31.1 → 8.32.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 (32) hide show
  1. package/.agents/skills/pull-request/SKILL.md +4 -3
  2. package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
  3. package/.agents/skills/pull-request/scripts/pull_request.py +5 -2
  4. package/.agents/skills/pull-request/scripts/test_pull_request.py +32 -0
  5. package/bin/codex-skill-load-block.mjs +87 -34
  6. package/bin/codex-skill-load-block.test.mjs +112 -4
  7. package/bin/install.mjs +23 -9
  8. package/bin/install.question-presentation.test.mjs +2 -2
  9. package/bin/install.test.mjs +105 -0
  10. package/bin/seed-codex-pstack-models.mjs +2 -2
  11. package/bin/seed-codex-pstack-models.test.mjs +52 -1
  12. package/commands/fix-followups.md +3 -3
  13. package/commands/sr-loop.md +4 -3
  14. package/hooks/advisory/test_pr_done_reminder.py +2 -1
  15. package/hooks/blocking/block_main_commit.py +2 -10
  16. package/hooks/blocking/test_block_main_commit.py +13 -0
  17. package/hooks/hooks_constants/block_main_commit_constants.py +9 -0
  18. package/hooks/hooks_constants/pr_done_reminder_constants.py +1 -1
  19. package/hooks/pyproject.toml +0 -1
  20. package/hooks/validators/README.md +0 -1
  21. package/hooks/validators/health_check.py +0 -1
  22. package/hooks/validators/run_all_validators.py +0 -13
  23. package/hooks/validators/test_run_all_validators.py +5 -36
  24. package/package.json +1 -1
  25. package/rules/agent-merges-its-own-green-pull-request.md +1 -1
  26. package/rules/flag-non-breaking-findings.md +1 -1
  27. package/rules/git-workflow.md +2 -7
  28. package/rules/review-closure-is-a-check.md +1 -1
  29. package/scripts/dev_env_scripts_constants/followup_constants.py +1 -1
  30. package/scripts/test_followup_cli.py +2 -1
  31. package/hooks/validators/git_checks.py +0 -110
  32. package/hooks/validators/test_git_checks.py +0 -112
@@ -54,6 +54,8 @@ import {
54
54
  } from './install.mjs';
55
55
  import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
56
56
  import {
57
+ PACKAGE_GUIDANCE_BLOCK_END,
58
+ PACKAGE_GUIDANCE_BLOCK_START,
57
59
  SKILL_LOAD_BLOCK_START,
58
60
  SKILL_LOAD_INSTRUCTION,
59
61
  QUESTION_PRESENTATION_BLOCK_START,
@@ -3162,6 +3164,109 @@ test('--no-pstack skips the plugin and delivers Codex question guidance', t => {
3162
3164
  );
3163
3165
  });
3164
3166
 
3167
+ const PSTACK_TEST_PACKAGE_GUIDANCE = readFileSync(
3168
+ join(dirname(fileURLToPath(import.meta.url)), '..', 'AGENTS.md'),
3169
+ 'utf8',
3170
+ );
3171
+
3172
+ function assertHoldsPackageGuidance(codexGuidance) {
3173
+ assert.ok(codexGuidance.includes(`${PACKAGE_GUIDANCE_BLOCK_START}\n${PSTACK_TEST_PACKAGE_GUIDANCE}`));
3174
+ assert.ok(codexGuidance.includes(PACKAGE_GUIDANCE_BLOCK_END));
3175
+ }
3176
+
3177
+ test('a fresh full install writes the package guidance into Codex AGENTS.md', t => {
3178
+ const sandbox = pstackPluginSandbox(t);
3179
+
3180
+ runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
3181
+
3182
+ const codexGuidance = readFileSync(join(sandbox.homeDirectory, '.codex', 'AGENTS.md'), 'utf8');
3183
+ assertHoldsPackageGuidance(codexGuidance);
3184
+ assert.ok(codexGuidance.startsWith(`${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n`));
3185
+ assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
3186
+ });
3187
+
3188
+ test('--no-pstack writes package and question guidance without the skill-load block', t => {
3189
+ const sandbox = pstackPluginSandbox(t);
3190
+
3191
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3192
+
3193
+ assert.deepEqual(
3194
+ sandbox.recordedCommands().filter(eachCommand => eachCommand.startsWith('codex plugin')),
3195
+ [],
3196
+ );
3197
+ const codexGuidance = readFileSync(join(sandbox.homeDirectory, '.codex', 'AGENTS.md'), 'utf8');
3198
+ assertHoldsPackageGuidance(codexGuidance);
3199
+ assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
3200
+ assert.equal(codexGuidance.includes(SKILL_LOAD_BLOCK_START), false);
3201
+ });
3202
+
3203
+ test('--no-pstack replaces an outdated package block and keeps the text around it', t => {
3204
+ const sandbox = pstackPluginSandbox(t);
3205
+ const codexHome = join(sandbox.homeDirectory, '.codex');
3206
+ mkdirSync(codexHome, { recursive: true });
3207
+ writeFileSync(
3208
+ join(codexHome, 'AGENTS.md'),
3209
+ `My notes\n${PACKAGE_GUIDANCE_BLOCK_START}\nOld guidance\n${PACKAGE_GUIDANCE_BLOCK_END}\nMore notes\n`,
3210
+ );
3211
+
3212
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3213
+
3214
+ const codexGuidance = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
3215
+ assert.ok(codexGuidance.startsWith(`My notes\n${PACKAGE_GUIDANCE_BLOCK_START}\n${PSTACK_TEST_PACKAGE_GUIDANCE}`));
3216
+ assert.ok(codexGuidance.includes(`${PACKAGE_GUIDANCE_BLOCK_END}\nMore notes\n`));
3217
+ assert.equal(codexGuidance.includes('Old guidance'), false);
3218
+ assert.equal(codexGuidance.includes(SKILL_LOAD_BLOCK_START), false);
3219
+ });
3220
+
3221
+ for (const linkTargetDirectory of ['.claude', '.agents']) {
3222
+ test(`--no-pstack turns a Codex link to the ${linkTargetDirectory} package guidance into a file`, t => {
3223
+ const sandbox = pstackPluginSandbox(t);
3224
+ const codexHome = join(sandbox.homeDirectory, '.codex');
3225
+ const codexAgentsPath = join(codexHome, 'AGENTS.md');
3226
+ mkdirSync(codexHome, { recursive: true });
3227
+ symlinkSync(join(sandbox.homeDirectory, linkTargetDirectory, 'AGENTS.md'), codexAgentsPath);
3228
+
3229
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3230
+
3231
+ assert.equal(lstatSync(codexAgentsPath).isSymbolicLink(), false);
3232
+ const codexGuidance = readFileSync(codexAgentsPath, 'utf8');
3233
+ assertHoldsPackageGuidance(codexGuidance);
3234
+ assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
3235
+ assert.equal(codexGuidance.includes(SKILL_LOAD_BLOCK_START), false);
3236
+ });
3237
+ }
3238
+
3239
+ test('--no-pstack leaves a Codex link to a file the package does not own unchanged', t => {
3240
+ const sandbox = pstackPluginSandbox(t);
3241
+ const codexHome = join(sandbox.homeDirectory, '.codex');
3242
+ const codexAgentsPath = join(codexHome, 'AGENTS.md');
3243
+ const ownGuidancePath = join(sandbox.homeDirectory, 'notes', 'codex.md');
3244
+ mkdirSync(codexHome, { recursive: true });
3245
+ mkdirSync(dirname(ownGuidancePath), { recursive: true });
3246
+ writeFileSync(ownGuidancePath, 'My own Codex notes\n');
3247
+ symlinkSync(ownGuidancePath, codexAgentsPath);
3248
+
3249
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3250
+
3251
+ assert.equal(lstatSync(codexAgentsPath).isSymbolicLink(), true);
3252
+ assert.equal(readFileSync(ownGuidancePath, 'utf8'), 'My own Codex notes\n');
3253
+ });
3254
+
3255
+ test('a pstack install after a --no-pstack install seeds the Codex model sheet', t => {
3256
+ const sandbox = pstackPluginSandbox(t);
3257
+ const codexHome = join(sandbox.homeDirectory, '.codex');
3258
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3259
+
3260
+ runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
3261
+
3262
+ assert.match(readFileSync(join(codexHome, 'pstack-models.md'), 'utf8'), /^session hook: on$/m);
3263
+ const codexGuidance = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
3264
+ assert.match(codexGuidance, /^feature, refactoring: gpt-6-sol$/m);
3265
+ assert.ok(codexGuidance.startsWith(`${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n`));
3266
+ assertHoldsPackageGuidance(codexGuidance);
3267
+ assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
3268
+ });
3269
+
3165
3270
  function continuityCommandCount(configurationPath) {
3166
3271
  if (!existsSync(configurationPath)) return 0;
3167
3272
  const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
@@ -1,7 +1,7 @@
1
1
  import { copyFileSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, constants as filesystemConstants } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { hasOnlyQuestionPresentationBlock } from './codex-skill-load-block.mjs';
4
+ import { hasOnlyPackageManagedBlocks } from './codex-skill-load-block.mjs';
5
5
 
6
6
  const CODEX_PRESET_PATH = fileURLToPath(new URL('../pstack-codex-models.md', import.meta.url));
7
7
 
@@ -16,7 +16,7 @@ export function seedCodexPstackModels(codexHome, writeAgentGuidance = writeFileS
16
16
  const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
17
17
  if (agentsEntry && !agentsEntry.isFile()) return null;
18
18
  const existingGuidance = agentsEntry ? readFileSync(agentsPath, 'utf8') : '';
19
- if (agentsEntry && !hasOnlyQuestionPresentationBlock(existingGuidance)) return null;
19
+ if (agentsEntry && !hasOnlyPackageManagedBlocks(existingGuidance)) return null;
20
20
 
21
21
  const preset = readFileSync(CODEX_PRESET_PATH, 'utf8');
22
22
  const agentGuidance = preset.replace(/^session hook: on\n?$/m, '');
@@ -4,7 +4,14 @@ import { existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, rmSync, sy
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
7
- import { QUESTION_PRESENTATION_BLOCK_END, QUESTION_PRESENTATION_BLOCK_START } from './codex-skill-load-block.mjs';
7
+ import {
8
+ PACKAGE_GUIDANCE_BLOCK_END,
9
+ PACKAGE_GUIDANCE_BLOCK_START,
10
+ QUESTION_PRESENTATION_BLOCK_END,
11
+ QUESTION_PRESENTATION_BLOCK_START,
12
+ SKILL_LOAD_BLOCK_END,
13
+ SKILL_LOAD_BLOCK_START,
14
+ } from './codex-skill-load-block.mjs';
8
15
 
9
16
  const QUESTION_GUIDANCE = `${QUESTION_PRESENTATION_BLOCK_START}\nAsk one question.\n${QUESTION_PRESENTATION_BLOCK_END}\n`;
10
17
 
@@ -114,3 +121,47 @@ test('a linked question-only Codex file does not trigger model seeding', (contex
114
121
  assert.equal(lstatSync(join(codexHome, 'AGENTS.md')).isSymbolicLink(), true);
115
122
  assert.equal(readFileSync(linkedGuidancePath, 'utf8'), QUESTION_GUIDANCE);
116
123
  });
124
+
125
+ const PACKAGE_GUIDANCE = `${PACKAGE_GUIDANCE_BLOCK_START}\nStatus\n${PACKAGE_GUIDANCE_BLOCK_END}\n`;
126
+ const SKILL_LOAD_GUIDANCE = `${SKILL_LOAD_BLOCK_START}\nLoad the skill.\n${SKILL_LOAD_BLOCK_END}\n`;
127
+
128
+ function codexHomeHolding(context, existingGuidance) {
129
+ const root = mkdtempSync(join(tmpdir(), 'cde-pstack-models-'));
130
+ context.after(() => rmSync(root, { recursive: true, force: true }));
131
+ const codexHome = join(root, '.codex');
132
+ mkdirSync(codexHome);
133
+ writeFileSync(join(codexHome, 'AGENTS.md'), existingGuidance);
134
+ return codexHome;
135
+ }
136
+
137
+ for (const [label, existingGuidance] of [
138
+ ['package block', PACKAGE_GUIDANCE],
139
+ ['package then question blocks', `${PACKAGE_GUIDANCE}\n${QUESTION_GUIDANCE}`],
140
+ ['question then package blocks', `${QUESTION_GUIDANCE}\n${PACKAGE_GUIDANCE}`],
141
+ ['all three managed blocks', `${SKILL_LOAD_GUIDANCE}\n${PACKAGE_GUIDANCE}\n${QUESTION_GUIDANCE}`],
142
+ ]) {
143
+ test(`a Codex file holding only the ${label} receives model defaults and keeps its blocks`, (context) => {
144
+ const codexHome = codexHomeHolding(context, existingGuidance);
145
+
146
+ const paths = seedCodexPstackModels(codexHome);
147
+
148
+ assert.deepEqual(paths, [join(codexHome, 'pstack-models.md'), join(codexHome, 'AGENTS.md')]);
149
+ const agents = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
150
+ assert.match(agents, /^feature, refactoring: gpt-6-sol$/m);
151
+ assert.ok(agents.endsWith(existingGuidance));
152
+ });
153
+ }
154
+
155
+ for (const [label, existingGuidance] of [
156
+ ['an empty file', ''],
157
+ ['a package block with personal notes', `${PACKAGE_GUIDANCE}My notes\n`],
158
+ ['personal notes before managed blocks', `My notes\n${PACKAGE_GUIDANCE}\n${QUESTION_GUIDANCE}`],
159
+ ]) {
160
+ test(`${label} does not trigger model seeding`, (context) => {
161
+ const codexHome = codexHomeHolding(context, existingGuidance);
162
+
163
+ assert.equal(seedCodexPstackModels(codexHome), null);
164
+ assert.equal(existsSync(join(codexHome, 'pstack-models.md')), false);
165
+ assert.equal(readFileSync(join(codexHome, 'AGENTS.md'), 'utf8'), existingGuidance);
166
+ });
167
+ }
@@ -1,10 +1,10 @@
1
1
  ---
2
- description: Fix every non-breaking finding the follow-up ledger holds and open a draft PR for it
2
+ description: Fix every non-breaking finding the follow-up ledger holds and open a PR for it
3
3
  argument-hint: [repository root, or blank for the current repository]
4
4
  ---
5
5
 
6
6
  Fix the non-breaking findings a local gate recorded rather than blocked on,
7
- and deliver them as one draft pull request.
7
+ and deliver them as one pull request.
8
8
 
9
9
  The repository root is `$ARGUMENTS`, or the current repository when that is
10
10
  blank.
@@ -50,7 +50,7 @@ finding names a generated file, regenerate it with the repository's tooling.
50
50
  Branch from the repository's default branch. Commit each group on its own,
51
51
  with a message naming the check identifier and the files.
52
52
 
53
- Open the pull request as a draft. Write the body as:
53
+ Open the pull request ready for review. Write the body as:
54
54
 
55
55
  - One `Before:` paragraph saying which findings stood open.
56
56
  - One `After:` paragraph saying what the tree now holds.
@@ -39,7 +39,7 @@ stack's design, intent, and changes.
39
39
  code, not the full repo suite), commit once (`git commit -F <file>`, body
40
40
  written with the Write tool, Co-Authored-By line, 10-minute timeout — the
41
41
  pre-commit gate runs its own tests), and push to the PR head branch. The PR
42
- stays draft.
42
+ keeps its draft or ready state.
43
43
  3. Repeat the invocation only after substantive findings or a user request.
44
44
  **Skips are sticky:** carry every adjudicated skip
45
45
  forward into the next pass as "already adjudicated — do not re-report: ..."
@@ -64,8 +64,9 @@ stack's design, intent, and changes.
64
64
 
65
65
  ## Finish
66
66
 
67
- When both phases are clean, or only fixed nits remain, mark the target PR ready
68
- with `gh pr ready`. Confirm that it is no longer a draft.
67
+ When both phases are clean, or only fixed nits remain, a target PR that is a
68
+ draft goes ready with `gh pr ready`. Confirm that it now reads ready. A
69
+ ready target PR stays as it is.
69
70
 
70
71
  Report: passes run per phase, commits pushed with hashes, fixes applied, and
71
72
  the standing skip list with reasons.
@@ -242,7 +242,8 @@ def test_main_should_remind_to_open_a_pr_when_the_branch_has_none(
242
242
  context = _additional_context(_run_main(monkeypatch, capsys, _payload("git push"), no_pr))
243
243
 
244
244
  assert "No open pull request found" in context
245
- assert "gh pr create --draft" in context
245
+ assert "Open a pull request with gh pr create." in context
246
+ assert "--draft" not in context
246
247
 
247
248
 
248
249
  def test_main_should_stay_quiet_on_any_other_gh_failure(
@@ -23,6 +23,7 @@ if _hooks_dir not in sys.path:
23
23
 
24
24
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
25
25
 
26
+ from hooks_constants.block_main_commit_constants import PULL_REQUEST_INSTRUCTION
26
27
  from hooks_constants.subprocess_window import hidden_window_creation_flags
27
28
 
28
29
  GIT_COMMAND_TIMEOUT_SECONDS = 5
@@ -164,20 +165,11 @@ def parse_bash_command_from_stdin() -> str:
164
165
  return bash_command
165
166
 
166
167
 
167
- DRAFT_PR_INSTRUCTION = (
168
- " Instead: (1) create a feature branch with `git checkout -b <descriptive-branch-name>`, "
169
- "(2) commit your changes there, "
170
- "(3) push with `git push -u origin <branch-name>`, "
171
- "(4) create a draft PR with `gh pr create --draft`. "
172
- "If you must commit to main, the user needs to approve explicitly."
173
- )
174
-
175
-
176
168
  def build_denial_response(branch_name: str, repo_dir: str | None) -> dict:
177
169
  location = f" in {repo_dir}" if repo_dir else ""
178
170
  denial_reason = (
179
171
  f"BLOCKED: Direct commit to '{branch_name}'{location} is not allowed."
180
- + DRAFT_PR_INSTRUCTION
172
+ + PULL_REQUEST_INSTRUCTION
181
173
  )
182
174
 
183
175
  return {
@@ -74,6 +74,19 @@ def test_blocks_commit_on_protected_branch_from_hook_event_cwd(tmp_path: Path) -
74
74
  assert str(repository) in hook_response["hookSpecificOutput"]["permissionDecisionReason"]
75
75
 
76
76
 
77
+ def test_denial_should_open_a_ready_pull_request(tmp_path: Path) -> None:
78
+ repository = _create_repository(tmp_path, "main")
79
+ process_repository = _create_repository(tmp_path, "agent-owned-change")
80
+
81
+ completed_process = _run_commit_gate(repository, process_repository)
82
+
83
+ denial_reason = json.loads(completed_process.stdout)["hookSpecificOutput"][
84
+ "permissionDecisionReason"
85
+ ]
86
+ assert "open a pull request with `gh pr create`" in denial_reason
87
+ assert "--draft" not in denial_reason
88
+
89
+
77
90
  def test_allows_commit_on_owned_branch_from_hook_event_cwd(tmp_path: Path) -> None:
78
91
  repository = _create_repository(tmp_path, "agent-owned-change")
79
92
  process_repository = _create_repository(tmp_path, "main")
@@ -0,0 +1,9 @@
1
+ """Constants for the protected-branch commit gate."""
2
+
3
+ PULL_REQUEST_INSTRUCTION = (
4
+ " Instead: (1) create a feature branch with `git checkout -b <descriptive-branch-name>`, "
5
+ "(2) commit your changes there, "
6
+ "(3) push with `git push -u origin <branch-name>`, "
7
+ "(4) open a pull request with `gh pr create`. "
8
+ "If you must commit to main, the user needs to approve explicitly."
9
+ )
@@ -87,7 +87,7 @@ VERDICT_DONE: str = "DONE. Add the label now:"
87
87
  VERDICT_NOT_DONE: str = "NOT DONE. Do not add the done label yet."
88
88
  NO_PULL_REQUEST_REMINDER: str = (
89
89
  "=== PR DONE CHECKLIST (context reminder, never a block) ===\n"
90
- "No open pull request found for this branch. Open a draft PR with gh pr create --draft.\n"
90
+ "No open pull request found for this branch. Open a pull request with gh pr create.\n"
91
91
  "A branch is done only when its PR has clean CI, no merge conflicts, and the done label."
92
92
  )
93
93
  RECHECK_COMMAND_TEMPLATE: str = (
@@ -17,7 +17,6 @@ exclude = [
17
17
  "validators/test_files/",
18
18
  "test_",
19
19
  "abbreviation_checks.py",
20
- "git_checks.py",
21
20
  "exempt_paths.py",
22
21
  "code_rules_typeddict_stub.py",
23
22
  ]
@@ -14,7 +14,6 @@ The checks live in `python_style_checks.py`. Shared source-line splitting, funct
14
14
  | `exempt_paths.py` | Shared config, test, and hook-infrastructure path exemptions |
15
15
  | `fast_save_validators.py` | In-process Write/Edit save-path validator roster |
16
16
  | `file_structure_checks.py` | File structure checks for pre-PR validation |
17
- | `git_checks.py` | Git and GitHub checks for pre-push review |
18
17
  | `health_check.py` | Validator availability, dependency, and version checks |
19
18
  | `hook_timing_harness.py` | Hook runtime measurement and threshold report |
20
19
  | `magic_value_checks.py` | Hardcoded magic-number checks |
@@ -23,7 +23,6 @@ VALIDATOR_FILES = [
23
23
  "test_safety_checks.py",
24
24
  "file_structure_checks.py",
25
25
  "react_checks.py",
26
- "git_checks.py",
27
26
  "comment_checks.py",
28
27
  ]
29
28
 
@@ -402,18 +402,6 @@ def run_react_checks(files: List[Path]) -> ValidatorResult:
402
402
  )
403
403
 
404
404
 
405
- def run_git_checks() -> ValidatorResult:
406
- """Run git/GitHub checks."""
407
- result = invoke_validator_module("git_checks", [])
408
-
409
- return ValidatorResult(
410
- name="Git/PR Workflow",
411
- checks="23,24",
412
- passed=result.returncode == 0,
413
- output=result.stdout or result.stderr or "All checks passed",
414
- )
415
-
416
-
417
405
  def run_comment_checks(files: List[Path]) -> ValidatorResult:
418
406
  """Report that comment policy runs through the explicit diff-aware linter."""
419
407
  return ValidatorResult(
@@ -1693,7 +1681,6 @@ def main() -> int:
1693
1681
 
1694
1682
  validators.extend([
1695
1683
  ("File Structure", run_file_structure_checks),
1696
- ("Git/PR", run_git_checks),
1697
1684
  ])
1698
1685
 
1699
1686
  for i, (name, validator_func) in enumerate(validators, 1):
@@ -19,7 +19,6 @@ from .run_all_validators import (
19
19
  format_timing_report,
20
20
  main,
21
21
  print_header,
22
- run_git_checks,
23
22
  run_python_style_checks,
24
23
  run_with_fallback,
25
24
  validate_proposed_file,
@@ -164,11 +163,9 @@ class TestFixFlag:
164
163
  def test_fix_flag_is_accepted(self) -> None:
165
164
  """Verify --fix flag is recognized without error."""
166
165
  with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
167
- patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
168
- patch("validators.run_all_validators.run_git_checks") as mock_git:
166
+ patch("validators.run_all_validators.run_file_structure_checks") as mock_file:
169
167
  mock_get_files.return_value = []
170
168
  mock_file.return_value = _passing_mock_result()
171
- mock_git.return_value = _passing_mock_result()
172
169
 
173
170
  assert _run_main_with_argv(["run_all_validators.py", "--fix"]) == 0
174
171
 
@@ -180,11 +177,10 @@ class TestFixFlag:
180
177
  patch("validators.run_all_validators.run_test_safety_checks") as mock_test, \
181
178
  patch("validators.run_all_validators.run_react_checks") as mock_react, \
182
179
  patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
183
- patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
184
- patch("validators.run_all_validators.run_git_checks") as mock_git:
180
+ patch("validators.run_all_validators.run_file_structure_checks") as mock_file:
185
181
  mock_get_files.return_value = [Path("test.py")]
186
182
  mock_fix.return_value = ["test.py"]
187
- for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file, mock_git):
183
+ for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file):
188
184
  each_mock.return_value = _passing_mock_result()
189
185
 
190
186
  _run_main_with_argv(["run_all_validators.py", "--fix"])
@@ -199,10 +195,9 @@ class TestFixFlag:
199
195
  patch("validators.run_all_validators.run_test_safety_checks") as mock_test, \
200
196
  patch("validators.run_all_validators.run_react_checks") as mock_react, \
201
197
  patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
202
- patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
203
- patch("validators.run_all_validators.run_git_checks") as mock_git:
198
+ patch("validators.run_all_validators.run_file_structure_checks") as mock_file:
204
199
  mock_get_files.return_value = [Path("test.py")]
205
- for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file, mock_git):
200
+ for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file):
206
201
  each_mock.return_value = _passing_mock_result()
207
202
 
208
203
  _run_main_with_argv(["run_all_validators.py"])
@@ -302,32 +297,6 @@ class TestStderrSurfacing:
302
297
 
303
298
  assert "ImportError" in validator_result.output
304
299
 
305
- def test_git_check_surfaces_stderr_when_stdout_empty(self) -> None:
306
- """When git validator crashes with no stdout, stderr must appear in output."""
307
- with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
308
- crashed_result = MagicMock()
309
- crashed_result.returncode = 1
310
- crashed_result.stdout = ""
311
- crashed_result.stderr = "SyntaxError: invalid syntax in git_checks.py"
312
- mock_invoke.return_value = crashed_result
313
-
314
- validator_result = run_git_checks()
315
-
316
- assert "SyntaxError" in validator_result.output
317
-
318
- def test_output_falls_back_to_all_checks_passed_when_both_empty(self) -> None:
319
- """When both stdout and stderr are empty and returncode is 0, use fallback."""
320
- with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
321
- clean_result = MagicMock()
322
- clean_result.returncode = 0
323
- clean_result.stdout = ""
324
- clean_result.stderr = ""
325
- mock_invoke.return_value = clean_result
326
-
327
- validator_result = run_git_checks()
328
-
329
- assert validator_result.output == "All checks passed"
330
-
331
300
 
332
301
  class TestTimingMetrics:
333
302
  def test_create_timing_metrics_empty(self) -> None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.31.1",
3
+ "version": "8.32.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,6 @@ Delete nothing by hand. The repository deletes the head branch on merge.
50
50
 
51
51
  | Rule | Role |
52
52
  |---|---|
53
- | [`git-workflow.md`](git-workflow.md) | Draft first, and confirm each required context fired after the push |
53
+ | [`git-workflow.md`](git-workflow.md) | Open ready for review, and confirm each required context fired after the push |
54
54
  | [`ci-owns-the-gate.md`](ci-owns-the-gate.md) | The gate runs once, and it runs on CI |
55
55
  | [`correction-lens.md`](correction-lens.md) | A correction becomes a control at the highest layer that can hold it |
@@ -84,7 +84,7 @@ escalates rather than opening one more quiet pull request. The number is the
84
84
  repository's setting, and raising or lowering it is one edit there.
85
85
 
86
86
  The `/fix-followups` command drives the whole pass: it reads the brief, fixes
87
- each rule group, opens a draft pull request, and clears the ledger.
87
+ each rule group, opens a pull request, and clears the ledger.
88
88
 
89
89
  ## Worked example
90
90
 
@@ -15,7 +15,8 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh`. Small
15
15
 
16
16
  ## Pull request submission rules
17
17
 
18
- **Create every pull request as a draft.** Use `gh pr create --draft`.
18
+ **Open every pull request ready for review.** Pass `--draft` only when the owner asks
19
+ for a draft.
19
20
 
20
21
  **A release bot's PR body is machine input. Leave it alone.** Release automation reads
21
22
  back the body of its own merged pull request to decide it owns that merge. Rewriting the
@@ -45,12 +46,6 @@ Use `.agents/skills/pull-request/scripts/recover_legacy_author.py
45
46
  author record. Do not infer a record from age alone. Keep every other record
46
47
  untouched.
47
48
 
48
- ## Git golden rules
49
-
50
- 1. **Draft before push.** Put a pull request in draft state before you push to it.
51
- - Before push: `gh pr ready --undo`
52
- - After review approved: `gh pr ready`
53
-
54
49
  ## Confirm the required checks fired, and let CI run them
55
50
 
56
51
  The gate runs once, and it runs on CI. Push the branch and read its verdict.
@@ -50,5 +50,5 @@ A repository that merges through a merge queue also runs the check on `merge_gro
50
50
  | Rule | Role |
51
51
  |---|---|
52
52
  | [`agent-merges-its-own-green-pull-request.md`](agent-merges-its-own-green-pull-request.md) | The agent that drives a pull request merges it once its gate passes |
53
- | [`git-workflow.md`](git-workflow.md) | Draft first, and confirm each required context fired after the push |
53
+ | [`git-workflow.md`](git-workflow.md) | Open ready for review, and confirm each required context fired after the push |
54
54
  | [`correction-lens.md`](correction-lens.md) | A correction becomes a control at the highest layer that can hold it |
@@ -72,7 +72,7 @@ USAGE_TEXT: str = (
72
72
  )
73
73
 
74
74
  BRIEF_HEADER: str = (
75
- "Fix every finding below in one change, then open a draft pull request "
75
+ "Fix every finding below in one change, then open a pull request "
76
76
  "for it. Each finding is non-breaking, so the change that raised it "
77
77
  "already shipped. Keep the fixes mechanical, touch no behavior, and run "
78
78
  "the repository's own checks before pushing."
@@ -130,7 +130,8 @@ def test_brief_names_every_recorded_finding_and_the_repository(tmp_path: Path) -
130
130
  assert exit_code == 0
131
131
  assert "instruction-git-mode" in output_text
132
132
  assert "wrong mode" in output_text
133
- assert "draft pull request" in output_text
133
+ assert "open a pull request for it" in output_text
134
+ assert "draft pull request" not in output_text
134
135
 
135
136
 
136
137
  def test_brief_asks_for_no_work_on_an_empty_ledger(tmp_path: Path) -> None:
@@ -1,110 +0,0 @@
1
- """Git and GitHub validation checks for pre-push review."""
2
-
3
- import json
4
- import subprocess
5
- import sys
6
- from dataclasses import dataclass
7
- from typing import List
8
-
9
- from hooks_constants.subprocess_window import hidden_window_creation_flags
10
-
11
- SUBPROCESS_TIMEOUT_SECONDS = 30
12
-
13
-
14
- @dataclass
15
- class Violation:
16
- """Represents a validation violation."""
17
- file: str
18
- line: int
19
- message: str
20
-
21
-
22
- def get_current_branch() -> str:
23
- """Get the current git branch name."""
24
- try:
25
- result = subprocess.run(
26
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
27
- capture_output=True,
28
- text=True,
29
- check=True,
30
- timeout=SUBPROCESS_TIMEOUT_SECONDS,
31
- creationflags=hidden_window_creation_flags(),
32
- )
33
- return result.stdout.strip()
34
- except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
35
- return ""
36
-
37
-
38
- def check_draft_pr_state() -> List[Violation]:
39
- """
40
- Check that PR is in draft state.
41
-
42
- Returns empty list if:
43
- - No PR exists for current branch
44
- - gh CLI not available
45
- - PR is in draft state
46
-
47
- Returns violation if:
48
- - PR is not in draft state
49
- """
50
- branch = get_current_branch()
51
- if not branch:
52
- return []
53
-
54
- try:
55
- pr_info = subprocess.run(
56
- ["gh", "pr", "list", "--head", branch, "--json", "number,isDraft"],
57
- capture_output=True,
58
- text=True,
59
- check=True,
60
- timeout=SUBPROCESS_TIMEOUT_SECONDS,
61
- creationflags=hidden_window_creation_flags(),
62
- )
63
- except FileNotFoundError:
64
- return []
65
- except subprocess.CalledProcessError:
66
- return []
67
- except subprocess.TimeoutExpired:
68
- return []
69
-
70
- if not pr_info.stdout.strip():
71
- return []
72
-
73
- try:
74
- pr_data = json.loads(pr_info.stdout)
75
- except json.JSONDecodeError:
76
- return []
77
-
78
- if not pr_data:
79
- return []
80
-
81
- is_draft = pr_data[0].get("isDraft", False)
82
-
83
- if is_draft:
84
- return []
85
-
86
- return [
87
- Violation(
88
- file="",
89
- line=0,
90
- message="PR must be in draft state before pushing. Run: gh pr ready --undo",
91
- )
92
- ]
93
-
94
-
95
- def main() -> None:
96
- """Run all git checks and exit with appropriate code."""
97
- violations: List[Violation] = []
98
-
99
- violations.extend(check_draft_pr_state())
100
-
101
- if violations:
102
- for violation in violations:
103
- print(violation.message)
104
- sys.exit(1)
105
-
106
- sys.exit(0)
107
-
108
-
109
- if __name__ == "__main__":
110
- main()