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.
- package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
- package/.agents/skills/fresh-branch/SKILL.md +1 -0
- package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
- package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/syncing-submodules/SKILL.md +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
- package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/SKILL.md +1 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
- package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
- package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
- package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
- package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
- package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
- package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
- package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
- package/_shared/pr-loop/scripts/preflight.py +7 -0
- package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
- package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
- package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
- package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
- package/bin/install-constants.mjs +4 -2
- package/bin/install.cursor-rules.test.mjs +57 -15
- package/bin/install.mjs +10 -15
- package/bin/pstack-model-preferences.claude.json +23 -0
- package/hooks/advisory/pr_done_reminder.py +7 -0
- package/hooks/advisory/refactor_guard.py +8 -0
- package/hooks/advisory/test_pr_done_reminder.py +21 -0
- package/hooks/advisory/test_refactor_guard.py +40 -0
- package/hooks/blocking/block_main_commit.py +4 -0
- package/hooks/blocking/code_rules_comments.py +39 -5
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +3 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
- package/hooks/git-hooks/pull_request_handoff.py +3 -0
- package/hooks/git-hooks/subprocess_window_access.py +41 -0
- package/hooks/git-hooks/test_verification_start.py +4 -0
- package/hooks/git-hooks/verification_notice_context.py +3 -0
- package/hooks/git-hooks/verification_start.py +7 -8
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/subprocess_window.py +141 -0
- package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/test_subprocess_window.py +128 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
- package/hooks/validation/mypy_validator.py +4 -0
- package/hooks/validators/README.md +1 -0
- package/hooks/validators/git_checks.py +4 -0
- package/hooks/validators/health_check.py +3 -0
- package/hooks/validators/hook_timing_harness.py +3 -0
- package/hooks/validators/mypy_integration.py +4 -0
- package/hooks/validators/ruff_integration.py +6 -0
- package/hooks/validators/run_all_validators.py +7 -0
- package/hooks/validators/subprocess_window_access.py +41 -0
- package/hooks/workflow/auto_formatter.py +8 -0
- package/hooks/workflow/test_auto_formatter.py +39 -0
- package/package.json +1 -1
- package/rules/pstack-models.md +4 -3
- package/scripts/automatic_advisory/checkout.py +8 -1
- package/scripts/automatic_advisory/cli.py +9 -3
- package/scripts/automatic_advisory/config/constants.py +0 -2
- package/scripts/automatic_advisory/execution.py +7 -1
- package/scripts/automatic_advisory/git.py +9 -1
- package/scripts/automatic_advisory/process_host.py +3 -0
- package/scripts/claude_chain_runner.py +3 -0
- package/scripts/gh_artifact_upload.py +3 -0
- package/scripts/grok_headless_runner.py +3 -0
- package/scripts/grok_patch_artifacts.py +4 -0
- package/scripts/grok_worker_preflight.py +3 -0
- package/scripts/invoke_code_review.py +3 -0
- package/scripts/local_verification/command_runner.py +3 -0
- package/scripts/local_verification/git_state.py +3 -0
- package/scripts/policy_lint/adapter_detectors.py +48 -2
- package/scripts/policy_lint/adapter_pairing.py +150 -2
- package/scripts/policy_lint/adapters.py +46 -4
- package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection_git.py +11 -0
- package/scripts/repository_checks/config/constants.py +5 -0
- package/scripts/repository_checks/config/policy_document.py +19 -0
- package/scripts/repository_checks/email_exemptions.py +14 -71
- package/scripts/repository_checks/path_exemptions.py +35 -0
- package/scripts/repository_checks/policy_document.py +120 -0
- package/scripts/repository_checks/tracked_secrets.py +27 -4
- package/scripts/setup_project_paths.py +3 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/subprocess_window_access.py +41 -0
- package/scripts/test_dispatcher_profile_import.py +4 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
- package/scripts/tests/test_email_exemptions.py +37 -0
- package/scripts/tests/test_path_exemptions.py +164 -0
- package/scripts/tests/test_policy_document.py +104 -0
- package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
- package/scripts/tests/test_policy_lint_selection.py +34 -0
- package/scripts/tests/test_subprocess_window_access.py +118 -0
- package/scripts/tests/test_tracked_secrets.py +79 -0
- package/scripts/validate_instruction_pairs.py +3 -0
- package/scripts/verify_installable_package.py +5 -0
- package/scripts/automatic_advisory/window_flags.py +0 -41
- 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
|
|
60
|
-
'pstack-model-preferences.
|
|
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
|
|
223
|
+
const claudePreferencesPath = join(
|
|
224
224
|
resolution.agentsHome,
|
|
225
225
|
'rules',
|
|
226
226
|
'pstack-model-preferences.claude.json',
|
|
227
227
|
);
|
|
228
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
494
|
+
function seedPstackPreferences(preferenceFileName) {
|
|
495
495
|
const preferenceDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
|
|
496
|
-
const preferencePath = join(
|
|
497
|
-
|
|
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
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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()
|