claude-dev-env 7.0.0 → 7.2.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/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +88 -0
- package/bin/install-session-continuity.test.mjs +55 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import {
|
|
5
5
|
FOLDED_HOOK_RELATIVE_PATHS,
|
|
6
|
+
RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS,
|
|
6
7
|
mergeHooksIntoSettings,
|
|
7
8
|
} from './install.mjs';
|
|
8
9
|
|
|
@@ -15,6 +16,37 @@ const allRetiredWriteHooks = [
|
|
|
15
16
|
'blocking/hook_prose_detector_consistency.py',
|
|
16
17
|
'blocking/workflow_substitution_slot_blocker.py',
|
|
17
18
|
];
|
|
19
|
+
const allRetiredPolicyRegistrations = [
|
|
20
|
+
'blocking/plain_language_blocker.py',
|
|
21
|
+
'lifecycle/config_change_guard.py',
|
|
22
|
+
'blocking/docstring_rule_gate_count_blocker.py',
|
|
23
|
+
'blocking/write_existing_file_blocker.py',
|
|
24
|
+
'blocking/sensitive_file_protector.py',
|
|
25
|
+
'blocking/pii_prevention_blocker.py',
|
|
26
|
+
'blocking/duplicate_rmtree_helper_blocker.py',
|
|
27
|
+
'blocking/claude_md_orphan_file_blocker.py',
|
|
28
|
+
'blocking/package_inventory_stale_blocker.py',
|
|
29
|
+
'blocking/env_var_table_code_drift_blocker.py',
|
|
30
|
+
'blocking/pytest_testpaths_orphan_blocker.py',
|
|
31
|
+
'blocking/destructive_command_blocker.py',
|
|
32
|
+
'blocking/shell_substitution_blocker.py',
|
|
33
|
+
'blocking/piped_pytest_blocker.py',
|
|
34
|
+
'blocking/cursor_cli_python_misfire_blocker.py',
|
|
35
|
+
'blocking/unscoped_search_blocker.py',
|
|
36
|
+
'blocking/nas_ssh_binary_enforcer.py',
|
|
37
|
+
'blocking/block_main_commit.py',
|
|
38
|
+
'blocking/session_edit_stage_gate.py',
|
|
39
|
+
'blocking/bash_pre_tool_use_dispatcher.py',
|
|
40
|
+
'blocking/stop_dispatcher.py',
|
|
41
|
+
'blocking/bot_mention_comment_blocker.py',
|
|
42
|
+
'blocking/fable_spawn_gate.py',
|
|
43
|
+
'blocking/luna_fast_mode_gate.py',
|
|
44
|
+
'blocking/orchestrator_refresh_reschedule_gate.py',
|
|
45
|
+
'blocking/ask_user_question_shape_blocker.py',
|
|
46
|
+
'blocking/send_user_file_open_locally_blocker.py',
|
|
47
|
+
'blocking/question_to_user_enforcer.py',
|
|
48
|
+
'blocking/session_handoff_blocker.py',
|
|
49
|
+
];
|
|
18
50
|
const sourceHooks = JSON.parse(readFileSync(new URL('../hooks/hooks.json', import.meta.url), 'utf8'));
|
|
19
51
|
const installedRoot = '/fixture-home/.claude';
|
|
20
52
|
const foreignCommand = 'python /unmanaged/keep_this_hook.py';
|
|
@@ -22,7 +54,11 @@ const foreignSessionCommand = 'node /unmanaged/session.js';
|
|
|
22
54
|
|
|
23
55
|
function legacySettings() {
|
|
24
56
|
return {
|
|
25
|
-
permissions: {
|
|
57
|
+
permissions: {
|
|
58
|
+
allow: ['Read'],
|
|
59
|
+
deny: ['Bash(git push --force)'],
|
|
60
|
+
ask: ['Bash(git push)'],
|
|
61
|
+
},
|
|
26
62
|
hooks: {
|
|
27
63
|
PreToolUse: [{
|
|
28
64
|
matcher: 'Write|Edit|MultiEdit|apply_patch',
|
|
@@ -33,6 +69,11 @@ function legacySettings() {
|
|
|
33
69
|
command: `python ${installedRoot}/hooks/${relativePath}`,
|
|
34
70
|
timeout: 10,
|
|
35
71
|
})),
|
|
72
|
+
...allRetiredPolicyRegistrations.map(relativePath => ({
|
|
73
|
+
type: 'command',
|
|
74
|
+
command: `python ${installedRoot}/hooks/${relativePath}`,
|
|
75
|
+
timeout: 10,
|
|
76
|
+
})),
|
|
36
77
|
],
|
|
37
78
|
}],
|
|
38
79
|
SessionStart: [{
|
|
@@ -54,18 +95,29 @@ test('every retired write registration already has an installer-owned prune path
|
|
|
54
95
|
}
|
|
55
96
|
});
|
|
56
97
|
|
|
98
|
+
test('every retired policy registration has an installer-owned prune path', () => {
|
|
99
|
+
for (const relativePath of allRetiredPolicyRegistrations) {
|
|
100
|
+
assert.ok(RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS.has(relativePath), relativePath);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
57
104
|
test('upgrade prunes legacy direct file checks while retaining foreign hooks', () => {
|
|
58
105
|
const settings = legacySettings();
|
|
59
106
|
mergeHooksIntoSettings(settings, sourceHooks, installedRoot, 'python3');
|
|
60
107
|
const commands = allCommands(settings);
|
|
61
|
-
for (const relativePath of allRetiredWriteHooks) {
|
|
108
|
+
for (const relativePath of [...allRetiredWriteHooks, ...allRetiredPolicyRegistrations]) {
|
|
62
109
|
assert.ok(!commands.some(command => command.includes(relativePath)), relativePath);
|
|
63
110
|
}
|
|
64
111
|
assert.ok(commands.includes(foreignCommand));
|
|
65
112
|
assert.ok(commands.includes(foreignSessionCommand));
|
|
66
|
-
assert.deepEqual(settings.permissions, {
|
|
113
|
+
assert.deepEqual(settings.permissions, {
|
|
114
|
+
allow: ['Read'],
|
|
115
|
+
deny: ['Bash(git push --force)'],
|
|
116
|
+
ask: ['Bash(git push)'],
|
|
117
|
+
});
|
|
118
|
+
assert.ok(commands.every(command => !command.includes('/blocking/stop_dispatcher.py')));
|
|
67
119
|
assert.ok(commands.some(command => command.includes('/blocking/pre_tool_use_dispatcher.py')));
|
|
68
|
-
assert.ok(commands.
|
|
120
|
+
assert.ok(commands.every(command => !command.includes('/blocking/bash_pre_tool_use_dispatcher.py')));
|
|
69
121
|
});
|
|
70
122
|
|
|
71
123
|
test('reinstall does not restore retired direct checks or duplicate foreign hooks', () => {
|
|
@@ -244,6 +244,16 @@ test('install.mjs --help documents profile target selection', () => {
|
|
|
244
244
|
assert.match(result.stdout, /targetIdentity|target identity|per target/i);
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
+
test('install.mjs --help states that a positional path argument carries no meaning', () => {
|
|
248
|
+
const result = spawnSync(process.execPath, [INSTALL_MODULE_PATH, '--help'], {
|
|
249
|
+
encoding: 'utf8',
|
|
250
|
+
env: process.env,
|
|
251
|
+
});
|
|
252
|
+
assert.equal(result.status, 0, result.stderr);
|
|
253
|
+
assert.match(result.stdout, /reads only flags/i);
|
|
254
|
+
assert.match(result.stdout, /path argument/i);
|
|
255
|
+
});
|
|
256
|
+
|
|
247
257
|
test('install.mjs wires select-install-targets for multi-profile entry', () => {
|
|
248
258
|
const source = readFileSync(INSTALL_MODULE_PATH, 'utf8');
|
|
249
259
|
assert.match(source, /select-install-targets\.mjs/);
|
|
@@ -13,6 +13,7 @@ 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 { detectPython } from './install.mjs';
|
|
16
17
|
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
18
19
|
const INSTALLER_PROCESS_TIMEOUT_MS = 60_000;
|
|
@@ -84,6 +85,8 @@ const STALE_RULE_FILE_NAME = 'retired-rule.md';
|
|
|
84
85
|
const STALE_SHARED_FILE_SEGMENTS = ['pr-loop', 'retired_shared.md'];
|
|
85
86
|
const STALE_SHARED_SKILL_FILE_SEGMENTS = [SHARED_DIRECTORY_NAME, 'retired_skill_shared.md'];
|
|
86
87
|
const RETIRED_HOOK_RELATIVE_SEGMENTS = ['blocking', 'retired_gate.py'];
|
|
88
|
+
const RETIRED_HOOK_SEEDED_CONTENTS = 'a gate an earlier revision shipped\n';
|
|
89
|
+
const FOLDED_RETIRED_HOOK_RELATIVE_SEGMENTS = ['blocking', 'ask_user_question_shape_blocker.py'];
|
|
87
90
|
const USER_HOOK_COMMAND = 'python3 my_own_gate.py --user-authored';
|
|
88
91
|
const RETIRED_HOOK_EVENT_TYPE = 'PreToolUse';
|
|
89
92
|
const DROPPED_HOOK_EVENT_TYPE = 'PreCompact';
|
|
@@ -177,7 +180,7 @@ function createSandbox(homeDirectoryPrefix = 'cdev-prune-home-') {
|
|
|
177
180
|
return { homeDirectory, claudeDirectory, skillsDirectory, manifestPath };
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
test('a core install runs the spaced-path Write dispatcher and
|
|
183
|
+
test('a core install runs the spaced-path Write dispatcher and allows the write', () => {
|
|
181
184
|
const sandbox = createSandbox('cdev prune & (home)-');
|
|
182
185
|
try {
|
|
183
186
|
runInstaller(sandbox.homeDirectory, ['--only', 'core']);
|
|
@@ -204,12 +207,8 @@ test('a core install runs the spaced-path Write dispatcher and captures its red
|
|
|
204
207
|
});
|
|
205
208
|
|
|
206
209
|
assert.equal(dispatcherRun.status, 0, dispatcherRun.stderr);
|
|
207
|
-
|
|
208
|
-
assert.equal(
|
|
209
|
-
assert.match(
|
|
210
|
-
shownRedDecision.hookSpecificOutput.permissionDecisionReason,
|
|
211
|
-
/BLOCKED: Write on existing file/,
|
|
212
|
-
);
|
|
210
|
+
assert.equal(dispatcherRun.stdout.trim(), '', 'no hosted hook denies a Write');
|
|
211
|
+
assert.equal(readFileSync(protectedFilePath, 'utf8'), 'existing content\n');
|
|
213
212
|
} finally {
|
|
214
213
|
rmSync(sandbox.homeDirectory, { recursive: true, force: true });
|
|
215
214
|
}
|
|
@@ -958,7 +957,7 @@ function seedRetiredManagedHook(sandbox) {
|
|
|
958
957
|
const retiredHookPath = join(
|
|
959
958
|
sandbox.claudeDirectory, HOOKS_DIRECTORY_NAME, ...RETIRED_HOOK_RELATIVE_SEGMENTS,
|
|
960
959
|
);
|
|
961
|
-
writeFileWithParents(retiredHookPath,
|
|
960
|
+
writeFileWithParents(retiredHookPath, RETIRED_HOOK_SEEDED_CONTENTS);
|
|
962
961
|
appendManifestFiles(sandbox.manifestPath, [retiredHookPath]);
|
|
963
962
|
const retiredHookCommand = `python3 "${retiredHookPath.replace(/\\/g, '/')}"`;
|
|
964
963
|
const lookalikeHookCommand = `python3 "${retiredHookPath.replace(/\\/g, '/')}.bak"`;
|
|
@@ -1058,7 +1057,7 @@ test('a full reinstall leaves a manifest-recorded path under no managed root in
|
|
|
1058
1057
|
}
|
|
1059
1058
|
});
|
|
1060
1059
|
|
|
1061
|
-
test('a reinstall
|
|
1060
|
+
test('a reinstall regenerates a stale post-commit shim and preserves a foreign git hook', () => {
|
|
1062
1061
|
const sandbox = createSandbox();
|
|
1063
1062
|
try {
|
|
1064
1063
|
runInstaller(sandbox.homeDirectory, []);
|
|
@@ -1068,18 +1067,18 @@ test('a reinstall removes a retired post-commit shim and preserves a foreign git
|
|
|
1068
1067
|
const postCommitShimPath = join(gitHooksDirectory, POST_COMMIT_HOOK_NAME);
|
|
1069
1068
|
const foreignHookPath = join(gitHooksDirectory, FOREIGN_HOOK_NAME);
|
|
1070
1069
|
const foreignHookContentBytes = Buffer.from('foreign hook content\n', 'utf8');
|
|
1071
|
-
|
|
1070
|
+
const staleShimContent = 'generated by an earlier install\n';
|
|
1071
|
+
writeFileWithParents(postCommitShimPath, staleShimContent);
|
|
1072
1072
|
writeFileWithParents(foreignHookPath, foreignHookContentBytes);
|
|
1073
1073
|
appendManifestFiles(sandbox.manifestPath, [postCommitShimPath]);
|
|
1074
1074
|
|
|
1075
1075
|
runInstaller(sandbox.homeDirectory, []);
|
|
1076
1076
|
|
|
1077
1077
|
const freshManifest = readManifest(sandbox.manifestPath);
|
|
1078
|
-
assert.
|
|
1078
|
+
assert.notEqual(readFileSync(postCommitShimPath, 'utf8'), staleShimContent);
|
|
1079
1079
|
assert.deepEqual(readFileSync(foreignHookPath), foreignHookContentBytes);
|
|
1080
|
-
assert.equal(freshManifest.files.includes(postCommitShimPath), false);
|
|
1081
1080
|
assert.equal(freshManifest.files.includes(foreignHookPath), false);
|
|
1082
|
-
for (const gitHookName of [PRE_COMMIT_HOOK_NAME, PRE_PUSH_HOOK_NAME]) {
|
|
1081
|
+
for (const gitHookName of [PRE_COMMIT_HOOK_NAME, PRE_PUSH_HOOK_NAME, POST_COMMIT_HOOK_NAME]) {
|
|
1083
1082
|
const generatedShimPath = join(gitHooksDirectory, gitHookName);
|
|
1084
1083
|
assert.equal(existsSync(generatedShimPath), true);
|
|
1085
1084
|
assert.equal(freshManifest.files.includes(generatedShimPath), true);
|
|
@@ -1089,7 +1088,7 @@ test('a reinstall removes a retired post-commit shim and preserves a foreign git
|
|
|
1089
1088
|
}
|
|
1090
1089
|
});
|
|
1091
1090
|
|
|
1092
|
-
test('a full reinstall
|
|
1091
|
+
test('a full reinstall retires a hook script and leaves an inert stand-in the open session can still run', () => {
|
|
1093
1092
|
const sandbox = createSandbox();
|
|
1094
1093
|
try {
|
|
1095
1094
|
runInstaller(sandbox.homeDirectory, []);
|
|
@@ -1097,9 +1096,26 @@ test('a full reinstall drops a retired hook script and its settings entry in one
|
|
|
1097
1096
|
retiredHookPath, retiredHookCommand, lookalikeHookCommand,
|
|
1098
1097
|
} = seedRetiredManagedHook(sandbox);
|
|
1099
1098
|
|
|
1100
|
-
|
|
1099
|
+
const installOutput = runInstallerReadingBothStreams(sandbox.homeDirectory, []);
|
|
1101
1100
|
|
|
1102
|
-
assert.
|
|
1101
|
+
assert.notEqual(
|
|
1102
|
+
readFileSync(retiredHookPath, 'utf8'),
|
|
1103
|
+
RETIRED_HOOK_SEEDED_CONTENTS,
|
|
1104
|
+
'the retired gate itself no longer runs at that path',
|
|
1105
|
+
);
|
|
1106
|
+
const standInRun = spawnSync(detectPython(), [retiredHookPath], { encoding: 'utf8' });
|
|
1107
|
+
assert.equal(
|
|
1108
|
+
standInRun.status,
|
|
1109
|
+
0,
|
|
1110
|
+
'a session holding the old registration runs the stand-in and gets an allow',
|
|
1111
|
+
);
|
|
1112
|
+
assert.equal(standInRun.stdout, '', 'the stand-in writes nothing to stdout');
|
|
1113
|
+
assert.equal(standInRun.stderr, '', 'the stand-in writes nothing to stderr');
|
|
1114
|
+
assert.match(
|
|
1115
|
+
installOutput,
|
|
1116
|
+
/Restart open sessions: this install retired blocking\/retired_gate\.py/,
|
|
1117
|
+
'the run names the retired registration and asks for a restart',
|
|
1118
|
+
);
|
|
1103
1119
|
assert.equal(
|
|
1104
1120
|
prunedBackupContains(
|
|
1105
1121
|
sandbox.claudeDirectory, join(HOOKS_DIRECTORY_NAME, ...RETIRED_HOOK_RELATIVE_SEGMENTS),
|
|
@@ -1123,6 +1139,59 @@ test('a full reinstall drops a retired hook script and its settings entry in one
|
|
|
1123
1139
|
true,
|
|
1124
1140
|
'a user command whose path is the retired tail plus a suffix names another file and stays',
|
|
1125
1141
|
);
|
|
1142
|
+
|
|
1143
|
+
runInstaller(sandbox.homeDirectory, []);
|
|
1144
|
+
|
|
1145
|
+
assert.equal(
|
|
1146
|
+
existsSync(retiredHookPath),
|
|
1147
|
+
false,
|
|
1148
|
+
'the next run finds no registration naming the stand-in, so the stand-in leaves too',
|
|
1149
|
+
);
|
|
1150
|
+
} finally {
|
|
1151
|
+
rmSync(sandbox.homeDirectory, { recursive: true, force: true });
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
test('a full reinstall leaves a stand-in for a registration the package itself stopped shipping', () => {
|
|
1156
|
+
const sandbox = createSandbox();
|
|
1157
|
+
try {
|
|
1158
|
+
runInstaller(sandbox.homeDirectory, []);
|
|
1159
|
+
const retiredHookPath = join(
|
|
1160
|
+
sandbox.claudeDirectory,
|
|
1161
|
+
HOOKS_DIRECTORY_NAME,
|
|
1162
|
+
...FOLDED_RETIRED_HOOK_RELATIVE_SEGMENTS,
|
|
1163
|
+
);
|
|
1164
|
+
writeFileWithParents(retiredHookPath, RETIRED_HOOK_SEEDED_CONTENTS);
|
|
1165
|
+
appendManifestFiles(sandbox.manifestPath, [retiredHookPath]);
|
|
1166
|
+
const retiredHookCommand = `python3 "${retiredHookPath.replace(/\\/g, '/')}"`;
|
|
1167
|
+
const settings = readSettings(sandbox.claudeDirectory);
|
|
1168
|
+
settings.hooks[RETIRED_HOOK_EVENT_TYPE].push({
|
|
1169
|
+
matcher: 'AskUserQuestion',
|
|
1170
|
+
hooks: [{ type: 'command', command: retiredHookCommand }],
|
|
1171
|
+
});
|
|
1172
|
+
writeFileSync(
|
|
1173
|
+
join(sandbox.claudeDirectory, SETTINGS_FILE_NAME),
|
|
1174
|
+
JSON.stringify(settings, null, 4) + '\n',
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
const installOutput = runInstallerReadingBothStreams(sandbox.homeDirectory, []);
|
|
1178
|
+
|
|
1179
|
+
assert.equal(
|
|
1180
|
+
allHookCommands(readSettings(sandbox.claudeDirectory)).includes(retiredHookCommand),
|
|
1181
|
+
false,
|
|
1182
|
+
'the registration the package stopped shipping leaves settings.json',
|
|
1183
|
+
);
|
|
1184
|
+
const standInRun = spawnSync(detectPython(), [retiredHookPath], { encoding: 'utf8' });
|
|
1185
|
+
assert.equal(
|
|
1186
|
+
standInRun.status,
|
|
1187
|
+
0,
|
|
1188
|
+
'a session still holding that registration runs a stand-in and gets an allow',
|
|
1189
|
+
);
|
|
1190
|
+
assert.match(
|
|
1191
|
+
installOutput,
|
|
1192
|
+
/Restart open sessions: this install retired blocking\/ask_user_question_shape_blocker\.py/,
|
|
1193
|
+
'the run names the registration the merge would otherwise drop in silence',
|
|
1194
|
+
);
|
|
1126
1195
|
} finally {
|
|
1127
1196
|
rmSync(sandbox.homeDirectory, { recursive: true, force: true });
|
|
1128
1197
|
}
|
|
@@ -1228,7 +1297,23 @@ test('a full reinstall removes retired pull request hooks and preserves foreign
|
|
|
1228
1297
|
|
|
1229
1298
|
const freshManifest = readManifest(sandbox.manifestPath);
|
|
1230
1299
|
const allCommands = allHookCommands(readSettings(sandbox.claudeDirectory));
|
|
1231
|
-
|
|
1300
|
+
const registeredRetiredPaths = new Set(RETIRED_PULL_REQUEST_REGISTRATION_PATHS);
|
|
1301
|
+
for (const relativePath of RETIRED_PULL_REQUEST_HOOK_RELATIVE_PATHS) {
|
|
1302
|
+
const retiredPath = join(
|
|
1303
|
+
sandbox.claudeDirectory, HOOKS_DIRECTORY_NAME, ...relativePath.split('/'),
|
|
1304
|
+
);
|
|
1305
|
+
if (registeredRetiredPaths.has(relativePath)) {
|
|
1306
|
+
assert.equal(
|
|
1307
|
+
spawnSync(detectPython(), [retiredPath], { encoding: 'utf8' }).status,
|
|
1308
|
+
0,
|
|
1309
|
+
`${relativePath} keeps an inert stand-in while a session still registers it`,
|
|
1310
|
+
);
|
|
1311
|
+
assert.ok(
|
|
1312
|
+
freshManifest.files.includes(retiredPath),
|
|
1313
|
+
`${relativePath} is recorded, so a later run retires the stand-in`,
|
|
1314
|
+
);
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1232
1317
|
assert.equal(existsSync(retiredPath), false, `${retiredPath} leaves installed hooks`);
|
|
1233
1318
|
assert.equal(freshManifest.files.includes(retiredPath), false);
|
|
1234
1319
|
}
|
|
@@ -1242,6 +1327,131 @@ test('a full reinstall removes retired pull request hooks and preserves foreign
|
|
|
1242
1327
|
}
|
|
1243
1328
|
});
|
|
1244
1329
|
|
|
1330
|
+
|
|
1331
|
+
const CODEX_DIRECTORY_NAME = '.codex';
|
|
1332
|
+
const CODEX_HOOKS_CONFIGURATION_FILE_NAME = 'hooks.json';
|
|
1333
|
+
const FOREIGN_TOOL_DIRECTORY_NAME = 'other-tool';
|
|
1334
|
+
const FOREIGN_CODEX_HOOK_COMMAND = 'python3 my_own_codex_gate.py --user-authored';
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Build the command one hook registration runs for a script under a hooks root.
|
|
1339
|
+
*
|
|
1340
|
+
* @param {string} hooksRoot The hooks directory the registration names.
|
|
1341
|
+
* @param {string[]} relativeSegments The script path segments under that directory.
|
|
1342
|
+
* @returns {string} The registration command.
|
|
1343
|
+
*/
|
|
1344
|
+
function hookCommandUnder(hooksRoot, relativeSegments) {
|
|
1345
|
+
return `python3 "${join(hooksRoot, ...relativeSegments).replace(/\\/g, '/')}"`;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Write a Codex hooks file registering one retired script beside a foreign entry.
|
|
1351
|
+
*
|
|
1352
|
+
* @param {{homeDirectory: string}} sandbox The sandbox paths.
|
|
1353
|
+
* @returns {{codexHooksPath: string, codexRetiredCommand: string}} The file and its retired entry.
|
|
1354
|
+
*/
|
|
1355
|
+
function seedCodexHookRegistrations(sandbox) {
|
|
1356
|
+
const codexDirectory = join(sandbox.homeDirectory, CODEX_DIRECTORY_NAME);
|
|
1357
|
+
const codexHooksPath = join(codexDirectory, CODEX_HOOKS_CONFIGURATION_FILE_NAME);
|
|
1358
|
+
const codexRetiredCommand = hookCommandUnder(
|
|
1359
|
+
join(codexDirectory, HOOKS_DIRECTORY_NAME), RETIRED_HOOK_RELATIVE_SEGMENTS,
|
|
1360
|
+
);
|
|
1361
|
+
writeFileWithParents(codexHooksPath, JSON.stringify({
|
|
1362
|
+
hooks: {
|
|
1363
|
+
[RETIRED_HOOK_EVENT_TYPE]: [{
|
|
1364
|
+
matcher: 'Bash',
|
|
1365
|
+
hooks: [
|
|
1366
|
+
{ type: 'command', command: codexRetiredCommand },
|
|
1367
|
+
{ type: 'command', command: FOREIGN_CODEX_HOOK_COMMAND },
|
|
1368
|
+
],
|
|
1369
|
+
}],
|
|
1370
|
+
},
|
|
1371
|
+
}, null, 4) + '\n');
|
|
1372
|
+
return { codexHooksPath, codexRetiredCommand };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
test('a full reinstall prunes a retired registration the Codex hooks file holds and leaves a stand-in', () => {
|
|
1377
|
+
const sandbox = createSandbox();
|
|
1378
|
+
try {
|
|
1379
|
+
runInstaller(sandbox.homeDirectory, []);
|
|
1380
|
+
const retiredHookPath = join(
|
|
1381
|
+
sandbox.claudeDirectory, HOOKS_DIRECTORY_NAME, ...RETIRED_HOOK_RELATIVE_SEGMENTS,
|
|
1382
|
+
);
|
|
1383
|
+
writeFileWithParents(retiredHookPath, RETIRED_HOOK_SEEDED_CONTENTS);
|
|
1384
|
+
appendManifestFiles(sandbox.manifestPath, [retiredHookPath]);
|
|
1385
|
+
const { codexHooksPath, codexRetiredCommand } = seedCodexHookRegistrations(sandbox);
|
|
1386
|
+
|
|
1387
|
+
const installOutput = runInstallerReadingBothStreams(sandbox.homeDirectory, []);
|
|
1388
|
+
|
|
1389
|
+
const codexCommands = allHookCommands(JSON.parse(readFileSync(codexHooksPath, 'utf8')));
|
|
1390
|
+
assert.equal(
|
|
1391
|
+
codexCommands.includes(codexRetiredCommand),
|
|
1392
|
+
false,
|
|
1393
|
+
'the Codex registration naming the retired script is pruned like a settings.json one',
|
|
1394
|
+
);
|
|
1395
|
+
assert.equal(
|
|
1396
|
+
codexCommands.includes(FOREIGN_CODEX_HOOK_COMMAND),
|
|
1397
|
+
true,
|
|
1398
|
+
'the user-authored Codex entry sharing the matcher group survives',
|
|
1399
|
+
);
|
|
1400
|
+
assert.equal(
|
|
1401
|
+
spawnSync(detectPython(), [retiredHookPath], { encoding: 'utf8' }).status,
|
|
1402
|
+
0,
|
|
1403
|
+
'the Codex registration alone earns the retired path an inert stand-in',
|
|
1404
|
+
);
|
|
1405
|
+
assert.match(
|
|
1406
|
+
installOutput,
|
|
1407
|
+
/Restart open sessions: this install retired blocking\/retired_gate\.py/,
|
|
1408
|
+
'the run names the retired registration and asks for a restart',
|
|
1409
|
+
);
|
|
1410
|
+
} finally {
|
|
1411
|
+
rmSync(sandbox.homeDirectory, { recursive: true, force: true });
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
test('a full reinstall leaves a registration under a hooks root the installer does not own', () => {
|
|
1417
|
+
const sandbox = createSandbox();
|
|
1418
|
+
try {
|
|
1419
|
+
runInstaller(sandbox.homeDirectory, []);
|
|
1420
|
+
seedRetiredManagedHook(sandbox);
|
|
1421
|
+
const foreignHooksRoot = join(
|
|
1422
|
+
sandbox.homeDirectory, FOREIGN_TOOL_DIRECTORY_NAME, '.claude', HOOKS_DIRECTORY_NAME,
|
|
1423
|
+
);
|
|
1424
|
+
const foreignHookPath = join(foreignHooksRoot, ...RETIRED_HOOK_RELATIVE_SEGMENTS);
|
|
1425
|
+
const foreignHookBytes = Buffer.from('another tool owns this gate\n', 'utf8');
|
|
1426
|
+
writeFileWithParents(foreignHookPath, foreignHookBytes);
|
|
1427
|
+
const foreignCommand = hookCommandUnder(foreignHooksRoot, RETIRED_HOOK_RELATIVE_SEGMENTS);
|
|
1428
|
+
const settings = readSettings(sandbox.claudeDirectory);
|
|
1429
|
+
settings.hooks[RETIRED_HOOK_EVENT_TYPE].push({
|
|
1430
|
+
matcher: 'Bash',
|
|
1431
|
+
hooks: [{ type: 'command', command: foreignCommand }],
|
|
1432
|
+
});
|
|
1433
|
+
writeFileSync(
|
|
1434
|
+
join(sandbox.claudeDirectory, SETTINGS_FILE_NAME),
|
|
1435
|
+
JSON.stringify(settings, null, 4) + '\n',
|
|
1436
|
+
);
|
|
1437
|
+
|
|
1438
|
+
runInstaller(sandbox.homeDirectory, []);
|
|
1439
|
+
|
|
1440
|
+
assert.equal(
|
|
1441
|
+
allHookCommands(readSettings(sandbox.claudeDirectory)).includes(foreignCommand),
|
|
1442
|
+
true,
|
|
1443
|
+
'a registration whose hooks root the installer does not own keeps its entry',
|
|
1444
|
+
);
|
|
1445
|
+
assert.deepEqual(
|
|
1446
|
+
readFileSync(foreignHookPath),
|
|
1447
|
+
foreignHookBytes,
|
|
1448
|
+
'the foreign script keeps its own bytes, so no stand-in replaced it',
|
|
1449
|
+
);
|
|
1450
|
+
} finally {
|
|
1451
|
+
rmSync(sandbox.homeDirectory, { recursive: true, force: true });
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1245
1455
|
test('a full reinstall removes a retired hook entry under an event type the current config leaves out', () => {
|
|
1246
1456
|
const sandbox = createSandbox();
|
|
1247
1457
|
try {
|