claude-dev-env 7.0.0 → 7.1.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 +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -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 +78 -0
- package/bin/install-session-continuity.test.mjs +42 -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
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The top-level skill directory names this package has ever shipped.
|
|
3
|
-
*
|
|
4
|
-
* The installer subtracts the skills the current package installs from this set
|
|
5
|
-
* to learn which shipped skills have retired, so a full reinstall can remove a
|
|
6
|
-
* retired skill left behind under ~/.agents/skills. Because the current set is
|
|
7
|
-
* subtracted at runtime, restoring a retired skill to the package protects it
|
|
8
|
-
* automatically — it re-enters the installed set and drops out of the retired
|
|
9
|
-
* set on the next install.
|
|
10
|
-
*
|
|
11
|
-
* The list is a committed literal rather than a git query, because a user
|
|
12
|
-
* installs from the npm tarball, which carries no source-repo history. Refresh
|
|
13
|
-
* it by running, from the repository root:
|
|
14
|
-
*
|
|
15
|
-
* git log --all --pretty=format: --name-only -- \
|
|
16
|
-
* 'packages/claude-dev-env/.agents/skills/*\/SKILL.md' \
|
|
17
|
-
* 'packages/claude-dev-env/skills/*\/SKILL.md' | sort -u
|
|
18
|
-
*
|
|
19
|
-
* and pasting each distinct top-level skill directory name below.
|
|
20
|
-
*/
|
|
21
1
|
export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
22
2
|
'agent-prompt',
|
|
23
3
|
'anthropic-plan',
|
|
@@ -109,6 +89,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
109
89
|
'reviewer-gates',
|
|
110
90
|
'reviews',
|
|
111
91
|
'run-claude-dev-env',
|
|
92
|
+
'session-continuity',
|
|
112
93
|
'session-log',
|
|
113
94
|
'session-tidy',
|
|
114
95
|
'pr-shared-extraction',
|
|
@@ -127,4 +108,5 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
127
108
|
'update',
|
|
128
109
|
'usage-pause',
|
|
129
110
|
'verified-build',
|
|
111
|
+
'windows-scheduled-task',
|
|
130
112
|
]);
|
|
@@ -36,3 +36,11 @@ test('EVER_SHIPPED_SKILL_NAMES retains the explicit test runner', () => {
|
|
|
36
36
|
test('EVER_SHIPPED_SKILL_NAMES includes the pull request skill', () => {
|
|
37
37
|
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('pull-request'), true);
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
test('EVER_SHIPPED_SKILL_NAMES includes the windows scheduled task skill', () => {
|
|
41
|
+
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('windows-scheduled-task'), true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('EVER_SHIPPED_SKILL_NAMES includes the session continuity companion', () => {
|
|
45
|
+
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('session-continuity'), true);
|
|
46
|
+
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Expand $HOME, ${HOME}, and ~/ in settings.json commands at install time.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* rewrites residual tokens to absolute home paths.
|
|
4
|
+
* Some hosts skip a hook when HOME is unset, which is common on Windows.
|
|
5
|
+
* The installer rewrites leftover tokens to absolute home paths.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -34,7 +33,7 @@ export function expandHomeDirectoryTokens(commandString, homeDirectory) {
|
|
|
34
33
|
() => normalizedHome,
|
|
35
34
|
);
|
|
36
35
|
expandedCommand = expandedCommand.replace(
|
|
37
|
-
/(^|[\s"'
|
|
36
|
+
/(^|[\s"'=;(])~\//g,
|
|
38
37
|
(_fullMatch, boundary) => `${boundary}${normalizedHome}/`,
|
|
39
38
|
);
|
|
40
39
|
return expandedCommand;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
expandHomeDirectoryTokens,
|
|
6
|
+
expandHomeDirectoryTokensInSettings,
|
|
7
|
+
} from './expand_home_directory_tokens.mjs';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
test('expandHomeDirectoryTokens expands $HOME, ${HOME}, and ~/', () => {
|
|
11
|
+
assert.equal(
|
|
12
|
+
expandHomeDirectoryTokens(
|
|
13
|
+
'python $HOME/.claude/hooks/session/fix_worktree_hookspath.py',
|
|
14
|
+
'C:\\Users\\x',
|
|
15
|
+
),
|
|
16
|
+
'python C:/Users/x/.claude/hooks/session/fix_worktree_hookspath.py',
|
|
17
|
+
);
|
|
18
|
+
assert.equal(
|
|
19
|
+
expandHomeDirectoryTokens('python ${HOME}/.claude/hooks/a.py', '/home/x'),
|
|
20
|
+
'python /home/x/.claude/hooks/a.py',
|
|
21
|
+
);
|
|
22
|
+
assert.equal(
|
|
23
|
+
expandHomeDirectoryTokens('python ~/.claude/hooks/a.py', '/home/x'),
|
|
24
|
+
'python /home/x/.claude/hooks/a.py',
|
|
25
|
+
);
|
|
26
|
+
assert.equal(
|
|
27
|
+
expandHomeDirectoryTokens('echo $HOMEPATH', 'C:/Users/x'),
|
|
28
|
+
'echo $HOMEPATH',
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
test('expandHomeDirectoryTokens inserts dollar characters in home paths literally', () => {
|
|
34
|
+
const homeWithReplaceMetacharacters = 'C:/Users/$&evil$1';
|
|
35
|
+
assert.equal(
|
|
36
|
+
expandHomeDirectoryTokens('python $HOME/.claude/a.py', homeWithReplaceMetacharacters),
|
|
37
|
+
'python C:/Users/$&evil$1/.claude/a.py',
|
|
38
|
+
);
|
|
39
|
+
assert.equal(
|
|
40
|
+
expandHomeDirectoryTokens('python ${HOME}/.claude/a.py', homeWithReplaceMetacharacters),
|
|
41
|
+
'python C:/Users/$&evil$1/.claude/a.py',
|
|
42
|
+
);
|
|
43
|
+
assert.equal(
|
|
44
|
+
expandHomeDirectoryTokens('python ~/.claude/a.py', homeWithReplaceMetacharacters),
|
|
45
|
+
'python C:/Users/$&evil$1/.claude/a.py',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
test('expandHomeDirectoryTokens expands ${HOME} before $HOME so braces stay intact', () => {
|
|
51
|
+
assert.equal(
|
|
52
|
+
expandHomeDirectoryTokens('python ${HOME}/.claude/a.py', '/home/x'),
|
|
53
|
+
'python /home/x/.claude/a.py',
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
test('expandHomeDirectoryTokens strips trailing slashes from the home directory', () => {
|
|
59
|
+
assert.equal(
|
|
60
|
+
expandHomeDirectoryTokens('python $HOME/.claude/a.py', 'C:/Users/x/'),
|
|
61
|
+
'python C:/Users/x/.claude/a.py',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
test('expandHomeDirectoryTokensInSettings skips non-array hook event values', () => {
|
|
67
|
+
const settings = {
|
|
68
|
+
hooks: {
|
|
69
|
+
SessionStart: 'not-an-array',
|
|
70
|
+
PreToolUse: [
|
|
71
|
+
{
|
|
72
|
+
matcher: 'Write',
|
|
73
|
+
hooks: [{ type: 'command', command: 'python $HOME/.claude/hooks/a.py' }],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
expandHomeDirectoryTokensInSettings(settings, 'C:/Users/x');
|
|
79
|
+
assert.equal(settings.hooks.SessionStart, 'not-an-array');
|
|
80
|
+
assert.equal(
|
|
81
|
+
settings.hooks.PreToolUse[0].hooks[0].command,
|
|
82
|
+
'python C:/Users/x/.claude/hooks/a.py',
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
test('expandHomeDirectoryTokensInSettings rewrites hooks and statusLine', () => {
|
|
88
|
+
const settings = {
|
|
89
|
+
hooks: {
|
|
90
|
+
SessionStart: [
|
|
91
|
+
{
|
|
92
|
+
matcher: '',
|
|
93
|
+
hooks: [
|
|
94
|
+
{
|
|
95
|
+
type: 'command',
|
|
96
|
+
command: 'python $HOME/.claude/hooks/session/fix_worktree_hookspath.py',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
statusLine: {
|
|
103
|
+
type: 'command',
|
|
104
|
+
command: 'python "$HOME/.claude/statusline-command.py"',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
expandHomeDirectoryTokensInSettings(settings, 'C:/Users/x');
|
|
108
|
+
assert.equal(
|
|
109
|
+
settings.hooks.SessionStart[0].hooks[0].command,
|
|
110
|
+
'python C:/Users/x/.claude/hooks/session/fix_worktree_hookspath.py',
|
|
111
|
+
);
|
|
112
|
+
assert.equal(
|
|
113
|
+
settings.statusLine.command,
|
|
114
|
+
'python "C:/Users/x/.claude/statusline-command.py"',
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
test('expandHomeDirectoryTokens expands a tilde path that follows a semicolon or an opening parenthesis', () => {
|
|
120
|
+
assert.equal(
|
|
121
|
+
expandHomeDirectoryTokens('cd /repo;~/.claude/hooks/x.py', '/home/x'),
|
|
122
|
+
'cd /repo;/home/x/.claude/hooks/x.py',
|
|
123
|
+
);
|
|
124
|
+
assert.equal(
|
|
125
|
+
expandHomeDirectoryTokens('(~/.claude/hooks/x.py)', '/home/x'),
|
|
126
|
+
'(/home/x/.claude/hooks/x.py)',
|
|
127
|
+
);
|
|
128
|
+
});
|
|
@@ -6,6 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
export const KNOWN_GIT_HOOK_NAMES = Object.freeze([
|
|
7
7
|
'pre-commit',
|
|
8
8
|
'pre-push',
|
|
9
|
+
'post-commit',
|
|
9
10
|
]);
|
|
10
11
|
|
|
11
12
|
export const SHIM_DOCSTRING = (
|
|
@@ -72,6 +73,12 @@ function deriveModuleNameFromGitNativeHookName(gitNativeHookName) {
|
|
|
72
73
|
|
|
73
74
|
function buildShimContent(pythonModuleName) {
|
|
74
75
|
const shimContentHeader = '#!/usr/bin/env python3\n';
|
|
76
|
+
const failureMessage = JSON.stringify(
|
|
77
|
+
`claude-dev-env: advisory hook ${pythonModuleName} failed: `,
|
|
78
|
+
);
|
|
79
|
+
const nonzeroMessage = JSON.stringify(
|
|
80
|
+
`claude-dev-env: advisory hook ${pythonModuleName} returned non-zero: `,
|
|
81
|
+
);
|
|
75
82
|
return (
|
|
76
83
|
shimContentHeader
|
|
77
84
|
+ `"""${SHIM_DOCSTRING}"""\n`
|
|
@@ -82,9 +89,21 @@ function buildShimContent(pythonModuleName) {
|
|
|
82
89
|
+ 'if str(shim_directory) not in sys.path:\n'
|
|
83
90
|
+ ' sys.path.insert(0, str(shim_directory))\n'
|
|
84
91
|
+ '\n'
|
|
85
|
-
+
|
|
86
|
-
+
|
|
87
|
-
+ `
|
|
92
|
+
+ 'try:\n'
|
|
93
|
+
+ ` import ${pythonModuleName}\n`
|
|
94
|
+
+ ` advisory_exit_code = ${pythonModuleName}.main()\n`
|
|
95
|
+
+ 'except SystemExit as advisory_system_exit:\n'
|
|
96
|
+
+ ' if advisory_system_exit.code == 130:\n'
|
|
97
|
+
+ ' raise\n'
|
|
98
|
+
+ ' if advisory_system_exit.code not in (None, 0):\n'
|
|
99
|
+
+ ` print(${failureMessage} + str(advisory_system_exit), file=sys.stderr)\n`
|
|
100
|
+
+ ' sys.exit(0)\n'
|
|
101
|
+
+ 'except Exception as advisory_error:\n'
|
|
102
|
+
+ ` print(${failureMessage} + str(advisory_error), file=sys.stderr)\n`
|
|
103
|
+
+ ' sys.exit(0)\n'
|
|
104
|
+
+ 'if advisory_exit_code:\n'
|
|
105
|
+
+ ` print(${nonzeroMessage} + str(advisory_exit_code), file=sys.stderr)\n`
|
|
106
|
+
+ 'sys.exit(0)\n'
|
|
88
107
|
);
|
|
89
108
|
}
|
|
90
109
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync, mkdirSync, statSync, symlinkSync } from 'node:fs';
|
|
3
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, mkdirSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
5
|
import { tmpdir } from 'node:os';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
|
|
@@ -58,16 +59,16 @@ test('writeAllGitHookShims creates one shim per known hook name', () => {
|
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
|
|
61
|
-
test('known git hooks
|
|
62
|
-
assert.
|
|
62
|
+
test('known git hooks include post-commit for current-head notices', () => {
|
|
63
|
+
assert.deepEqual(KNOWN_GIT_HOOK_NAMES, ['pre-commit', 'pre-push', 'post-commit']);
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
test('writeAllGitHookShims
|
|
67
|
+
test('writeAllGitHookShims creates the post-commit shim', () => {
|
|
67
68
|
const { temporaryRoot, gitHooksDirectory } = makeTemporaryGitHooksDirectory();
|
|
68
69
|
try {
|
|
69
70
|
writeAllGitHookShims({ gitHooksDirectory });
|
|
70
|
-
assert.equal(existsSync(join(gitHooksDirectory, 'post-commit')),
|
|
71
|
+
assert.equal(existsSync(join(gitHooksDirectory, 'post-commit')), true);
|
|
71
72
|
} finally {
|
|
72
73
|
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
73
74
|
}
|
|
@@ -221,3 +222,156 @@ test('writeGitHookShim rejects hooks directory that is a symlink (loopP5c-5)', (
|
|
|
221
222
|
}
|
|
222
223
|
});
|
|
223
224
|
|
|
225
|
+
|
|
226
|
+
test('generated advisory shims keep temporary Git commit and push available after module failure', () => {
|
|
227
|
+
const { temporaryRoot, gitHooksDirectory } = makeTemporaryGitHooksDirectory();
|
|
228
|
+
const repositoryRoot = join(temporaryRoot, 'repository');
|
|
229
|
+
const bareRepositoryRoot = join(temporaryRoot, 'remote.git');
|
|
230
|
+
const targetRemote = 'https://github.com/JonEcho/python-automation.git';
|
|
231
|
+
try {
|
|
232
|
+
writeFileSync(
|
|
233
|
+
join(gitHooksDirectory, 'broken_advisory.py'),
|
|
234
|
+
'def main():\n raise RuntimeError("advisory failure")\n',
|
|
235
|
+
);
|
|
236
|
+
writeGitHookShim({
|
|
237
|
+
gitHooksDirectory,
|
|
238
|
+
gitNativeHookName: 'pre-commit',
|
|
239
|
+
pythonModuleName: 'broken_advisory',
|
|
240
|
+
});
|
|
241
|
+
writeGitHookShim({
|
|
242
|
+
gitHooksDirectory,
|
|
243
|
+
gitNativeHookName: 'pre-push',
|
|
244
|
+
pythonModuleName: 'broken_advisory',
|
|
245
|
+
});
|
|
246
|
+
mkdirSync(repositoryRoot);
|
|
247
|
+
execFileSync('git', ['init', '--initial-branch=main', '--quiet'], { cwd: repositoryRoot });
|
|
248
|
+
execFileSync('git', ['config', 'user.email', 'test@example.invalid'], { cwd: repositoryRoot });
|
|
249
|
+
execFileSync('git', ['config', 'user.name', 'Verification Test'], { cwd: repositoryRoot });
|
|
250
|
+
execFileSync('git', ['config', 'core.hooksPath', gitHooksDirectory], { cwd: repositoryRoot });
|
|
251
|
+
writeFileSync(join(repositoryRoot, 'README.md'), 'check\n');
|
|
252
|
+
execFileSync('git', ['add', 'README.md'], { cwd: repositoryRoot });
|
|
253
|
+
|
|
254
|
+
const commitResult = spawnSync('git', ['commit', '-m', 'initial'], {
|
|
255
|
+
cwd: repositoryRoot,
|
|
256
|
+
encoding: 'utf8',
|
|
257
|
+
});
|
|
258
|
+
assert.equal(commitResult.status, 0);
|
|
259
|
+
assert.match(commitResult.stderr, /advisory hook broken_advisory failed: advisory failure/);
|
|
260
|
+
|
|
261
|
+
execFileSync('git', ['init', '--bare', '--quiet', bareRepositoryRoot], { cwd: temporaryRoot });
|
|
262
|
+
execFileSync('git', ['remote', 'add', 'origin', targetRemote], { cwd: repositoryRoot });
|
|
263
|
+
execFileSync('git', ['config', `url.${bareRepositoryRoot}.insteadOf`, targetRemote], {
|
|
264
|
+
cwd: repositoryRoot,
|
|
265
|
+
});
|
|
266
|
+
const pushResult = spawnSync('git', ['push', 'origin', 'main'], {
|
|
267
|
+
cwd: repositoryRoot,
|
|
268
|
+
encoding: 'utf8',
|
|
269
|
+
});
|
|
270
|
+
assert.equal(pushResult.status, 0);
|
|
271
|
+
assert.match(pushResult.stderr, /advisory hook broken_advisory failed: advisory failure/);
|
|
272
|
+
} finally {
|
|
273
|
+
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
test('generated advisory shim turns a nonzero module return into a success with a warning', () => {
|
|
279
|
+
const { temporaryRoot, gitHooksDirectory } = makeTemporaryGitHooksDirectory();
|
|
280
|
+
try {
|
|
281
|
+
writeFileSync(
|
|
282
|
+
join(gitHooksDirectory, 'nonzero_advisory.py'),
|
|
283
|
+
'def main():\n return 1\n',
|
|
284
|
+
);
|
|
285
|
+
const shimPath = writeGitHookShim({
|
|
286
|
+
gitHooksDirectory,
|
|
287
|
+
gitNativeHookName: 'pre-commit',
|
|
288
|
+
pythonModuleName: 'nonzero_advisory',
|
|
289
|
+
});
|
|
290
|
+
const shimResult = spawnSync('python', [shimPath], {
|
|
291
|
+
encoding: 'utf8',
|
|
292
|
+
});
|
|
293
|
+
assert.equal(shimResult.status, 0);
|
|
294
|
+
assert.match(shimResult.stderr, /advisory hook nonzero_advisory returned non-zero: 1/);
|
|
295
|
+
} finally {
|
|
296
|
+
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
function runGeneratedAdvisoryModule(pythonModuleName, moduleSource) {
|
|
302
|
+
const { temporaryRoot, gitHooksDirectory } = makeTemporaryGitHooksDirectory();
|
|
303
|
+
try {
|
|
304
|
+
if (moduleSource !== null) {
|
|
305
|
+
writeFileSync(join(gitHooksDirectory, pythonModuleName + '.py'), moduleSource);
|
|
306
|
+
}
|
|
307
|
+
const shimPath = writeGitHookShim({
|
|
308
|
+
gitHooksDirectory,
|
|
309
|
+
gitNativeHookName: 'pre-commit',
|
|
310
|
+
pythonModuleName,
|
|
311
|
+
});
|
|
312
|
+
return spawnSync('python', [shimPath], {
|
|
313
|
+
encoding: 'utf8',
|
|
314
|
+
});
|
|
315
|
+
} finally {
|
|
316
|
+
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
test('generated advisory shim reports a missing module and succeeds', () => {
|
|
322
|
+
const shimResult = runGeneratedAdvisoryModule('missing_advisory', null);
|
|
323
|
+
assert.equal(shimResult.status, 0);
|
|
324
|
+
assert.match(shimResult.stderr, /advisory hook missing_advisory failed: No module named/);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
test('generated advisory shim reports a syntax error and succeeds', () => {
|
|
329
|
+
const shimResult = runGeneratedAdvisoryModule(
|
|
330
|
+
'broken_syntax',
|
|
331
|
+
'def main(:\n return 1\n',
|
|
332
|
+
);
|
|
333
|
+
assert.equal(shimResult.status, 0);
|
|
334
|
+
assert.match(shimResult.stderr, /advisory hook broken_syntax failed:/);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
test('generated advisory shim reports ordinary SystemExit and succeeds', () => {
|
|
339
|
+
const shimResult = runGeneratedAdvisoryModule(
|
|
340
|
+
'ordinary_exit',
|
|
341
|
+
'def main():\n raise SystemExit(1)\n',
|
|
342
|
+
);
|
|
343
|
+
assert.equal(shimResult.status, 0);
|
|
344
|
+
assert.match(shimResult.stderr, /advisory hook ordinary_exit failed: 1/);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
test('generated advisory shim treats successful SystemExit as silent success', () => {
|
|
349
|
+
for (const [pythonModuleName, moduleSource] of [
|
|
350
|
+
['zero_exit', 'def main():\n raise SystemExit(0)\n'],
|
|
351
|
+
['empty_exit', 'def main():\n raise SystemExit()\n'],
|
|
352
|
+
]) {
|
|
353
|
+
const shimResult = runGeneratedAdvisoryModule(pythonModuleName, moduleSource);
|
|
354
|
+
assert.equal(shimResult.status, 0);
|
|
355
|
+
assert.equal(shimResult.stderr, '');
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
test('generated advisory shim preserves cancellation SystemExit 130', () => {
|
|
361
|
+
const shimResult = runGeneratedAdvisoryModule(
|
|
362
|
+
'cancelled_exit',
|
|
363
|
+
'def main():\n raise SystemExit(130)\n',
|
|
364
|
+
);
|
|
365
|
+
assert.equal(shimResult.status, 130);
|
|
366
|
+
assert.doesNotMatch(shimResult.stderr, /advisory hook cancelled_exit/);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
test('generated advisory shim preserves KeyboardInterrupt cancellation', () => {
|
|
371
|
+
const shimResult = runGeneratedAdvisoryModule(
|
|
372
|
+
'keyboard_cancel',
|
|
373
|
+
'def main():\n raise KeyboardInterrupt()\n',
|
|
374
|
+
);
|
|
375
|
+
assert.notEqual(shimResult.status, 0);
|
|
376
|
+
assert.match(shimResult.stderr, /KeyboardInterrupt/);
|
|
377
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export function continuityHookConfiguration(host, script) {
|
|
6
|
+
if (host === 'cursor') {
|
|
7
|
+
throw new Error('Cursor automatic activation is unsupported: beforeSubmitPrompt has no agent-context output, sessionStart is fire-and-forget, and preCompact is observational. No Cursor settings changed.');
|
|
8
|
+
}
|
|
9
|
+
if (!['claude', 'codex'].includes(host)) throw new Error(`Unsupported host ${host}`);
|
|
10
|
+
if (/[\r\n"`$%]/.test(script)) throw new Error('Hook script path contains shell expansion characters');
|
|
11
|
+
const command = `node "${script.replace(/\\/g, '/')}" hook ${host}`;
|
|
12
|
+
const events = host === 'claude' ? ['UserPromptSubmit', 'UserPromptExpansion', 'SessionStart'] : ['UserPromptSubmit', 'SessionStart'];
|
|
13
|
+
return Object.fromEntries(events.map(event => [event, [{
|
|
14
|
+
...(event === 'SessionStart' ? { matcher: host === 'claude' ? 'startup|resume|compact|clear|fork' : 'startup|resume|compact|clear' } : {}),
|
|
15
|
+
...(event === 'UserPromptExpansion' ? { matcher: '^(pstack:)?poteto-mode$|^(claude-dev-env:)?session-continuity$' } : {}),
|
|
16
|
+
hooks: [{ type: 'command', command, timeout: 10 }],
|
|
17
|
+
}]]));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mergeContinuityHooks(existing, host, script) {
|
|
21
|
+
const additions = continuityHookConfiguration(host, script);
|
|
22
|
+
const mergedConfiguration = structuredClone(existing);
|
|
23
|
+
mergedConfiguration.hooks ||= {};
|
|
24
|
+
for (const [event, groups] of Object.entries(additions)) {
|
|
25
|
+
const current = mergedConfiguration.hooks[event] || [];
|
|
26
|
+
for (const group of current) {
|
|
27
|
+
for (const hook of group.hooks || []) {
|
|
28
|
+
if (hook.command?.includes('/session-continuity/continuity.mjs') && hook.command !== groups[0].hooks[0].command) {
|
|
29
|
+
throw new Error('Another continuity installation owns this host config. Select its profile or remove that installation explicitly.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const kept = current.flatMap(group => {
|
|
34
|
+
const hooks = (group.hooks || []).filter(hook => hook.command !== groups[0].hooks[0].command);
|
|
35
|
+
return hooks.length ? [{ ...group, hooks }] : [];
|
|
36
|
+
});
|
|
37
|
+
mergedConfiguration.hooks[event] = kept.concat(groups);
|
|
38
|
+
}
|
|
39
|
+
return mergedConfiguration;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const requested = process.argv.slice(2);
|
|
44
|
+
const selected = requested.length ? requested : ['claude', 'codex'];
|
|
45
|
+
if (selected.some(host => !['claude', 'codex'].includes(host))) continuityHookConfiguration(selected.find(host => !['claude', 'codex'].includes(host)), '');
|
|
46
|
+
const { resolveInstallRoot } = await import('./resolve-install-root.mjs');
|
|
47
|
+
const roots = resolveInstallRoot();
|
|
48
|
+
const script = join(roots.skillsInstallDirectory, 'session-continuity', 'continuity.mjs');
|
|
49
|
+
if (!existsSync(script) || !existsSync(join(dirname(script), 'SKILL.md'))) {
|
|
50
|
+
throw new Error('Run the full claude-dev-env installer from this checkout first so the companion is in the canonical agents home.');
|
|
51
|
+
}
|
|
52
|
+
const paths = { claude: join(roots.managedRoot, 'settings.json'), codex: join(dirname(roots.codexRulesInstallDirectory), 'hooks.json') };
|
|
53
|
+
const plans = selected.map(host => {
|
|
54
|
+
const path = paths[host];
|
|
55
|
+
if (!existsSync(dirname(path))) throw new Error(`Host config directory is absent: ${dirname(path)}`);
|
|
56
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
57
|
+
return { path, existing, content: JSON.stringify(mergeContinuityHooks(existing ? JSON.parse(existing) : {}, host, script), null, 2) + '\n' };
|
|
58
|
+
});
|
|
59
|
+
for (const plan of plans) {
|
|
60
|
+
if (plan.existing === plan.content) { console.log(`Already configured: ${plan.path}`); continue; }
|
|
61
|
+
if (plan.existing !== null) {
|
|
62
|
+
const backup = `${plan.path}.before-session-continuity`;
|
|
63
|
+
if (!existsSync(backup)) writeFileSync(backup, plan.existing, { flag: 'wx', mode: 0o600 });
|
|
64
|
+
}
|
|
65
|
+
const current = existsSync(plan.path) ? readFileSync(plan.path, 'utf8') : null;
|
|
66
|
+
if (current !== plan.existing) throw new Error(`Config changed during setup: ${plan.path}. Rerun setup.`);
|
|
67
|
+
const temporary = `${plan.path}.continuity-${process.pid}.tmp`;
|
|
68
|
+
writeFileSync(temporary, plan.content, { flag: 'wx', mode: 0o600 });
|
|
69
|
+
renameSync(temporary, plan.path);
|
|
70
|
+
if (readFileSync(plan.path, 'utf8') !== plan.content) throw new Error(`Config read-back mismatch: ${plan.path}`);
|
|
71
|
+
console.log(`Configured and read back: ${plan.path}`);
|
|
72
|
+
}
|
|
73
|
+
console.log('Review and trust the new hooks in each host. Cursor remains unsupported; no Cursor settings changed.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (process.argv[1] && resolve(realpathSync(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
77
|
+
main().catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
78
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { continuityHookConfiguration, mergeContinuityHooks } from './install-session-continuity.mjs';
|
|
4
|
+
|
|
5
|
+
test('setup preserves other hooks and settings, is idempotent, and rejects competing profiles', () => {
|
|
6
|
+
const existing = { permissions: { deny: ['secret'] }, hooks: { SessionStart: [{ hooks: [{ type: 'command', command: 'existing-hook' }] }] } };
|
|
7
|
+
const script = '/home/jon/.agents/skills/session-continuity/continuity.mjs';
|
|
8
|
+
const first = mergeContinuityHooks(existing, 'claude', script);
|
|
9
|
+
assert.deepEqual(mergeContinuityHooks(first, 'claude', script), first);
|
|
10
|
+
assert.deepEqual(first.permissions, existing.permissions);
|
|
11
|
+
assert.equal(first.hooks.SessionStart[0].hooks[0].command, 'existing-hook');
|
|
12
|
+
assert.equal(existing.hooks.SessionStart.length, 1);
|
|
13
|
+
assert.throws(() => mergeContinuityHooks(first, 'claude', '/other/session-continuity/continuity.mjs'), /Another continuity installation/);
|
|
14
|
+
assert.throws(() => continuityHookConfiguration('claude', '/path/with$expansion/continuity.mjs'), /expansion/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('Cursor setup refuses an unsupported automatic activation claim without changing config', () => {
|
|
18
|
+
const existing = { version: 1, hooks: { beforeSubmitPrompt: [{ command: 'existing' }] } };
|
|
19
|
+
const before = JSON.stringify(existing);
|
|
20
|
+
assert.throws(() => mergeContinuityHooks(existing, 'cursor', '/skill/continuity.mjs'), /beforeSubmitPrompt has no agent-context output/);
|
|
21
|
+
assert.equal(JSON.stringify(existing), before);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
for (const host of ['claude', 'codex']) {
|
|
25
|
+
test(`${host}: setup registers only documented context-loading events`, () => {
|
|
26
|
+
const configuration = continuityHookConfiguration(host, '/home/jon/.agents/skills/session-continuity/continuity.mjs');
|
|
27
|
+
const expected = host === 'claude'
|
|
28
|
+
? ['UserPromptSubmit', 'UserPromptExpansion', 'SessionStart']
|
|
29
|
+
: ['UserPromptSubmit', 'SessionStart'];
|
|
30
|
+
assert.deepEqual(Object.keys(configuration), expected);
|
|
31
|
+
for (const groups of Object.values(configuration)) {
|
|
32
|
+
assert.equal(groups.length, 1);
|
|
33
|
+
assert.equal(groups[0].hooks[0].type, 'command');
|
|
34
|
+
assert.equal(groups[0].hooks[0].command, `node "/home/jon/.agents/skills/session-continuity/continuity.mjs" hook ${host}`);
|
|
35
|
+
}
|
|
36
|
+
assert.equal(new RegExp(configuration.SessionStart[0].matcher).test('compact'), true);
|
|
37
|
+
if (host === 'claude') {
|
|
38
|
+
assert.equal(new RegExp(configuration.UserPromptExpansion[0].matcher).test('pstack:poteto-mode'), true);
|
|
39
|
+
assert.equal(new RegExp(configuration.UserPromptExpansion[0].matcher).test('explain-poteto-mode'), false);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -185,7 +185,7 @@ test('the shipped Codex rules file has no production-scanner home-path findings'
|
|
|
185
185
|
);
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
test('--only journal
|
|
188
|
+
test('--only journal is rejected before Codex rules change; --only core copies them', () => {
|
|
189
189
|
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-codex-groups-'));
|
|
190
190
|
try {
|
|
191
191
|
const installedRulesPath = join(
|
|
@@ -194,7 +194,10 @@ test('--only journal skips Codex rules; --only core copies them', () => {
|
|
|
194
194
|
CODEX_RULES_DIRECTORY_NAME,
|
|
195
195
|
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
196
196
|
);
|
|
197
|
-
|
|
197
|
+
assert.throws(
|
|
198
|
+
() => runInstaller(homeDirectory, ['--only', 'journal']),
|
|
199
|
+
error => error.status === 1 && /Unknown group\(s\): journal/.test(error.stderr),
|
|
200
|
+
);
|
|
198
201
|
assert.equal(existsSync(installedRulesPath), false);
|
|
199
202
|
|
|
200
203
|
runInstaller(homeDirectory, ['--only', 'core']);
|
|
@@ -100,7 +100,7 @@ test('a full install writes stem-named Cursor rules and leaves a local extra mdc
|
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
test('--only journal
|
|
103
|
+
test('--only journal is rejected before Cursor rules change; --only core writes them', () => {
|
|
104
104
|
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-groups-'));
|
|
105
105
|
try {
|
|
106
106
|
const generatedPath = join(
|
|
@@ -115,7 +115,10 @@ test('--only journal skips Cursor rule generation; --only core writes them', ()
|
|
|
115
115
|
'rules',
|
|
116
116
|
'pstack-model-preferences.codex.json',
|
|
117
117
|
);
|
|
118
|
-
|
|
118
|
+
assert.throws(
|
|
119
|
+
() => runInstaller(homeDirectory, ['--only', 'journal']),
|
|
120
|
+
error => error.status === 1 && /Unknown group\(s\): journal/.test(error.stderr),
|
|
121
|
+
);
|
|
119
122
|
assert.equal(existsSync(generatedPath), false);
|
|
120
123
|
assert.equal(existsSync(preferencePath), false);
|
|
121
124
|
|