claude-dev-env 7.5.0 → 7.6.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 (125) 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/session-continuity/README.md +19 -9
  7. package/.agents/skills/syncing-submodules/SKILL.md +1 -0
  8. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
  9. package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
  10. package/.agents/skills/test-runner/SKILL.md +1 -0
  11. package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
  12. package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
  13. package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
  14. package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
  15. package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
  16. package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
  17. package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
  24. package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
  25. package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
  26. package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
  27. package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
  28. package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
  29. package/_shared/pr-loop/scripts/preflight.py +7 -0
  30. package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
  31. package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
  32. package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
  33. package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
  34. package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
  35. package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
  36. package/bin/install-constants.mjs +4 -2
  37. package/bin/install-session-continuity.mjs +73 -30
  38. package/bin/install-session-continuity.test.mjs +52 -1
  39. package/bin/install.cursor-rules.test.mjs +57 -15
  40. package/bin/install.mjs +80 -15
  41. package/bin/install.test.mjs +57 -0
  42. package/bin/pstack-model-preferences.claude.json +23 -0
  43. package/hooks/advisory/pr_done_reminder.py +7 -0
  44. package/hooks/advisory/refactor_guard.py +8 -0
  45. package/hooks/advisory/test_pr_done_reminder.py +21 -0
  46. package/hooks/advisory/test_refactor_guard.py +40 -0
  47. package/hooks/blocking/block_main_commit.py +4 -0
  48. package/hooks/blocking/code_rules_comments.py +39 -5
  49. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
  50. package/hooks/blocking/precommit_code_rules_gate.py +3 -0
  51. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
  52. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
  53. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
  54. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
  55. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
  56. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
  57. package/hooks/git-hooks/pull_request_handoff.py +3 -0
  58. package/hooks/git-hooks/subprocess_window_access.py +41 -0
  59. package/hooks/git-hooks/test_verification_start.py +4 -0
  60. package/hooks/git-hooks/verification_notice_context.py +3 -0
  61. package/hooks/git-hooks/verification_start.py +7 -8
  62. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
  63. package/hooks/hooks_constants/subprocess_window.py +141 -0
  64. package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
  65. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
  66. package/hooks/hooks_constants/test_subprocess_window.py +128 -0
  67. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
  68. package/hooks/validation/mypy_validator.py +4 -0
  69. package/hooks/validators/README.md +1 -0
  70. package/hooks/validators/git_checks.py +4 -0
  71. package/hooks/validators/health_check.py +3 -0
  72. package/hooks/validators/hook_timing_harness.py +3 -0
  73. package/hooks/validators/mypy_integration.py +4 -0
  74. package/hooks/validators/ruff_integration.py +6 -0
  75. package/hooks/validators/run_all_validators.py +7 -0
  76. package/hooks/validators/subprocess_window_access.py +41 -0
  77. package/hooks/workflow/auto_formatter.py +8 -0
  78. package/hooks/workflow/test_auto_formatter.py +39 -0
  79. package/package.json +1 -1
  80. package/rules/pstack-models.md +4 -3
  81. package/scripts/automatic_advisory/checkout.py +8 -1
  82. package/scripts/automatic_advisory/cli.py +9 -3
  83. package/scripts/automatic_advisory/config/constants.py +0 -2
  84. package/scripts/automatic_advisory/execution.py +7 -1
  85. package/scripts/automatic_advisory/git.py +9 -1
  86. package/scripts/automatic_advisory/process_host.py +3 -0
  87. package/scripts/claude_chain_runner.py +3 -0
  88. package/scripts/gh_artifact_upload.py +3 -0
  89. package/scripts/grok_headless_runner.py +3 -0
  90. package/scripts/grok_patch_artifacts.py +4 -0
  91. package/scripts/grok_worker_preflight.py +3 -0
  92. package/scripts/invoke_code_review.py +3 -0
  93. package/scripts/local_verification/command_runner.py +3 -0
  94. package/scripts/local_verification/git_state.py +3 -0
  95. package/scripts/policy_lint/adapter_detectors.py +48 -2
  96. package/scripts/policy_lint/adapter_pairing.py +150 -2
  97. package/scripts/policy_lint/adapters.py +46 -4
  98. package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
  99. package/scripts/policy_lint/config/constants.py +3 -0
  100. package/scripts/policy_lint/registry.py +1 -1
  101. package/scripts/policy_lint/selection_git.py +11 -0
  102. package/scripts/repository_checks/config/constants.py +5 -0
  103. package/scripts/repository_checks/config/policy_document.py +19 -0
  104. package/scripts/repository_checks/email_exemptions.py +14 -71
  105. package/scripts/repository_checks/path_exemptions.py +35 -0
  106. package/scripts/repository_checks/policy_document.py +120 -0
  107. package/scripts/repository_checks/tracked_secrets.py +27 -4
  108. package/scripts/setup_project_paths.py +3 -0
  109. package/scripts/spawn_grok_batch.py +3 -0
  110. package/scripts/subprocess_window_access.py +41 -0
  111. package/scripts/test_dispatcher_profile_import.py +4 -0
  112. package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
  113. package/scripts/tests/test_email_exemptions.py +37 -0
  114. package/scripts/tests/test_path_exemptions.py +164 -0
  115. package/scripts/tests/test_policy_document.py +104 -0
  116. package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
  117. package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
  118. package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
  119. package/scripts/tests/test_policy_lint_selection.py +34 -0
  120. package/scripts/tests/test_subprocess_window_access.py +118 -0
  121. package/scripts/tests/test_tracked_secrets.py +79 -0
  122. package/scripts/validate_instruction_pairs.py +3 -0
  123. package/scripts/verify_installable_package.py +5 -0
  124. package/scripts/automatic_advisory/window_flags.py +0 -41
  125. package/scripts/repository_checks/config/email_exemptions.py +0 -10
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';
@@ -44,6 +44,11 @@ import {
44
44
  refreshPstackPluginManifest,
45
45
  } from '../scripts/refresh_pstack_plugin_skills.mjs';
46
46
  import { installPstack } from './pstack.mjs';
47
+ import {
48
+ configureContinuityHosts,
49
+ continuityHostConfigurationPaths,
50
+ removeContinuityHooks,
51
+ } from './install-session-continuity.mjs';
47
52
  import {
48
53
  resolveInstallRoot,
49
54
  parseExplicitTargetFromArgv,
@@ -491,17 +496,10 @@ export function refreshInstalledPstackPluginManifest(
491
496
  return join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
492
497
  }
493
498
 
494
- function seedCodexPstackPreferences() {
499
+ function seedPstackPreferences(preferenceFileName) {
495
500
  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
- );
501
+ const preferencePath = join(preferenceDirectory, preferenceFileName);
502
+ const preferenceSource = join(PACKAGE_ROOT, 'bin', preferenceFileName);
505
503
  mkdirSync(preferenceDirectory, { recursive: true });
506
504
  try {
507
505
  copyFileSync(
@@ -516,6 +514,63 @@ function seedCodexPstackPreferences() {
516
514
  }
517
515
  }
518
516
 
517
+ /**
518
+ * Registers the session-continuity companion in each host configuration this
519
+ * install root resolves. Without this step the companion ships to the agents
520
+ * home but no host ever calls it, so Poteto Mode only activates when someone
521
+ * remembers a second setup command.
522
+ *
523
+ * A competing profile's registration, or an unwritable host configuration, is
524
+ * reported and leaves the rest of the install intact.
525
+ *
526
+ * @returns {string[]} The host configuration paths this run registered.
527
+ */
528
+ function registerSessionContinuityHooks() {
529
+ const configurationPaths = continuityHostConfigurationPaths(INSTALL_ROOT_RESOLUTION);
530
+ const hosts = Object.keys(configurationPaths)
531
+ .filter(host => existsSync(dirname(configurationPaths[host])));
532
+ if (hosts.length === 0) return [];
533
+ try {
534
+ const results = configureContinuityHosts(INSTALL_ROOT_RESOLUTION, hosts);
535
+ for (const result of results) {
536
+ const state = result.changed ? 'registered' : 'already registered';
537
+ console.log(` Session continuity: ${state} in ${result.path}`);
538
+ }
539
+ return results.map(result => result.path);
540
+ } catch (registrationError) {
541
+ console.warn(
542
+ ` Warning: session-continuity hooks were not registered (${registrationError.message}) — `
543
+ + 'run bin/install-session-continuity.mjs once the cause is resolved.',
544
+ );
545
+ return [];
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Removes the companion's registrations from the Codex and Cursor hook files
551
+ * during an uninstall. The Claude settings.json copy is pruned inside the
552
+ * settings block, which owns that file's single write.
553
+ *
554
+ * Each host this installer registered is a host it has to clean, or the host
555
+ * keeps calling a script the uninstall deleted.
556
+ *
557
+ * @returns {string[]} The host configuration paths this run changed.
558
+ */
559
+ function removeSessionContinuityHooksFromOtherHosts() {
560
+ const configurationPaths = continuityHostConfigurationPaths(INSTALL_ROOT_RESOLUTION);
561
+ const changedPaths = [];
562
+ for (const host of ['codex', 'cursor']) {
563
+ const configurationPath = configurationPaths[host];
564
+ if (!existsSync(configurationPath)) continue;
565
+ const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
566
+ if (removeContinuityHooks(configuration) === 0) continue;
567
+ writeFileSync(configurationPath, JSON.stringify(configuration, null, 2) + '\n');
568
+ changedPaths.push(configurationPath);
569
+ console.log(` Session continuity: hook registrations removed from ${configurationPath}`);
570
+ }
571
+ return changedPaths;
572
+ }
573
+
519
574
  /**
520
575
  * Formats an absolute interpreter path as a settings.json hook command prefix:
521
576
  * forward-slash separators, double-quoted when the path contains a space so the
@@ -2756,10 +2811,12 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2756
2811
  copyFileSync(pstackRuleSource, sharedRulePath);
2757
2812
  allInstalledFiles.push(sharedRulePath);
2758
2813
  syncWrittenPaths(allInstalledFiles);
2759
- const seededPreferencePath = seedCodexPstackPreferences();
2760
- if (seededPreferencePath) {
2761
- allInstalledFiles.push(seededPreferencePath);
2762
- allUserOwnedPreferencePaths.add(seededPreferencePath);
2814
+ for (const eachPreferenceFileName of PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES) {
2815
+ const seededPreferencePath = seedPstackPreferences(eachPreferenceFileName);
2816
+ if (seededPreferencePath) {
2817
+ allInstalledFiles.push(seededPreferencePath);
2818
+ allUserOwnedPreferencePaths.add(seededPreferencePath);
2819
+ }
2763
2820
  }
2764
2821
  syncWrittenPaths(allInstalledFiles);
2765
2822
  }
@@ -2927,6 +2984,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2927
2984
  console.log(` \u2713 ${relative(CLAUDE_HOME, agentsHubDest)} (canonical guidance)`);
2928
2985
  }
2929
2986
  const isFullInstall = !selectedGroups;
2987
+ if (isFullInstall && shouldInstallAnyHooks) {
2988
+ summary.sessionContinuity = { configuredPaths: registerSessionContinuityHooks() };
2989
+ }
2930
2990
  const didPruneRun = isFullInstall && UNRESOLVED_DEPENDENCY_NAMES.length === 0;
2931
2991
  let failedPrunePaths = [];
2932
2992
  let stalePrunedTotal = 0;
@@ -3144,6 +3204,10 @@ function executeUninstallPlan(plan, helpers = {}) {
3144
3204
  pruneManagedHooksFromSettings(settings, managedHookRelativePaths);
3145
3205
  didSettingsChange = true;
3146
3206
  console.log(' Hook entries removed from settings.json');
3207
+ const removedContinuityCount = removeContinuityHooks(settings);
3208
+ if (removedContinuityCount > 0) {
3209
+ console.log(` Session continuity: ${removedContinuityCount} hook registration(s) removed from settings.json`);
3210
+ }
3147
3211
  }
3148
3212
  const managedDenyFromPlan = plan.managedPermissionDenyEntries.length > 0
3149
3213
  ? plan.managedPermissionDenyEntries
@@ -3161,6 +3225,7 @@ function executeUninstallPlan(plan, helpers = {}) {
3161
3225
  writeFileSync(plan.settingsPath, JSON.stringify(settings, null, 4) + '\n');
3162
3226
  }
3163
3227
  }
3228
+ removeSessionContinuityHooksFromOtherHosts();
3164
3229
  throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
3165
3230
 
3166
3231
  unsetGlobalGitHooksPathIfOurs();
@@ -3088,3 +3088,60 @@ test('--no-pstack leaves the pstack store absent', t => {
3088
3088
  assert.equal(existsSync(join(homeDirectory, '.claude', 'pstack')), false);
3089
3089
  });
3090
3090
  });
3091
+
3092
+ function runContinuityInstaller(homeDirectory, extraArguments) {
3093
+ return execFileSync('node', [PSTACK_TEST_INSTALLER_PATH, ...extraArguments], {
3094
+ cwd: dirname(PSTACK_TEST_PACKAGE_ROOT),
3095
+ encoding: 'utf8',
3096
+ env: {
3097
+ ...process.env,
3098
+ CDE_INSTALL_PSTACK: '0',
3099
+ HOME: homeDirectory,
3100
+ USERPROFILE: homeDirectory,
3101
+ GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
3102
+ CODEX_HOME: join(homeDirectory, '.codex'),
3103
+ },
3104
+ });
3105
+ }
3106
+
3107
+ function continuityCommandCount(configurationPath) {
3108
+ if (!existsSync(configurationPath)) return 0;
3109
+ const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
3110
+ const allCommands = Object.values(configuration.hooks || {})
3111
+ .flat()
3112
+ .flatMap(entry => (entry.hooks ? entry.hooks.map(eachHook => eachHook.command) : [entry.command]));
3113
+ return allCommands.filter(
3114
+ eachCommand => typeof eachCommand === 'string'
3115
+ && eachCommand.includes('session-continuity/continuity.mjs'),
3116
+ ).length;
3117
+ }
3118
+
3119
+ test('a full install registers the session-continuity companion in every host, and uninstall takes it back out', t => {
3120
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-continuity-install-'));
3121
+ t.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
3122
+ const allConfigurationPaths = [
3123
+ join(homeDirectory, '.claude', 'settings.json'),
3124
+ join(homeDirectory, '.codex', 'hooks.json'),
3125
+ join(homeDirectory, '.cursor', 'hooks.json'),
3126
+ ];
3127
+
3128
+ runContinuityInstaller(homeDirectory, []);
3129
+
3130
+ for (const configurationPath of allConfigurationPaths) {
3131
+ assert.equal(
3132
+ continuityCommandCount(configurationPath) > 0,
3133
+ true,
3134
+ `the install registers the companion in ${configurationPath} without a second setup command`,
3135
+ );
3136
+ }
3137
+
3138
+ runContinuityInstaller(homeDirectory, ['--uninstall']);
3139
+
3140
+ for (const configurationPath of allConfigurationPaths) {
3141
+ assert.equal(
3142
+ continuityCommandCount(configurationPath),
3143
+ 0,
3144
+ `uninstall leaves no registration in ${configurationPath} pointing at a removed script`,
3145
+ );
3146
+ }
3147
+ });
@@ -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()
@@ -30,6 +30,7 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
30
30
  ALL_TOKEN_ANCHORED_EXEMPT_COMMENT_BODIES,
31
31
  CHAINED_INLINE_COMMENT_PATTERN,
32
32
  MAX_COMMENT_ISSUES,
33
+ STEALTH_KEEP_COMMENT_MARKER,
33
34
  )
34
35
  _javascript_comment_scanner = importlib.import_module("javascript_comment_scanner")
35
36
  extract_javascript_comment_occurrences = (
@@ -152,10 +153,36 @@ def _python_comment_occurrences(
152
153
  return all_occurrences, True
153
154
 
154
155
 
156
+ def _is_stealth_keep_occurrence(comment_text: str) -> bool:
157
+ """Return True for a ``# STEALTH: Keep`` occurrence.
158
+
159
+ ``STEALTH: Keep`` is meant to be freely added and to survive edits to
160
+ the line it sits on, unlike ``noqa`` or ``TODO``, which AGENTS.md
161
+ requires to be removed rather than added or justified. It is excluded
162
+ here rather than through the shared exempt-marker set that
163
+ ``check_comment_changes`` deliberately still blocks on add.
164
+ """
165
+ return comment_text.startswith("#") and comment_text[1:].lstrip().startswith(
166
+ STEALTH_KEEP_COMMENT_MARKER
167
+ )
168
+
169
+
170
+ def _without_stealth_keep_occurrences(
171
+ all_occurrences: list[tuple[str, int, bool]]
172
+ ) -> list[tuple[str, int, bool]]:
173
+ return [
174
+ each_occurrence
175
+ for each_occurrence in all_occurrences
176
+ if not _is_stealth_keep_occurrence(each_occurrence[0])
177
+ ]
178
+
179
+
155
180
  def check_comment_changes(old_content: str, new_content: str, file_path: str) -> list[str]:
156
181
  """Check for comment additions or removals between old and new content.
157
182
 
158
- Inline and standalone comments are blocking findings when added.
183
+ Inline and standalone comments are blocking findings when added, except
184
+ ``STEALTH: Keep`` occurrences, which are excluded entirely so the marker
185
+ can be added and can persist across edits to its line.
159
186
  Existing comments can be removed when the touched code no longer needs them.
160
187
 
161
188
  When the file is Python and either *old_content* or *new_content* cannot
@@ -178,6 +205,9 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
178
205
  old_occurrences = extract_javascript_comment_occurrences(old_content, True)
179
206
  new_occurrences = extract_javascript_comment_occurrences(new_content, True)
180
207
 
208
+ old_occurrences = _without_stealth_keep_occurrences(old_occurrences)
209
+ new_occurrences = _without_stealth_keep_occurrences(new_occurrences)
210
+
181
211
  old_occurrence_counts = Counter(
182
212
  (each_text, is_inline)
183
213
  for each_text, _line_number, is_inline in old_occurrences
@@ -264,6 +294,8 @@ def _retained_comment_issues(
264
294
  (old_occurrences, old_tokenize_ok), (new_occurrences, new_tokenize_ok) = (_comment_occurrences(old_content, file_path, True), _comment_occurrences(new_content, file_path, True))
265
295
  if not (old_tokenize_ok and new_tokenize_ok):
266
296
  return []
297
+ old_occurrences = _without_stealth_keep_occurrences(old_occurrences)
298
+ new_occurrences = _without_stealth_keep_occurrences(new_occurrences)
267
299
  all_changed_lines, all_deleted_lines, old_line_by_new_line = _line_diff_data(old_content, new_content)
268
300
  old_line_by_key = {
269
301
  each_key: [each_line for each_text, each_line, each_is_inline in old_occurrences if (each_text, each_is_inline) == each_key]
@@ -346,12 +378,14 @@ def _is_exempt_python_comment(comment_token: tokenize.TokenInfo) -> bool:
346
378
  token-anchored directive body never legitimately carries a ``#``
347
379
  (noqa codes, pylint symbols, and pragma directives contain none), so
348
380
  any inner ``#`` reliably marks chained prose. Free-form markers
349
- (``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``) accept any
350
- trailing prose:
381
+ (``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``, ``STEALTH: Keep``)
382
+ accept any trailing prose:
351
383
  ``# type:`` participates in the documented justification
352
384
  convention enforced by ``check_type_escape_hatches`` (which
353
- requires a trailing reason), and the TODO-family markers carry
354
- annotation text by convention.
385
+ requires a trailing reason), the TODO-family markers carry
386
+ annotation text by convention, and ``STEALTH: Keep`` is the
387
+ acknowledged-bypass marker the CDP runtime-usage scanner reads
388
+ from the flagged line together with its reason.
355
389
  """
356
390
  comment_string = comment_token.string
357
391
  if comment_string.startswith("#!") and comment_token.start == (1, 0):
@@ -16,6 +16,8 @@ import sys
16
16
  from pathlib import Path
17
17
  from urllib.parse import urlparse
18
18
 
19
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
20
+
19
21
  try:
20
22
  _blocking_directory = str(Path(__file__).resolve().parent.parent)
21
23
  _hooks_directory = str(Path(__file__).resolve().parent.parent.parent)
@@ -186,6 +188,7 @@ def _repository_origin_slug(repository_root: Path) -> str | None:
186
188
  text=True,
187
189
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
188
190
  cwd=str(repository_root),
191
+ creationflags=hidden_window_creation_flags(),
189
192
  )
190
193
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
191
194
  return None
@@ -18,6 +18,8 @@ from hooks_constants.precommit_code_rules_gate_constants import ( # noqa: E402
18
18
  GIT_COMMAND_TIMEOUT_SECONDS,
19
19
  )
20
20
 
21
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
22
+
21
23
 
22
24
  def resolve_repository_root(working_directory: str | None) -> Path | None:
23
25
  """Resolve the Git repository root for a directory.
@@ -36,6 +38,7 @@ def resolve_repository_root(working_directory: str | None) -> Path | None:
36
38
  text=True,
37
39
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
38
40
  cwd=working_directory,
41
+ creationflags=hidden_window_creation_flags(),
39
42
  )
40
43
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
41
44
  return None
@@ -30,6 +30,10 @@ def _safe_constant_attribute_calls() -> frozenset[tuple[str, str]]:
30
30
  return frozenset({("re", "compile")})
31
31
 
32
32
 
33
+ def _safe_string_literal_methods() -> frozenset[str]:
34
+ return frozenset({"join"})
35
+
36
+
33
37
  def _node_is_unsafe_construct(subnode: ast.AST) -> bool:
34
38
  return isinstance(
35
39
  subnode,
@@ -37,7 +41,18 @@ def _node_is_unsafe_construct(subnode: ast.AST) -> bool:
37
41
  )
38
42
 
39
43
 
44
+ def _is_safe_string_literal_method_call(function_node: ast.Attribute) -> bool:
45
+ receiver = function_node.value
46
+ return (
47
+ isinstance(receiver, ast.Constant)
48
+ and isinstance(receiver.value, str)
49
+ and function_node.attr in _safe_string_literal_methods()
50
+ )
51
+
52
+
40
53
  def _attribute_call_is_unsafe(function_node: ast.Attribute) -> bool:
54
+ if _is_safe_string_literal_method_call(function_node):
55
+ return False
41
56
  if not isinstance(function_node.value, ast.Name):
42
57
  return True
43
58
  pair = (function_node.value.id, function_node.attr)
@@ -56,9 +71,10 @@ def _call_is_unsafe(call_node: ast.Call) -> bool:
56
71
  def _rhs_has_unsafe_call(rhs_node: ast.AST) -> bool:
57
72
  """Return True when a constant's value contains a non-allowlisted call.
58
73
 
59
- Safe calls are value constructors (``Path(...)``, ``re.compile(...)``) that
60
- build objects without side effects; any other call or a comprehension reads
61
- as import-time behavior.
74
+ Safe calls are value constructors (``Path(...)``, ``re.compile(...)``) and
75
+ a pure string-literal method call (``"|".join(...)``) that build objects
76
+ without side effects; any other call or a comprehension reads as
77
+ import-time behavior.
62
78
 
63
79
  Args:
64
80
  rhs_node: The assignment's right-hand-side expression node.
@@ -29,6 +29,8 @@ from tdd_enforcer_parts.config.tdd_enforcer_constants import (
29
29
  GIT_PATHSPEC_SEPARATOR,
30
30
  )
31
31
 
32
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
33
+
32
34
 
33
35
  def _git_ls_files_command(file_name: str) -> list[str]:
34
36
  return [GIT_EXECUTABLE_NAME, GIT_LS_FILES_SUBCOMMAND, GIT_PATHSPEC_SEPARATOR, file_name]
@@ -57,6 +59,7 @@ def _git_tracks_path(path: Path) -> bool:
57
59
  text=True,
58
60
  check=False,
59
61
  timeout=GIT_LS_FILES_TIMEOUT_SECONDS,
62
+ creationflags=hidden_window_creation_flags(),
60
63
  )
61
64
  except (OSError, subprocess.SubprocessError):
62
65
  return False
@@ -80,6 +83,7 @@ def _working_tree_differs_from_head(path: Path) -> bool:
80
83
  text=True,
81
84
  check=False,
82
85
  timeout=GIT_DIFF_TIMEOUT_SECONDS,
86
+ creationflags=hidden_window_creation_flags(),
83
87
  )
84
88
  except (OSError, subprocess.SubprocessError):
85
89
  return True
@@ -12,6 +12,31 @@ def test_constants_only_rejects_function_definition() -> None:
12
12
  assert content_analysis._is_constants_only_python_content("def f():\n return 1\n") is False
13
13
 
14
14
 
15
+ def test_constants_only_accepts_join_on_a_string_literal_inside_re_compile() -> None:
16
+ content = (
17
+ '"""doc."""\n'
18
+ "import re\n"
19
+ "NAMES = ('one', 'two')\n"
20
+ 'PATTERN = re.compile("(" + "|".join(NAMES) + ")")\n'
21
+ )
22
+ assert content_analysis._is_constants_only_python_content(content) is True
23
+
24
+
25
+ def test_constants_only_rejects_join_on_a_non_literal_receiver() -> None:
26
+ content = (
27
+ '"""doc."""\n'
28
+ 'SEPARATOR = "|"\n'
29
+ 'NAMES = ("one", "two")\n'
30
+ "PATTERN = SEPARATOR.join(NAMES)\n"
31
+ )
32
+ assert content_analysis._is_constants_only_python_content(content) is False
33
+
34
+
35
+ def test_constants_only_rejects_a_non_allowlisted_method_on_a_string_literal() -> None:
36
+ content = '"""doc."""\nSHOUT = "hi".upper()\n'
37
+ assert content_analysis._is_constants_only_python_content(content) is False
38
+
39
+
15
40
  def test_post_edit_import_only_allows_import_removal() -> None:
16
41
  existing = "import os\n\ndef run(): return 1\n"
17
42
  tool_input = {"old_string": "import os\n", "new_string": ""}
@@ -133,6 +133,33 @@ def test_check_comment_changes_allows_comment_removal_without_advisory(
133
133
  assert captured.err == ""
134
134
 
135
135
 
136
+ def test_check_comment_changes_does_not_flag_added_stealth_keep_marker() -> None:
137
+ old_content = "x = 1\n"
138
+ new_content = "x = 1 # STEALTH: Keep -- reason\n"
139
+ issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
140
+ assert issues == []
141
+
142
+
143
+ def test_check_comment_changes_does_not_flag_stealth_keep_retained_on_touched_line() -> None:
144
+ old_content = "x = 1 # STEALTH: Keep -- reason\n"
145
+ new_content = "x = 2 # STEALTH: Keep -- reason\n"
146
+ issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
147
+ assert issues == []
148
+
149
+
150
+ @pytest.mark.parametrize(
151
+ "each_marker",
152
+ ("# noqa: E501", "# type: ignore[misc]", "# TODO: revisit", "# FIXME: revisit"),
153
+ )
154
+ def test_check_comment_changes_still_flags_other_added_directive_markers(
155
+ each_marker: str,
156
+ ) -> None:
157
+ old_content = "x = 1\n"
158
+ new_content = f"x = 1 {each_marker}\n"
159
+ issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
160
+ assert any("comment added" in each_issue.lower() for each_issue in issues)
161
+
162
+
136
163
  def test_python_check_should_exempt_directive_without_space_after_hash() -> None:
137
164
  for each_directive in ("#noqa", "#type: ignore", "#pylint: disable", "#pragma: no cover"):
138
165
  content = f"{each_directive}\n"