claude-dev-env 7.5.0 → 7.6.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 (121) hide show
  1. package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
  2. package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
  3. package/.agents/skills/fresh-branch/SKILL.md +1 -0
  4. package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
  5. package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
  6. package/.agents/skills/syncing-submodules/SKILL.md +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
  8. package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
  9. package/.agents/skills/test-runner/SKILL.md +1 -0
  10. package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
  11. package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
  12. package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
  13. package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
  14. package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
  15. package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
  16. package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
  23. package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
  24. package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
  25. package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
  26. package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
  27. package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
  28. package/_shared/pr-loop/scripts/preflight.py +7 -0
  29. package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
  30. package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
  31. package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
  33. package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
  34. package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
  35. package/bin/install-constants.mjs +4 -2
  36. package/bin/install.cursor-rules.test.mjs +57 -15
  37. package/bin/install.mjs +10 -15
  38. package/bin/pstack-model-preferences.claude.json +23 -0
  39. package/hooks/advisory/pr_done_reminder.py +7 -0
  40. package/hooks/advisory/refactor_guard.py +8 -0
  41. package/hooks/advisory/test_pr_done_reminder.py +21 -0
  42. package/hooks/advisory/test_refactor_guard.py +40 -0
  43. package/hooks/blocking/block_main_commit.py +4 -0
  44. package/hooks/blocking/code_rules_comments.py +39 -5
  45. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
  46. package/hooks/blocking/precommit_code_rules_gate.py +3 -0
  47. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
  48. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
  49. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
  50. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
  51. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
  52. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
  53. package/hooks/git-hooks/pull_request_handoff.py +3 -0
  54. package/hooks/git-hooks/subprocess_window_access.py +41 -0
  55. package/hooks/git-hooks/test_verification_start.py +4 -0
  56. package/hooks/git-hooks/verification_notice_context.py +3 -0
  57. package/hooks/git-hooks/verification_start.py +7 -8
  58. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
  59. package/hooks/hooks_constants/subprocess_window.py +141 -0
  60. package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
  61. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
  62. package/hooks/hooks_constants/test_subprocess_window.py +128 -0
  63. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
  64. package/hooks/validation/mypy_validator.py +4 -0
  65. package/hooks/validators/README.md +1 -0
  66. package/hooks/validators/git_checks.py +4 -0
  67. package/hooks/validators/health_check.py +3 -0
  68. package/hooks/validators/hook_timing_harness.py +3 -0
  69. package/hooks/validators/mypy_integration.py +4 -0
  70. package/hooks/validators/ruff_integration.py +6 -0
  71. package/hooks/validators/run_all_validators.py +7 -0
  72. package/hooks/validators/subprocess_window_access.py +41 -0
  73. package/hooks/workflow/auto_formatter.py +8 -0
  74. package/hooks/workflow/test_auto_formatter.py +39 -0
  75. package/package.json +1 -1
  76. package/rules/pstack-models.md +4 -3
  77. package/scripts/automatic_advisory/checkout.py +8 -1
  78. package/scripts/automatic_advisory/cli.py +9 -3
  79. package/scripts/automatic_advisory/config/constants.py +0 -2
  80. package/scripts/automatic_advisory/execution.py +7 -1
  81. package/scripts/automatic_advisory/git.py +9 -1
  82. package/scripts/automatic_advisory/process_host.py +3 -0
  83. package/scripts/claude_chain_runner.py +3 -0
  84. package/scripts/gh_artifact_upload.py +3 -0
  85. package/scripts/grok_headless_runner.py +3 -0
  86. package/scripts/grok_patch_artifacts.py +4 -0
  87. package/scripts/grok_worker_preflight.py +3 -0
  88. package/scripts/invoke_code_review.py +3 -0
  89. package/scripts/local_verification/command_runner.py +3 -0
  90. package/scripts/local_verification/git_state.py +3 -0
  91. package/scripts/policy_lint/adapter_detectors.py +48 -2
  92. package/scripts/policy_lint/adapter_pairing.py +150 -2
  93. package/scripts/policy_lint/adapters.py +46 -4
  94. package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
  95. package/scripts/policy_lint/config/constants.py +3 -0
  96. package/scripts/policy_lint/registry.py +1 -1
  97. package/scripts/policy_lint/selection_git.py +11 -0
  98. package/scripts/repository_checks/config/constants.py +5 -0
  99. package/scripts/repository_checks/config/policy_document.py +19 -0
  100. package/scripts/repository_checks/email_exemptions.py +14 -71
  101. package/scripts/repository_checks/path_exemptions.py +35 -0
  102. package/scripts/repository_checks/policy_document.py +120 -0
  103. package/scripts/repository_checks/tracked_secrets.py +27 -4
  104. package/scripts/setup_project_paths.py +3 -0
  105. package/scripts/spawn_grok_batch.py +3 -0
  106. package/scripts/subprocess_window_access.py +41 -0
  107. package/scripts/test_dispatcher_profile_import.py +4 -0
  108. package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
  109. package/scripts/tests/test_email_exemptions.py +37 -0
  110. package/scripts/tests/test_path_exemptions.py +164 -0
  111. package/scripts/tests/test_policy_document.py +104 -0
  112. package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
  113. package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
  114. package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
  115. package/scripts/tests/test_policy_lint_selection.py +34 -0
  116. package/scripts/tests/test_subprocess_window_access.py +118 -0
  117. package/scripts/tests/test_tracked_secrets.py +79 -0
  118. package/scripts/validate_instruction_pairs.py +3 -0
  119. package/scripts/verify_installable_package.py +5 -0
  120. package/scripts/automatic_advisory/window_flags.py +0 -41
  121. package/scripts/repository_checks/config/email_exemptions.py +0 -10
@@ -48,6 +48,8 @@ from reviews_disabled import (
48
48
  is_bugteam_disabled_via_env,
49
49
  )
50
50
 
51
+ from subprocess_window_access import hidden_window_creation_flags, inherited_stream_startup_info
52
+
51
53
 
52
54
  def verify_git_hooks_path(repository_root: Path | None = None) -> int:
53
55
  """Check that core.hooksPath resolves to the claude-dev-env git-hooks directory.
@@ -95,6 +97,7 @@ def verify_git_hooks_path(repository_root: Path | None = None) -> int:
95
97
  encoding="utf-8",
96
98
  errors="replace",
97
99
  check=False,
100
+ creationflags=hidden_window_creation_flags(),
98
101
  )
99
102
  except FileNotFoundError:
100
103
  print(
@@ -201,6 +204,7 @@ def has_discoverable_tests(root: Path) -> bool | None:
201
204
  encoding="utf-8",
202
205
  errors="replace",
203
206
  check=True,
207
+ creationflags=hidden_window_creation_flags(),
204
208
  )
205
209
  except FileNotFoundError:
206
210
  print(
@@ -260,6 +264,7 @@ def run_pytest(
260
264
  command,
261
265
  cwd=str(repository_root),
262
266
  check=False,
267
+ startupinfo=inherited_stream_startup_info(),
263
268
  )
264
269
  if completed.returncode == _pytest_exit_code_no_tests_collected():
265
270
  return 0
@@ -302,6 +307,7 @@ def get_changed_files(repository_root: Path, base_ref: str) -> list[Path] | None
302
307
  encoding="utf-8",
303
308
  errors="replace",
304
309
  check=False,
310
+ creationflags=hidden_window_creation_flags(),
305
311
  )
306
312
  except FileNotFoundError:
307
313
  print(
@@ -398,6 +404,7 @@ def run_pre_commit(repository_root: Path) -> int:
398
404
  list(ALL_PRE_COMMIT_RUN_ALL_FILES_COMMAND),
399
405
  cwd=str(repository_root),
400
406
  check=False,
407
+ startupinfo=inherited_stream_startup_info(),
401
408
  )
402
409
  return completed.returncode
403
410
 
@@ -24,6 +24,8 @@ from pr_loop_shared_constants.preflight_self_heal_constants import ( # noqa: E4
24
24
  ALL_GIT_CONFIG_LOCAL_UNSET_ALL_HOOKS_PATH_ARGUMENTS,
25
25
  )
26
26
 
27
+ from subprocess_window_access import hidden_window_creation_flags
28
+
27
29
 
28
30
  def _is_canonical_hooks_path_entry(
29
31
  raw_hooks_path_entry: str,
@@ -71,6 +73,7 @@ def _canonical_global_hooks_path_is_set(expected_hooks_path_suffix: str) -> bool
71
73
  encoding="utf-8",
72
74
  errors="replace",
73
75
  check=False,
76
+ creationflags=hidden_window_creation_flags(),
74
77
  )
75
78
  except (FileNotFoundError, OSError):
76
79
  return False
@@ -127,6 +130,7 @@ def silently_clear_stale_local_hooks_path_override(
127
130
  encoding="utf-8",
128
131
  errors="replace",
129
132
  check=False,
133
+ creationflags=hidden_window_creation_flags(),
130
134
  )
131
135
  except (FileNotFoundError, OSError):
132
136
  return
@@ -159,6 +163,7 @@ def silently_clear_stale_local_hooks_path_override(
159
163
  encoding="utf-8",
160
164
  errors="replace",
161
165
  check=False,
166
+ creationflags=hidden_window_creation_flags(),
162
167
  )
163
168
  except (FileNotFoundError, OSError):
164
169
  return
@@ -90,6 +90,8 @@ from codex_review_scripts_constants.run_constants import ( # noqa: E402
90
90
  VERSION_PROBE_PATTERN,
91
91
  )
92
92
 
93
+ from subprocess_window_access import hidden_window_creation_flags
94
+
93
95
 
94
96
  def _open_codex_popen(
95
97
  all_arguments: list[str],
@@ -110,6 +112,7 @@ def _open_codex_popen(
110
112
  encoding=stream_encoding if isinstance(stream_encoding, str) else None,
111
113
  env=process_environment if isinstance(process_environment, dict) else None,
112
114
  start_new_session=should_start_new_session(),
115
+ creationflags=hidden_window_creation_flags(),
113
116
  )
114
117
 
115
118
 
@@ -0,0 +1,41 @@
1
+ """Reach the hidden-window helpers from the scripts this directory deploys.
2
+
3
+ The installer copies this directory into a profile that holds no installed
4
+ Python distribution, and this repository's own tests run the deployed scripts
5
+ under ``python -S -E``, which hides every installed package from the child
6
+ process. On both routes ``hooks_constants`` is reachable only through the
7
+ deployed ``hooks`` directory, so this module puts that directory on
8
+ ``sys.path`` and re-exports the helpers a caller asks for::
9
+
10
+ from subprocess_window_access import hidden_window_creation_flags
11
+
12
+ The directory goes on the end of ``sys.path``. ``PathFinder`` runs ahead of
13
+ every editable-install finder, so a trailing entry still selects the deployed
14
+ copy, and the head of ``sys.path`` stays exactly as the caller left it. A
15
+ caller that prepended its own roots keeps them first.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ _subprocess_window_hooks_directory = str(
24
+ Path(__file__).resolve().parents[3] / "hooks"
25
+ )
26
+ if _subprocess_window_hooks_directory not in sys.path:
27
+ sys.path.append(_subprocess_window_hooks_directory)
28
+
29
+ from hooks_constants.subprocess_window import (
30
+ HiddenWindowStartupInfo,
31
+ detached_hidden_window_creation_flags,
32
+ hidden_window_creation_flags,
33
+ inherited_stream_startup_info,
34
+ )
35
+
36
+ __all__ = [
37
+ "HiddenWindowStartupInfo",
38
+ "detached_hidden_window_creation_flags",
39
+ "hidden_window_creation_flags",
40
+ "inherited_stream_startup_info",
41
+ ]
@@ -75,6 +75,8 @@ from pr_loop_shared_constants.terminology_sweep_constants import (
75
75
  TEST_FILE_SUFFIX,
76
76
  )
77
77
 
78
+ from subprocess_window_access import hidden_window_creation_flags
79
+
78
80
  IdentifierTuple = tuple[str, ...]
79
81
 
80
82
 
@@ -656,6 +658,7 @@ def _base_tree_names(
656
658
  timeout=GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS,
657
659
  check=False,
658
660
  env=repository_environment(),
661
+ creationflags=hidden_window_creation_flags(),
659
662
  )
660
663
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
661
664
  continue
@@ -678,6 +681,7 @@ def _run_strict_git(
678
681
  timeout=GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS,
679
682
  check=False,
680
683
  env=repository_environment(),
684
+ creationflags=hidden_window_creation_flags(),
681
685
  )
682
686
  except (subprocess.TimeoutExpired, OSError) as e:
683
687
  raise RuntimeError(str(e)) from e
@@ -807,6 +811,7 @@ def staged_terminology_findings(repository_root: Path) -> list[str]:
807
811
  timeout=GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS,
808
812
  check=False,
809
813
  env=repository_environment(),
814
+ creationflags=hidden_window_creation_flags(),
810
815
  )
811
816
  if diff_process.returncode != 0:
812
817
  return []
@@ -39,6 +39,8 @@ from process_tree_scripts_constants.process_tree_kill_constants import ( # noqa
39
39
  WINDOWS_TASKKILL_TREE_FLAG,
40
40
  )
41
41
 
42
+ from subprocess_window_access import hidden_window_creation_flags
43
+
42
44
  process_tree_subprocess_run = subprocess.run
43
45
 
44
46
 
@@ -73,6 +75,7 @@ def _kill_windows_process_tree(process_identifier: int) -> None:
73
75
  stderr=subprocess.DEVNULL,
74
76
  check=False,
75
77
  timeout=PROCESS_TREE_KILL_TIMEOUT_SECONDS,
78
+ creationflags=hidden_window_creation_flags(),
76
79
  )
77
80
  except (subprocess.TimeoutExpired, OSError):
78
81
  return
@@ -0,0 +1,41 @@
1
+ """Reach the hidden-window helpers from the scripts this directory deploys.
2
+
3
+ The installer copies this directory into a profile that holds no installed
4
+ Python distribution, and this repository's own tests run the deployed scripts
5
+ under ``python -S -E``, which hides every installed package from the child
6
+ process. On both routes ``hooks_constants`` is reachable only through the
7
+ deployed ``hooks`` directory, so this module puts that directory on
8
+ ``sys.path`` and re-exports the helpers a caller asks for::
9
+
10
+ from subprocess_window_access import hidden_window_creation_flags
11
+
12
+ The directory goes on the end of ``sys.path``. ``PathFinder`` runs ahead of
13
+ every editable-install finder, so a trailing entry still selects the deployed
14
+ copy, and the head of ``sys.path`` stays exactly as the caller left it. A
15
+ caller that prepended its own roots keeps them first.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ _subprocess_window_hooks_directory = str(
24
+ Path(__file__).resolve().parents[3] / "hooks"
25
+ )
26
+ if _subprocess_window_hooks_directory not in sys.path:
27
+ sys.path.append(_subprocess_window_hooks_directory)
28
+
29
+ from hooks_constants.subprocess_window import (
30
+ HiddenWindowStartupInfo,
31
+ detached_hidden_window_creation_flags,
32
+ hidden_window_creation_flags,
33
+ inherited_stream_startup_info,
34
+ )
35
+
36
+ __all__ = [
37
+ "HiddenWindowStartupInfo",
38
+ "detached_hidden_window_creation_flags",
39
+ "hidden_window_creation_flags",
40
+ "inherited_stream_startup_info",
41
+ ]
@@ -56,8 +56,10 @@ export const RUN_BACKUP_DIRECTORY_NAME_PATTERN =
56
56
  export const PACKAGE_AGENTS_HOME_DIRECTORY_NAME = '.agents';
57
57
 
58
58
  export const PSTACK_MODEL_RULE_FILE_NAME = 'pstack-models.mdc';
59
- export const PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME =
60
- 'pstack-model-preferences.codex.json';
59
+ export const PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES = Object.freeze([
60
+ 'pstack-model-preferences.claude.json',
61
+ 'pstack-model-preferences.codex.json',
62
+ ]);
61
63
 
62
64
  /**
63
65
  * The directory name skill directories carry in a package source and under
@@ -220,18 +220,60 @@ for (const targetName of [null, 'profile']) {
220
220
  model: 'gpt-5.6-sol',
221
221
  });
222
222
 
223
- const foreignPreferencesPath = join(
223
+ const claudePreferencesPath = join(
224
224
  resolution.agentsHome,
225
225
  'rules',
226
226
  'pstack-model-preferences.claude.json',
227
227
  );
228
- writeFileSync(foreignPreferencesPath, JSON.stringify({
228
+ const seededClaudePreferences = JSON.parse(
229
+ readFileSync(claudePreferencesPath, 'utf8'),
230
+ );
231
+ assert.deepEqual(seededClaudePreferences, {
232
+ host: 'claude',
233
+ modelsByRole: {
234
+ 'feature, refactoring': ['opus'],
235
+ 'bug-fix': ['opus'],
236
+ 'perf-issue': ['fable'],
237
+ hillclimb: ['fable'],
238
+ 'judgment and prose': ['fable'],
239
+ 'hardest tasks': ['fable'],
240
+ 'how explorer': ['sonnet'],
241
+ 'how explainer': ['opus'],
242
+ 'how critics': ['sonnet', 'opus', 'fable'],
243
+ 'why investigators': ['sonnet'],
244
+ 'why synthesizer': ['fable'],
245
+ 'reflect tooling': ['opus'],
246
+ 'reflect judgment, divergent, synthesizer': ['fable'],
247
+ 'arena runners': ['sonnet', 'opus', 'fable'],
248
+ 'arena cross-judge pool': ['opus', 'fable'],
249
+ 'swarm workers': ['sonnet'],
250
+ 'architect runners': ['opus', 'fable'],
251
+ 'interrogate reviewers': ['sonnet', 'opus', 'fable'],
252
+ },
253
+ });
254
+ const claudeSelection = runSelector(selectorPath, {
255
+ host: 'claude',
256
+ inventoryHost: 'claude',
257
+ role: 'swarm workers',
258
+ delegationIndex: 0,
259
+ availableModelIds: ['sonnet', 'opus', 'fable'],
260
+ confirmedSuitableModelIds: [],
261
+ parentFallback: {
262
+ isAllowed: false,
263
+ hasMaterialCapabilityLoss: false,
264
+ },
265
+ });
266
+ assert.deepEqual(claudeSelection.nativeSpawnArguments, {
267
+ model: 'sonnet',
268
+ });
269
+
270
+ writeFileSync(claudePreferencesPath, JSON.stringify({
229
271
  host: 'claude',
230
272
  modelsByRole: {
231
273
  'feature, refactoring': ['confirmed-claude-model'],
232
274
  },
233
275
  }));
234
- const foreignSelection = runSelector(selectorPath, {
276
+ const editedClaudeSelection = runSelector(selectorPath, {
235
277
  host: 'claude',
236
278
  inventoryHost: 'claude',
237
279
  role: 'feature, refactoring',
@@ -243,7 +285,7 @@ for (const targetName of [null, 'profile']) {
243
285
  hasMaterialCapabilityLoss: false,
244
286
  },
245
287
  });
246
- assert.deepEqual(foreignSelection.nativeSpawnArguments, {
288
+ assert.deepEqual(editedClaudeSelection.nativeSpawnArguments, {
247
289
  model: 'confirmed-claude-model',
248
290
  });
249
291
 
@@ -251,7 +293,7 @@ for (const targetName of [null, 'profile']) {
251
293
  assert.equal(manifest.files.includes(sharedPath), true);
252
294
  assert.equal(manifest.files.includes(managedSelectorPath), true);
253
295
  assert.equal(manifest.files.includes(preferencesPath), false);
254
- assert.equal(manifest.files.includes(foreignPreferencesPath), false);
296
+ assert.equal(manifest.files.includes(claudePreferencesPath), false);
255
297
 
256
298
  writeFileSync(sharedPath, 'stale');
257
299
  writeFileSync(preferencesPath, JSON.stringify({
@@ -268,7 +310,7 @@ for (const targetName of [null, 'profile']) {
268
310
  ['user-edited-codex-model'],
269
311
  );
270
312
  assert.deepEqual(
271
- JSON.parse(readFileSync(foreignPreferencesPath, 'utf8'))
313
+ JSON.parse(readFileSync(claudePreferencesPath, 'utf8'))
272
314
  .modelsByRole['feature, refactoring'],
273
315
  ['confirmed-claude-model'],
274
316
  );
@@ -295,7 +337,7 @@ for (const targetName of [null, 'profile']) {
295
337
  assert.equal(existsSync(cursorPath), false);
296
338
  assert.equal(existsSync(selectorPath), false);
297
339
  assert.equal(existsSync(preferencesPath), true);
298
- assert.equal(existsSync(foreignPreferencesPath), true);
340
+ assert.equal(existsSync(claudePreferencesPath), true);
299
341
  } finally {
300
342
  rmSync(homeDirectory, { recursive: true, force: true });
301
343
  }
@@ -317,21 +359,21 @@ test('a blocked shared rule destination rolls back generated Cursor files', () =
317
359
  }
318
360
  });
319
361
 
320
- test('a failed install rolls back a newly seeded Codex preference file', () => {
362
+ test('a failed install rolls back newly seeded host preference files', () => {
321
363
  const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-seed-rollback-'));
322
364
  try {
323
- const preferencesPath = join(
324
- homeDirectory,
325
- '.agents',
326
- 'rules',
327
- 'pstack-model-preferences.codex.json',
328
- );
365
+ const preferencesDirectory = join(homeDirectory, '.agents', 'rules');
329
366
  assert.throws(() => runInstaller(
330
367
  homeDirectory,
331
368
  ['--only', 'core'],
332
369
  { CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging' },
333
370
  ));
334
- assert.equal(existsSync(preferencesPath), false);
371
+ for (const hostName of ['claude', 'codex']) {
372
+ assert.equal(
373
+ existsSync(join(preferencesDirectory, 'pstack-model-preferences.' + hostName + '.json')),
374
+ false,
375
+ );
376
+ }
335
377
  } finally {
336
378
  rmSync(homeDirectory, { recursive: true, force: true });
337
379
  }
package/bin/install.mjs CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  CURSOR_SYNC_SCRIPT_FILE_NAME,
35
35
  CURSOR_RULES_DIRECTORY_NAME,
36
36
  PSTACK_MODEL_RULE_FILE_NAME,
37
- PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME,
37
+ PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES,
38
38
  WINDOWS_PYTHON_LAUNCHER_COMMAND,
39
39
  PYTHON_PROBE_TIMEOUT_MILLISECONDS,
40
40
  } from './install-constants.mjs';
@@ -491,17 +491,10 @@ export function refreshInstalledPstackPluginManifest(
491
491
  return join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
492
492
  }
493
493
 
494
- function seedCodexPstackPreferences() {
494
+ function seedPstackPreferences(preferenceFileName) {
495
495
  const preferenceDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
496
- const preferencePath = join(
497
- preferenceDirectory,
498
- PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME,
499
- );
500
- const preferenceSource = join(
501
- PACKAGE_ROOT,
502
- 'bin',
503
- PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME,
504
- );
496
+ const preferencePath = join(preferenceDirectory, preferenceFileName);
497
+ const preferenceSource = join(PACKAGE_ROOT, 'bin', preferenceFileName);
505
498
  mkdirSync(preferenceDirectory, { recursive: true });
506
499
  try {
507
500
  copyFileSync(
@@ -2756,10 +2749,12 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2756
2749
  copyFileSync(pstackRuleSource, sharedRulePath);
2757
2750
  allInstalledFiles.push(sharedRulePath);
2758
2751
  syncWrittenPaths(allInstalledFiles);
2759
- const seededPreferencePath = seedCodexPstackPreferences();
2760
- if (seededPreferencePath) {
2761
- allInstalledFiles.push(seededPreferencePath);
2762
- allUserOwnedPreferencePaths.add(seededPreferencePath);
2752
+ for (const eachPreferenceFileName of PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES) {
2753
+ const seededPreferencePath = seedPstackPreferences(eachPreferenceFileName);
2754
+ if (seededPreferencePath) {
2755
+ allInstalledFiles.push(seededPreferencePath);
2756
+ allUserOwnedPreferencePaths.add(seededPreferencePath);
2757
+ }
2763
2758
  }
2764
2759
  syncWrittenPaths(allInstalledFiles);
2765
2760
  }
@@ -0,0 +1,23 @@
1
+ {
2
+ "host": "claude",
3
+ "modelsByRole": {
4
+ "feature, refactoring": ["opus"],
5
+ "bug-fix": ["opus"],
6
+ "perf-issue": ["fable"],
7
+ "hillclimb": ["fable"],
8
+ "judgment and prose": ["fable"],
9
+ "hardest tasks": ["fable"],
10
+ "how explorer": ["sonnet"],
11
+ "how explainer": ["opus"],
12
+ "how critics": ["sonnet", "opus", "fable"],
13
+ "why investigators": ["sonnet"],
14
+ "why synthesizer": ["fable"],
15
+ "reflect tooling": ["opus"],
16
+ "reflect judgment, divergent, synthesizer": ["fable"],
17
+ "arena runners": ["sonnet", "opus", "fable"],
18
+ "arena cross-judge pool": ["opus", "fable"],
19
+ "swarm workers": ["sonnet"],
20
+ "architect runners": ["opus", "fable"],
21
+ "interrogate reviewers": ["sonnet", "opus", "fable"]
22
+ }
23
+ }
@@ -36,6 +36,12 @@ import subprocess
36
36
  import sys
37
37
  from pathlib import Path
38
38
 
39
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
40
+ if _subprocess_window_hooks_directory not in sys.path:
41
+ sys.path.append(_subprocess_window_hooks_directory)
42
+
43
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
44
+
39
45
  try:
40
46
  _hooks_root_directory = str(Path(__file__).resolve().parent.parent)
41
47
  if _hooks_root_directory not in sys.path:
@@ -294,6 +300,7 @@ def _probe_pull_request(cwd: str) -> str | None:
294
300
  errors="replace",
295
301
  timeout=GH_PR_VIEW_TIMEOUT_SECONDS,
296
302
  check=False,
303
+ creationflags=hidden_window_creation_flags(),
297
304
  )
298
305
  except (OSError, subprocess.TimeoutExpired):
299
306
  return None
@@ -11,6 +11,12 @@ import sys
11
11
  from collections import Counter
12
12
  from pathlib import Path
13
13
 
14
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
15
+ if _subprocess_window_hooks_directory not in sys.path:
16
+ sys.path.append(_subprocess_window_hooks_directory)
17
+
18
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
19
+
14
20
  try:
15
21
  hooks_root_directory = str(Path(__file__).resolve().parent.parent)
16
22
  if hooks_root_directory not in sys.path:
@@ -62,6 +68,7 @@ def _read_added_lines_from_git(all_git_arguments: tuple[str, ...], file_path: st
62
68
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
63
69
  cwd=working_directory,
64
70
  env=_git_environment(),
71
+ creationflags=hidden_window_creation_flags(),
65
72
  )
66
73
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
67
74
  return []
@@ -106,6 +113,7 @@ def is_new_file(file_path: str) -> bool:
106
113
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
107
114
  cwd=working_directory,
108
115
  env=_git_environment(),
116
+ creationflags=hidden_window_creation_flags(),
109
117
  )
110
118
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
111
119
  return False
@@ -28,6 +28,10 @@ except ImportError as import_error:
28
28
  ) from import_error
29
29
 
30
30
 
31
+ WINDOWS_NO_WINDOW_FLAG = 0x08000000
32
+ EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
33
+
34
+
31
35
  _CLEAN_PR_OBJECT: dict[str, object] = {
32
36
  "number": 42,
33
37
  "url": "https://github.com/acme/widgets/pull/42",
@@ -248,3 +252,20 @@ def test_main_should_stay_quiet_on_any_other_gh_failure(
248
252
 
249
253
  assert _run_main(monkeypatch, capsys, _payload("git push"), gh_down) == ""
250
254
 
255
+
256
+
257
+ def test_gh_probe_should_start_without_a_console_window(
258
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
259
+ ) -> None:
260
+ all_recorded_keyword_arguments: dict[str, object] = {}
261
+
262
+ def _record(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
263
+ del args
264
+ all_recorded_keyword_arguments.update(kwargs)
265
+ return subprocess.CompletedProcess(
266
+ [], 0, stdout=json.dumps(_CLEAN_PR_OBJECT), stderr=""
267
+ )
268
+
269
+ _run_main(monkeypatch, capsys, _payload("git push"), _record)
270
+
271
+ assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
@@ -9,9 +9,12 @@ return no added lines, so a qualifying rename is eligible without fixture setup.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import subprocess
12
13
  import sys
13
14
  from pathlib import Path
14
15
 
16
+ import pytest
17
+
15
18
  try:
16
19
  _ADVISORY_DIRECTORY = Path(__file__).resolve().parent
17
20
  if str(_ADVISORY_DIRECTORY) not in sys.path:
@@ -25,6 +28,10 @@ except ImportError as import_error:
25
28
  ) from import_error
26
29
 
27
30
 
31
+ WINDOWS_NO_WINDOW_FLAG = 0x08000000
32
+ EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
33
+
34
+
28
35
  def _multi_edit_payload(file_path: str, all_edits: list[dict[str, str]]) -> dict[str, object]:
29
36
  return {
30
37
  "tool_name": "MultiEdit",
@@ -85,3 +92,36 @@ def test_multi_edit_returns_none_when_no_edit_qualifies(tmp_path: Path) -> None:
85
92
  )
86
93
 
87
94
  assert refactor_guard._multi_edit_refactor_advisory_description(payload) is None
95
+
96
+
97
+ def _record_creation_flags(
98
+ monkeypatch: pytest.MonkeyPatch, standard_output: str
99
+ ) -> dict[str, object]:
100
+ all_recorded_keyword_arguments: dict[str, object] = {}
101
+
102
+ def _record(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
103
+ del args
104
+ all_recorded_keyword_arguments.update(kwargs)
105
+ return subprocess.CompletedProcess([], 0, stdout=standard_output, stderr="")
106
+
107
+ monkeypatch.setattr(refactor_guard.subprocess, "run", _record)
108
+ return all_recorded_keyword_arguments
109
+
110
+
111
+ def test_git_diff_should_start_without_a_console_window(
112
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
113
+ ) -> None:
114
+ all_recorded_keyword_arguments = _record_creation_flags(monkeypatch, "+added line\n")
115
+
116
+ refactor_guard.get_git_diff_added_lines(str(tmp_path / "module.py"))
117
+
118
+ assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
119
+
120
+
121
+ def test_untracked_probe_should_start_without_a_console_window(
122
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
123
+ ) -> None:
124
+ all_recorded_keyword_arguments = _record_creation_flags(monkeypatch, "module.py\n")
125
+
126
+ assert refactor_guard.is_new_file(str(tmp_path / "module.py")) is True
127
+ assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
@@ -23,6 +23,8 @@ 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.subprocess_window import hidden_window_creation_flags
27
+
26
28
  GIT_COMMAND_TIMEOUT_SECONDS = 5
27
29
  PROTECTED_BRANCHES = ("main", "master")
28
30
  PROTECTED_REMOTE_PATTERNS: list[str] = []
@@ -111,6 +113,7 @@ def get_branch_at_directory(working_dir: str | None = None) -> str | None:
111
113
  text=True,
112
114
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
113
115
  cwd=working_dir,
116
+ creationflags=hidden_window_creation_flags(),
114
117
  )
115
118
  if completed_process.returncode == 0:
116
119
  return completed_process.stdout.strip()
@@ -130,6 +133,7 @@ def is_protected_repo(working_dir: str | None = None) -> bool:
130
133
  text=True,
131
134
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
132
135
  cwd=working_dir,
136
+ creationflags=hidden_window_creation_flags(),
133
137
  )
134
138
  if completed_process.returncode == 0:
135
139
  remote_url = completed_process.stdout.strip()