claude-dev-env 2.18.0 → 2.20.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/AGENTS.md +4 -4
- package/.agents/skills/_shared/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +119 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +5 -0
- package/.agents/skills/e-code-review/SKILL.md +12 -1
- package/.agents/skills/e-code-review/reference/fix.md +5 -1
- package/.agents/skills/e-code-review/reference/loop.md +4 -0
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
- package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
- package/.agents/skills/pr-cleanup/SKILL.md +105 -75
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +15 -0
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +20 -0
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/SKILL.md +18 -12
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/offense-examples.md +1 -1
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/task-seeds.md +3 -2
- package/.agents/skills/pr-refinement/SKILL.md +31 -0
- package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/SKILL.md +24 -7
- package/.agents/skills/{small-cl → pr-small-cl}/SKILL.md +1 -1
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +33 -5
- package/.agents/skills/skill-builder/SKILL.md +1 -0
- package/.agents/skills-archived/bugteam/SKILL.md +1 -1
- package/.agents/skills-archived/bugteam/test_skill_additions.py +1 -1
- package/.agents/skills-archived/comments/SKILL.md +1 -1
- package/.agents/skills-archived/descriptions/SKILL.md +1 -1
- package/.agents/skills-archived/reviews/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/pr-loop/AGENTS.md +2 -1
- package/_shared/pr-loop/preflight-proposal.md +56 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +47 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
- package/bin/ever-shipped-skills.mjs +14 -0
- package/bin/install-constants.mjs +5 -0
- package/bin/install.agents-home.test.mjs +23 -0
- package/bin/install.codex-rules.test.mjs +61 -6
- package/bin/install.mjs +57 -9
- package/bin/install.prune.test.mjs +65 -3
- package/bin/install.test.mjs +202 -2
- package/docs/CODE_RULES.md +6 -0
- package/hooks/advisory/conftest.py +10 -0
- package/hooks/advisory/migration_safety_advisor.py +14 -14
- package/hooks/advisory/refactor_guard.py +250 -144
- package/hooks/advisory/refactor_guard_test_support.py +46 -0
- package/hooks/advisory/test_migration_safety_advisor.py +120 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
- package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
- package/hooks/atomic_file_writer.py +81 -0
- package/hooks/blocking/AGENTS.md +2 -1
- package/hooks/blocking/block_main_commit.py +66 -33
- package/hooks/blocking/code_rules_banned_identifiers.py +55 -4
- package/hooks/blocking/code_rules_blast_radius.py +194 -0
- package/hooks/blocking/code_rules_enforcer.py +12 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +92 -0
- package/hooks/blocking/destructive_command_blocker.py +8 -7
- package/hooks/blocking/pre_tool_use_dispatcher.py +11 -7
- package/hooks/blocking/precommit_code_rules_gate.py +14 -139
- package/hooks/blocking/sensitive_file_protector.py +8 -6
- package/hooks/blocking/session_edit_stage_gate.py +31 -1
- package/hooks/blocking/test_block_main_commit.py +145 -0
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +171 -1
- package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +37 -12
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +54 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +15 -53
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +13 -62
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +309 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +14 -71
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +14 -16
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +31 -46
- package/hooks/blocking/test_code_rules_enforcer_stage.py +192 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +13 -59
- package/hooks/blocking/test_destructive_command_blocker.py +154 -138
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +85 -25
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +58 -38
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +113 -1
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +70 -3
- package/hooks/blocking/test_hook_subprocess_support.py +296 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +1 -1
- package/hooks/blocking/test_pii_scanner.py +81 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +32 -3
- package/hooks/blocking/test_precommit_code_rules_gate.py +11 -16
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +234 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +25 -18
- package/hooks/blocking/test_sensitive_file_protector.py +145 -5
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +64 -0
- package/hooks/blocking/test_session_edit_stage_gate_support.py +204 -0
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +116 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +31 -20
- package/hooks/blocking/test_test_preflight_check.py +80 -0
- package/hooks/git-hooks/AGENTS.md +1 -1
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
- package/hooks/git-hooks/post_commit.py +160 -51
- package/hooks/git-hooks/pre_commit.py +9 -5
- package/hooks/git-hooks/test_post_commit.py +203 -0
- package/hooks/git-hooks/test_pre_commit.py +2 -2
- package/hooks/git-hooks/test_pre_push.py +57 -0
- package/hooks/hooks_constants/AGENTS.md +3 -1
- package/hooks/hooks_constants/atomic_file_writer_constants.py +4 -0
- package/hooks/hooks_constants/banned_identifiers_constants.py +1 -0
- package/hooks/hooks_constants/blast_radius_constants.py +14 -0
- package/hooks/hooks_constants/destructive_command_environment_constants.py +18 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +1 -11
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +9 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +1 -1
- package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +2 -4
- package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
- package/hooks/json_file_reader.py +22 -0
- package/hooks/observability/test_instructions_loaded_logger.py +54 -0
- package/hooks/pending_sidecars.py +33 -0
- package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
- package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
- package/hooks/test_atomic_file_writer.py +73 -0
- package/hooks/test_json_file_reader.py +24 -0
- package/hooks/test_pending_sidecars.py +25 -0
- package/hooks/validation/mypy_validator.py +213 -80
- package/hooks/validation/test_mypy_validator.py +288 -13
- package/hooks/workflow/auto_formatter.py +225 -93
- package/hooks/workflow/investigation_tracker_reset.py +2 -0
- package/hooks/workflow/test_auto_formatter.py +261 -12
- package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
- package/package.json +1 -1
- package/rules/failure-blast-radius.md +126 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/fetch-commands.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/report-template.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/rule-checklist.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/examples.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/offense-taxonomy.md +0 -0
|
@@ -300,6 +300,45 @@ def test_main_staged_mode_blocks_when_staged_lines_introduce_violations(
|
|
|
300
300
|
assert exit_code == 1
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
def test_main_immediate_mode_blocks_staged_rule_violations(
|
|
304
|
+
temporary_git_repository: Path,
|
|
305
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
306
|
+
) -> None:
|
|
307
|
+
write_file(temporary_git_repository / "module.py", "first_count = 1\n")
|
|
308
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
309
|
+
write_file(
|
|
310
|
+
temporary_git_repository / "module.py",
|
|
311
|
+
"first_count = 1\n"
|
|
312
|
+
"def compute_total(operand):\n"
|
|
313
|
+
" result = operand + 1\n"
|
|
314
|
+
" return result\n",
|
|
315
|
+
)
|
|
316
|
+
stage_file(temporary_git_repository, "module.py")
|
|
317
|
+
|
|
318
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
319
|
+
exit_code = gate_module.main(["--immediate"])
|
|
320
|
+
|
|
321
|
+
assert exit_code == 1
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_main_immediate_mode_skips_staged_test_execution(
|
|
325
|
+
temporary_git_repository: Path,
|
|
326
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
327
|
+
) -> None:
|
|
328
|
+
write_file(temporary_git_repository / "module.py", "first_count = 1\n")
|
|
329
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
330
|
+
write_file(
|
|
331
|
+
temporary_git_repository / "test_staged_failure.py",
|
|
332
|
+
"def test_intentionally_fails() -> None:\n assert False\n",
|
|
333
|
+
)
|
|
334
|
+
stage_file(temporary_git_repository, "test_staged_failure.py")
|
|
335
|
+
|
|
336
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
337
|
+
exit_code = gate_module.main(["--immediate"])
|
|
338
|
+
|
|
339
|
+
assert exit_code == 0
|
|
340
|
+
|
|
341
|
+
|
|
303
342
|
def test_main_staged_mode_passes_when_no_staged_violations(
|
|
304
343
|
temporary_git_repository: Path,
|
|
305
344
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -2336,10 +2375,18 @@ def test_parse_arguments_reads_staged_base_and_prefix_flags() -> None:
|
|
|
2336
2375
|
assert parsed_arguments.paths == [Path("explicit_file.py")]
|
|
2337
2376
|
|
|
2338
2377
|
|
|
2378
|
+
def test_parse_arguments_reads_immediate_scope_flag() -> None:
|
|
2379
|
+
parsed_arguments = gate_module.parse_arguments(["--immediate"])
|
|
2380
|
+
|
|
2381
|
+
assert parsed_arguments.immediate is True
|
|
2382
|
+
assert parsed_arguments.staged is False
|
|
2383
|
+
|
|
2384
|
+
|
|
2339
2385
|
def test_parse_arguments_applies_documented_defaults() -> None:
|
|
2340
2386
|
parsed_arguments = gate_module.parse_arguments([])
|
|
2341
2387
|
|
|
2342
2388
|
assert parsed_arguments.staged is False
|
|
2389
|
+
assert parsed_arguments.immediate is False
|
|
2343
2390
|
assert parsed_arguments.base == "origin/main"
|
|
2344
2391
|
assert parsed_arguments.repo_root is None
|
|
2345
2392
|
assert parsed_arguments.only_under == []
|
|
@@ -30,6 +30,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
30
30
|
'build-goal-prompt',
|
|
31
31
|
'bugteam',
|
|
32
32
|
'caveman',
|
|
33
|
+
'cert-classification-rule',
|
|
33
34
|
'closeout',
|
|
34
35
|
'code',
|
|
35
36
|
'comments',
|
|
@@ -40,6 +41,9 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
40
41
|
'copilot-review',
|
|
41
42
|
'deep-research',
|
|
42
43
|
'descriptions',
|
|
44
|
+
'e-code-review',
|
|
45
|
+
'e-simplify',
|
|
46
|
+
'eli5',
|
|
43
47
|
'emergencies',
|
|
44
48
|
'everything-search',
|
|
45
49
|
'findbugs',
|
|
@@ -64,6 +68,8 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
64
68
|
'midjourney-prompt',
|
|
65
69
|
'midjourney-submit',
|
|
66
70
|
'monitor-open-prs',
|
|
71
|
+
'pr-name-by-capability',
|
|
72
|
+
'pr-refinement',
|
|
67
73
|
'orchestrator',
|
|
68
74
|
'orchestrator-refresh',
|
|
69
75
|
'plan-to-pr',
|
|
@@ -71,6 +77,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
71
77
|
'pmid',
|
|
72
78
|
'pmin',
|
|
73
79
|
'post-audit-findings',
|
|
80
|
+
'pr-cleanup',
|
|
74
81
|
'pr-consistency-audit',
|
|
75
82
|
'pr-converge',
|
|
76
83
|
'pr-fix-protocol',
|
|
@@ -80,6 +87,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
80
87
|
'pr-scope-resolve',
|
|
81
88
|
'pre-compact',
|
|
82
89
|
'privacy-hygiene',
|
|
90
|
+
'prototype',
|
|
83
91
|
'prompt-generator',
|
|
84
92
|
'qbug',
|
|
85
93
|
'rebase',
|
|
@@ -90,14 +98,20 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
90
98
|
'release-notes-html',
|
|
91
99
|
'remember',
|
|
92
100
|
'research-mode',
|
|
101
|
+
'review-router',
|
|
102
|
+
'review-tier',
|
|
93
103
|
'reviewer-gates',
|
|
94
104
|
'reviews',
|
|
95
105
|
'run-claude-dev-env',
|
|
96
106
|
'session-log',
|
|
97
107
|
'session-tidy',
|
|
108
|
+
'pr-shared-extraction',
|
|
98
109
|
'show',
|
|
99
110
|
'skill-builder',
|
|
100
111
|
'small-cl',
|
|
112
|
+
'pr-small-cl',
|
|
113
|
+
'source-command-logifix',
|
|
114
|
+
'source-command-sr-loop',
|
|
101
115
|
'split-pr',
|
|
102
116
|
'structure-prompt',
|
|
103
117
|
'task-build',
|
|
@@ -170,3 +170,8 @@ export const CURSOR_SYNC_SCRIPT_FILE_NAME = 'sync_to_cursor.py';
|
|
|
170
170
|
* Windows Python launcher command the installer may bake into hook settings.
|
|
171
171
|
*/
|
|
172
172
|
export const WINDOWS_PYTHON_LAUNCHER_COMMAND = 'py -3';
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Maximum time each Python interpreter probe may run during detection.
|
|
176
|
+
*/
|
|
177
|
+
export const PYTHON_PROBE_TIMEOUT_MILLISECONDS = 10_000;
|
|
@@ -33,6 +33,10 @@ const SHIPPED_SKILL_NAME = 'privacy-hygiene';
|
|
|
33
33
|
const ELI5_SKILL_NAME = 'eli5';
|
|
34
34
|
const SHIPPED_AGENT_FILE_NAME = 'clean-coder.md';
|
|
35
35
|
const PERSONAL_SKILL_NAME = 'my-notes';
|
|
36
|
+
const SHARED_DIRECTORY_NAME = '_shared';
|
|
37
|
+
const PR_LOOP_DIRECTORY_NAME = 'pr-loop';
|
|
38
|
+
const PREFLIGHT_PROPOSAL_FILE_NAME = 'preflight-proposal.md';
|
|
39
|
+
const PREFLIGHT_PROPOSAL_POINTER = '@~/.claude/_shared/pr-loop/preflight-proposal.md';
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* @param {string} homeDirectory
|
|
@@ -53,6 +57,24 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
53
57
|
});
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
/**
|
|
61
|
+
* @param {{ homeDirectory: string, skillsInstallDirectory: string }} installationPaths
|
|
62
|
+
*/
|
|
63
|
+
function assertProposalContractInstallation(installationPaths) {
|
|
64
|
+
const { homeDirectory, skillsInstallDirectory } = installationPaths;
|
|
65
|
+
const allContractPathSegments = [
|
|
66
|
+
SHARED_DIRECTORY_NAME, PR_LOOP_DIRECTORY_NAME, PREFLIGHT_PROPOSAL_FILE_NAME,
|
|
67
|
+
];
|
|
68
|
+
const installedPointerPath = join(skillsInstallDirectory, ...allContractPathSegments);
|
|
69
|
+
const pointerLine = readFileSync(installedPointerPath, 'utf8').trimEnd().split('\n').at(-1);
|
|
70
|
+
const allTargetSegments = pointerLine.slice('@~/'.length).split('/');
|
|
71
|
+
const installedContractPath = join(homeDirectory, ...allTargetSegments);
|
|
72
|
+
const sourceContractPath = join(PACKAGE_DIRECTORY, ...allContractPathSegments);
|
|
73
|
+
|
|
74
|
+
assert.equal(pointerLine, PREFLIGHT_PROPOSAL_POINTER);
|
|
75
|
+
assert.equal(readFileSync(installedContractPath, 'utf8'), readFileSync(sourceContractPath, 'utf8'));
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
test('CONTENT_DIRECTORIES omits agents because that tree installs to the agents home', () => {
|
|
57
79
|
assert.equal(CONTENT_DIRECTORIES.includes(MANAGED_AGENTS_DIRECTORY_NAME), false);
|
|
58
80
|
});
|
|
@@ -106,6 +128,7 @@ test('a full install writes skills and agents under .agents and points .claude a
|
|
|
106
128
|
);
|
|
107
129
|
assert.equal(realpathSync(lookupSkillFile), realpathSync(canonicalSkillFile));
|
|
108
130
|
assert.equal(realpathSync(lookupEli5SkillFile), realpathSync(canonicalEli5SkillFile));
|
|
131
|
+
assertProposalContractInstallation({ homeDirectory, skillsInstallDirectory });
|
|
109
132
|
assert.equal(
|
|
110
133
|
readFileSync(lookupAgentFile, 'utf8'),
|
|
111
134
|
readFileSync(canonicalAgentFile, 'utf8'),
|
|
@@ -23,16 +23,32 @@ import {
|
|
|
23
23
|
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
24
24
|
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
25
25
|
} from './install-constants.mjs';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
CONTENT_DIRECTORIES,
|
|
28
|
+
detectPython,
|
|
29
|
+
INSTALL_GROUPS,
|
|
30
|
+
pythonFileAndPrefixArguments,
|
|
31
|
+
} from './install.mjs';
|
|
27
32
|
|
|
28
33
|
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
29
34
|
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
30
35
|
const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
|
|
36
|
+
const INSTALLER_PROCESS_TIMEOUT_MS = 60_000;
|
|
31
37
|
const SHIPPED_RULES_SOURCE_PATH = join(
|
|
32
38
|
PACKAGE_DIRECTORY,
|
|
33
39
|
CODEX_RULES_PACKAGE_DIRECTORY_NAME,
|
|
34
40
|
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
35
41
|
);
|
|
42
|
+
const BLOCKING_HOOKS_DIRECTORY = join(PACKAGE_DIRECTORY, 'hooks', 'blocking');
|
|
43
|
+
const PYTHON_PII_SCAN_SCRIPT = [
|
|
44
|
+
'import json',
|
|
45
|
+
'import sys',
|
|
46
|
+
'from dataclasses import asdict',
|
|
47
|
+
'from pii_scanner import scan_text_for_pii',
|
|
48
|
+
'all_findings = scan_text_for_pii(sys.stdin.read())',
|
|
49
|
+
"home_path_findings = [asdict(each_finding) for each_finding in all_findings if each_finding.category == 'home-path']",
|
|
50
|
+
'print(json.dumps(home_path_findings))',
|
|
51
|
+
].join('\n');
|
|
36
52
|
|
|
37
53
|
function runInstaller(homeDirectory, extraArguments) {
|
|
38
54
|
return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
|
|
@@ -45,9 +61,44 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
45
61
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
46
62
|
[CODEX_HOME_ENVIRONMENT_VARIABLE]: join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME),
|
|
47
63
|
},
|
|
64
|
+
timeout: INSTALLER_PROCESS_TIMEOUT_MS,
|
|
48
65
|
});
|
|
49
66
|
}
|
|
50
67
|
|
|
68
|
+
function scanTextWithProductionPiiScanner(scannedText) {
|
|
69
|
+
const temporaryDirectory = mkdtempSync(join(tmpdir(), 'cdev-pii-scan-'));
|
|
70
|
+
try {
|
|
71
|
+
const detectedPythonCommand = detectPython();
|
|
72
|
+
if (!detectedPythonCommand) {
|
|
73
|
+
throw new Error('No usable Python interpreter found for the PII scanner test');
|
|
74
|
+
}
|
|
75
|
+
const { file, prefixArguments } = pythonFileAndPrefixArguments(
|
|
76
|
+
detectedPythonCommand,
|
|
77
|
+
);
|
|
78
|
+
const scannerJson = execFileSync(
|
|
79
|
+
file,
|
|
80
|
+
[...prefixArguments, '-c', PYTHON_PII_SCAN_SCRIPT],
|
|
81
|
+
{
|
|
82
|
+
cwd: PACKAGE_DIRECTORY,
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
env: {
|
|
85
|
+
...process.env,
|
|
86
|
+
PYTHONPATH: BLOCKING_HOOKS_DIRECTORY,
|
|
87
|
+
CLAUDE_LOCAL_IDENTITY_PATH: join(
|
|
88
|
+
temporaryDirectory,
|
|
89
|
+
'missing-local-identity.json',
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
input: scannedText,
|
|
93
|
+
timeout: INSTALLER_PROCESS_TIMEOUT_MS,
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
return JSON.parse(scannerJson);
|
|
97
|
+
} finally {
|
|
98
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
51
102
|
test('CONTENT_DIRECTORIES omits codex-rules because that tree installs to the Codex home', () => {
|
|
52
103
|
assert.equal(CONTENT_DIRECTORIES.includes(CODEX_RULES_PACKAGE_DIRECTORY_NAME), false);
|
|
53
104
|
});
|
|
@@ -123,11 +174,15 @@ test('a full install copies shipped Codex rules and leaves a local default.rules
|
|
|
123
174
|
}
|
|
124
175
|
});
|
|
125
176
|
|
|
126
|
-
test('
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
assert.
|
|
177
|
+
test('the shipped Codex rules file has no production-scanner home-path findings', () => {
|
|
178
|
+
const allHomePathFindings = scanTextWithProductionPiiScanner(
|
|
179
|
+
readFileSync(SHIPPED_RULES_SOURCE_PATH, 'utf8'),
|
|
180
|
+
);
|
|
181
|
+
assert.deepEqual(
|
|
182
|
+
allHomePathFindings,
|
|
183
|
+
[],
|
|
184
|
+
`home-path findings: ${JSON.stringify(allHomePathFindings, null, 2)}`,
|
|
185
|
+
);
|
|
131
186
|
});
|
|
132
187
|
|
|
133
188
|
test('--only journal skips Codex rules; --only core copies them', () => {
|
package/bin/install.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
CODEX_RULES_PACKAGE_DIRECTORY_NAME,
|
|
28
28
|
CURSOR_SYNC_SCRIPT_FILE_NAME,
|
|
29
29
|
WINDOWS_PYTHON_LAUNCHER_COMMAND,
|
|
30
|
+
PYTHON_PROBE_TIMEOUT_MILLISECONDS,
|
|
30
31
|
} from './install-constants.mjs';
|
|
31
32
|
import {
|
|
32
33
|
resolveInstallRoot,
|
|
@@ -163,7 +164,7 @@ export const CORE_INCLUDE_DIRECTORIES = [
|
|
|
163
164
|
export const CORE_SKILLS = [
|
|
164
165
|
'orchestrator', 'orchestrator-refresh', 'team-advisor',
|
|
165
166
|
'grok-spawn',
|
|
166
|
-
'small-cl',
|
|
167
|
+
'pr-small-cl',
|
|
167
168
|
'everything-search',
|
|
168
169
|
'privacy-hygiene',
|
|
169
170
|
'issue-tracker',
|
|
@@ -434,6 +435,42 @@ export function interpreterCommandFromPath(executablePath) {
|
|
|
434
435
|
return forwardSlashedPath.includes(' ') ? `"${forwardSlashedPath}"` : forwardSlashedPath;
|
|
435
436
|
}
|
|
436
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Formats a path as one shell argument for the selected platform.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} path Path to format.
|
|
442
|
+
* @param {string} platform Platform whose shell will parse the argument.
|
|
443
|
+
* @returns {string} A quoted shell argument.
|
|
444
|
+
*/
|
|
445
|
+
export function commandArgumentFromPath(path, platform = process.platform) {
|
|
446
|
+
const forwardSlashedPath = path.replace(/\\/g, '/');
|
|
447
|
+
if (platform === 'win32') {
|
|
448
|
+
if (/^[A-Za-z0-9_./:@+=,-]+$/.test(forwardSlashedPath)) return forwardSlashedPath;
|
|
449
|
+
return forwardSlashedPath.split('%').map(segment => `"${segment}"`).join('^%');
|
|
450
|
+
}
|
|
451
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(forwardSlashedPath)) return forwardSlashedPath;
|
|
452
|
+
return `'${forwardSlashedPath.replace(/'/g, "'\\''")}'`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Quotes standalone plugin-root hook paths while preserving inline commands.
|
|
457
|
+
*
|
|
458
|
+
* @param {string} commandString Source hook command.
|
|
459
|
+
* @param {string} pluginRootForward Forward-slash plugin root directory.
|
|
460
|
+
* @returns {string} Rewritten hook command.
|
|
461
|
+
*/
|
|
462
|
+
function quoteStandalonePluginRootHookPath(commandString, pluginRootForward) {
|
|
463
|
+
const standalonePathPattern = /(^|\s)(["']?)\$\{CLAUDE_PLUGIN_ROOT\}(\/hooks\/[^\s"']*?\.py)\2(?=$|[\s;&|])/g;
|
|
464
|
+
return commandString.replace(
|
|
465
|
+
standalonePathPattern,
|
|
466
|
+
(_match, argumentPrefix, _sourceQuote, relativeHookPath) => (
|
|
467
|
+
`${argumentPrefix}${commandArgumentFromPath(
|
|
468
|
+
`${pluginRootForward}${relativeHookPath}`,
|
|
469
|
+
)}`
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
437
474
|
/**
|
|
438
475
|
* Picks the interpreter command baked into every managed hook in settings.json.
|
|
439
476
|
* On win32 the first working candidate is resolved to its absolute
|
|
@@ -444,14 +481,14 @@ export function interpreterCommandFromPath(executablePath) {
|
|
|
444
481
|
*
|
|
445
482
|
* @returns {string|null} The interpreter command, or null when none is usable.
|
|
446
483
|
*/
|
|
447
|
-
function detectPython() {
|
|
484
|
+
export function detectPython() {
|
|
448
485
|
const candidates = pythonCandidatesForPlatform(process.platform);
|
|
449
486
|
for (const { command, versionFlag } of candidates) {
|
|
450
487
|
try {
|
|
451
|
-
const version = execSync(`${command} ${versionFlag}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
488
|
+
const version = execSync(`${command} ${versionFlag}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: PYTHON_PROBE_TIMEOUT_MILLISECONDS }).trim();
|
|
452
489
|
if (!version.includes('Python 3.')) continue;
|
|
453
490
|
if (process.platform !== 'win32') return command;
|
|
454
|
-
const executablePath = execSync(`${command} -c "import sys; print(sys.executable)"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
491
|
+
const executablePath = execSync(`${command} -c "import sys; print(sys.executable)"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: PYTHON_PROBE_TIMEOUT_MILLISECONDS }).trim();
|
|
455
492
|
if (!executablePath || isWindowsStorePythonStub(executablePath)) continue;
|
|
456
493
|
return interpreterCommandFromPath(executablePath);
|
|
457
494
|
} catch { /* try next */ }
|
|
@@ -1266,11 +1303,13 @@ function commandTailEndsAtManagedHook(normalizedCommand, relativePath) {
|
|
|
1266
1303
|
* @returns {boolean} True when the command is the inline validators runner.
|
|
1267
1304
|
*/
|
|
1268
1305
|
export function commandIsInlineManagedValidatorRunner(normalizedCommand) {
|
|
1269
|
-
|
|
1270
|
-
return
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
);
|
|
1306
|
+
if (!normalizedCommand.includes('sys.path.insert(')) return false;
|
|
1307
|
+
if (!normalizedCommand.includes('from validators.run_all_validators import main')) return false;
|
|
1308
|
+
const hasLiteralHooksPath = normalizedCommand.includes('/.claude/hooks');
|
|
1309
|
+
const hasShippedBase64Path = normalizedCommand.includes('__claude_dev_env_managed_hooks__');
|
|
1310
|
+
if (!hasLiteralHooksPath && !hasShippedBase64Path) return false;
|
|
1311
|
+
const inlineValidatorRunnerMarker = /sys\.path\.insert\([\s\S]*from validators\.run_all_validators import main/;
|
|
1312
|
+
return inlineValidatorRunnerMarker.test(normalizedCommand);
|
|
1274
1313
|
}
|
|
1275
1314
|
|
|
1276
1315
|
/**
|
|
@@ -1348,6 +1387,7 @@ function startEventFromHookGroupList(settings, eventType) {
|
|
|
1348
1387
|
export function mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, pythonCommand) {
|
|
1349
1388
|
const managedHookRelativePaths = managedHookScriptRelativePaths(hooksConfig);
|
|
1350
1389
|
const pluginRootForward = pluginRootDir.replace(/\\/g, '/');
|
|
1390
|
+
const encodedPluginHooksPath = Buffer.from(`${pluginRootForward}/hooks`, 'utf8').toString('base64');
|
|
1351
1391
|
if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
|
|
1352
1392
|
let groupCount = 0;
|
|
1353
1393
|
for (const [eventType, matcherGroups] of Object.entries(hooksConfig.hooks)) {
|
|
@@ -1356,6 +1396,14 @@ export function mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, pyt
|
|
|
1356
1396
|
for (const sourceGroup of matcherGroups) {
|
|
1357
1397
|
const rewrittenHooks = sourceGroup.hooks.map(hook => {
|
|
1358
1398
|
let command = hook.command;
|
|
1399
|
+
command = command.replace(
|
|
1400
|
+
/r(['"])\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\1/g,
|
|
1401
|
+
() => (
|
|
1402
|
+
`(__import__('base64').b64decode('${encodedPluginHooksPath}').decode(), `
|
|
1403
|
+
+ "'__claude_dev_env_managed_hooks__')[0]"
|
|
1404
|
+
),
|
|
1405
|
+
);
|
|
1406
|
+
command = quoteStandalonePluginRootHookPath(command, pluginRootForward);
|
|
1359
1407
|
command = command.replace(
|
|
1360
1408
|
/\$\{CLAUDE_PLUGIN_ROOT\}/g,
|
|
1361
1409
|
() => pluginRootForward,
|
|
@@ -13,10 +13,16 @@ import {
|
|
|
13
13
|
import { tmpdir } from 'node:os';
|
|
14
14
|
import { join, dirname, basename } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import {
|
|
17
|
+
MANAGED_SKILLS_DIRECTORY_NAME,
|
|
18
|
+
PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
|
|
19
|
+
} from './install-constants.mjs';
|
|
20
|
+
import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
|
|
16
21
|
|
|
17
22
|
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
18
23
|
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
19
24
|
const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
|
|
25
|
+
const INSTALLER_PROCESS_TIMEOUT_MS = 60_000;
|
|
20
26
|
const EXCLUDED_PACKAGE_COPY_DIRECTORY = 'node_modules';
|
|
21
27
|
|
|
22
28
|
const RETIRED_SKILL_DIRECTORIES = [
|
|
@@ -35,7 +41,7 @@ const STALE_SKILL_FILE_RELATIVE_SEGMENTS = ['scripts', 'retired_module.py'];
|
|
|
35
41
|
const RUNTIME_ARTIFACT_RELATIVE_SEGMENTS = ['scripts', '__pycache__', 'helper.cpython-312.pyc'];
|
|
36
42
|
const SCOPED_GROUP_SKILL_DIRECTORY = 'orchestrator';
|
|
37
43
|
const CORE_REVIEW_GUIDE_SKILL_DIRECTORIES = [
|
|
38
|
-
'small-cl',
|
|
44
|
+
'pr-small-cl',
|
|
39
45
|
];
|
|
40
46
|
const PRIOR_RUN_BACKUP_DIRECTORY_NAMES = [
|
|
41
47
|
'2020-01-01T00-00-00-000Z',
|
|
@@ -102,8 +108,8 @@ function prunedSkillBackupContains(claudeDirectory, skillsRelativePath) {
|
|
|
102
108
|
*
|
|
103
109
|
* @returns {{homeDirectory: string, claudeDirectory: string, skillsDirectory: string, manifestPath: string}}
|
|
104
110
|
*/
|
|
105
|
-
function createSandbox() {
|
|
106
|
-
const homeDirectory = mkdtempSync(join(tmpdir(),
|
|
111
|
+
function createSandbox(homeDirectoryPrefix = 'cdev-prune-home-') {
|
|
112
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), homeDirectoryPrefix));
|
|
107
113
|
const claudeDirectory = join(homeDirectory, '.claude');
|
|
108
114
|
const skillsDirectory = join(claudeDirectory, 'skills');
|
|
109
115
|
mkdirSync(skillsDirectory, { recursive: true });
|
|
@@ -111,6 +117,61 @@ function createSandbox() {
|
|
|
111
117
|
return { homeDirectory, claudeDirectory, skillsDirectory, manifestPath };
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
test('the retired-skill fallback covers every current shipped skill', () => {
|
|
121
|
+
const sourceSkillsDirectory = join(
|
|
122
|
+
PACKAGE_DIRECTORY,
|
|
123
|
+
PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
|
|
124
|
+
MANAGED_SKILLS_DIRECTORY_NAME,
|
|
125
|
+
);
|
|
126
|
+
const allCurrentSkillNames = readdirSync(sourceSkillsDirectory, { withFileTypes: true })
|
|
127
|
+
.filter(eachEntry => eachEntry.isDirectory())
|
|
128
|
+
.filter(eachEntry => existsSync(join(sourceSkillsDirectory, eachEntry.name, 'SKILL.md')))
|
|
129
|
+
.map(eachEntry => eachEntry.name);
|
|
130
|
+
const allUncoveredSkillNames = allCurrentSkillNames.filter(
|
|
131
|
+
eachSkillName => !EVER_SHIPPED_SKILL_NAMES.has(eachSkillName),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
assert.deepEqual(allUncoveredSkillNames, [], 'every current skill must enter the fallback set');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a core install runs the spaced-path Write dispatcher and captures its red denial', () => {
|
|
138
|
+
const sandbox = createSandbox('cdev prune & (home)-');
|
|
139
|
+
try {
|
|
140
|
+
runInstaller(sandbox.homeDirectory, ['--only', 'core']);
|
|
141
|
+
const settings = readSettings(sandbox.claudeDirectory);
|
|
142
|
+
const dispatcherHooks = settings.hooks.PreToolUse
|
|
143
|
+
.flatMap(eachGroup => eachGroup.hooks)
|
|
144
|
+
.filter(eachHook => /(?:^|[\\/])pre_tool_use_dispatcher\.py["']?(?:\s|$)/.test(eachHook.command));
|
|
145
|
+
assert.equal(dispatcherHooks.length, 1, 'the core install writes one PreToolUse dispatcher');
|
|
146
|
+
|
|
147
|
+
const protectedFilePath = join(sandbox.homeDirectory, 'protected file.txt');
|
|
148
|
+
writeFileSync(protectedFilePath, 'existing content\n');
|
|
149
|
+
const writePayload = JSON.stringify({
|
|
150
|
+
tool_name: 'Write',
|
|
151
|
+
tool_input: { file_path: protectedFilePath, content: 'replacement content\n' },
|
|
152
|
+
});
|
|
153
|
+
const { childEnvironment } = resolveInstallerInvocation(sandbox.homeDirectory);
|
|
154
|
+
const dispatcherRun = spawnSync(dispatcherHooks[0].command, {
|
|
155
|
+
cwd: dirname(INSTALLER_PATH),
|
|
156
|
+
encoding: 'utf8',
|
|
157
|
+
env: childEnvironment,
|
|
158
|
+
input: `${writePayload}\n`,
|
|
159
|
+
shell: true,
|
|
160
|
+
timeout: 30000,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.equal(dispatcherRun.status, 0, dispatcherRun.stderr);
|
|
164
|
+
const shownRedDecision = JSON.parse(dispatcherRun.stdout.trim());
|
|
165
|
+
assert.equal(shownRedDecision.hookSpecificOutput.permissionDecision, 'deny');
|
|
166
|
+
assert.match(
|
|
167
|
+
shownRedDecision.hookSpecificOutput.permissionDecisionReason,
|
|
168
|
+
/BLOCKED: Write on existing file/,
|
|
169
|
+
);
|
|
170
|
+
} finally {
|
|
171
|
+
rmSync(sandbox.homeDirectory, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
114
175
|
/**
|
|
115
176
|
* Plant a skill directory under the sandbox with a single marker file.
|
|
116
177
|
*
|
|
@@ -142,6 +203,7 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
142
203
|
cwd: dirname(installerPath),
|
|
143
204
|
encoding: 'utf8',
|
|
144
205
|
env: childEnvironment,
|
|
206
|
+
timeout: INSTALLER_PROCESS_TIMEOUT_MS,
|
|
145
207
|
});
|
|
146
208
|
}
|
|
147
209
|
|