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.
Files changed (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +213 -0
  6. package/.agents/skills/session-continuity/SKILL.md +125 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +447 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +88 -0
  20. package/bin/install-session-continuity.test.mjs +55 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
package/bin/cde.test.mjs CHANGED
@@ -7,6 +7,7 @@ import test from "node:test";
7
7
 
8
8
  import {
9
9
  buildLintCommand,
10
+ buildVerifyCommand,
10
11
  createHelpText,
11
12
  findPython,
12
13
  main,
@@ -43,6 +44,8 @@ test("help describes lint source modes", () => {
43
44
  assert.match(helpText, /--base/);
44
45
  assert.match(helpText, /--repository/);
45
46
  assert.match(helpText, /--text-as/);
47
+ assert.match(helpText, /cde verify/);
48
+ assert.match(helpText, /--manifest/);
46
49
  });
47
50
 
48
51
 
@@ -67,6 +70,51 @@ test("forwards lint arguments to the python command without a shell", () => {
67
70
  });
68
71
 
69
72
 
73
+ test("forwards verification arguments to the authoritative python command", () => {
74
+ const command = buildVerifyCommand("python-test", [
75
+ "--manifest",
76
+ "manifest with spaces.json",
77
+ "--repo",
78
+ "repo with spaces",
79
+ "--base",
80
+ "base-sha",
81
+ "--output",
82
+ "report.json",
83
+ ]);
84
+ assert.equal(command.executable, "python-test");
85
+ assert.match(command.arguments[0], /scripts[\\/]local_verification[\\/]cli\.py$/);
86
+ assert.deepEqual(command.arguments.slice(1), [
87
+ "--manifest",
88
+ "manifest with spaces.json",
89
+ "--repo",
90
+ "repo with spaces",
91
+ "--base",
92
+ "base-sha",
93
+ "--output",
94
+ "report.json",
95
+ ]);
96
+ });
97
+
98
+
99
+ test("dispatches verify without changing forwarded arguments", async () => {
100
+ let receivedCommand;
101
+ const exitCode = await main(
102
+ ["verify", "--python", "python-test", "--manifest", "manifest.json"],
103
+ {
104
+ findPython: async () => "unused-python",
105
+ runCommand: async (command) => {
106
+ receivedCommand = command;
107
+ return 0;
108
+ },
109
+ },
110
+ );
111
+ assert.equal(exitCode, 0);
112
+ assert.equal(receivedCommand.executable, "python-test");
113
+ assert.match(receivedCommand.arguments[0], /local_verification[\\/]cli\.py$/);
114
+ assert.deepEqual(receivedCommand.arguments.slice(1), ["--manifest", "manifest.json"]);
115
+ });
116
+
117
+
70
118
  test("strips the launcher python override from forwarded arguments", async () => {
71
119
  let receivedCommand;
72
120
  const exitCode = await main(["lint", "--python", "python-test", "--staged"], {
@@ -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
- * Install-time expansion of $HOME / ${HOME} / ~/ in settings.json commands.
2
+ * Expand $HOME, ${HOME}, and ~/ in settings.json commands at install time.
3
3
  *
4
- * Hosts that require referenced env vars at hook load time (for example a third-party host)
5
- * skip the hook when HOME is unset — common on Windows — so the installer
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"'=])~\//g,
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
- + `import ${pythonModuleName}\n`
86
- + '\n'
87
- + `sys.exit(${pythonModuleName}.main())\n`
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 exclude post-commit', () => {
62
- assert.equal(KNOWN_GIT_HOOK_NAMES.includes('post-commit'), false);
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 does not create a post-commit shim', () => {
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')), false);
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,88 @@
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
+ const supportedHosts = ['claude', 'codex', 'cursor'];
6
+
7
+ export function continuityHookConfiguration(host, script) {
8
+ if (!supportedHosts.includes(host)) throw new Error(`Unsupported host ${host}`);
9
+ if (/[\r\n"`$%]/.test(script)) throw new Error('Hook script path contains shell expansion characters');
10
+ const command = `node "${script.replace(/\\/g, '/')}" hook ${host}`;
11
+ if (host === 'cursor') {
12
+ return Object.fromEntries(['sessionStart', 'beforeSubmitPrompt', 'preCompact', 'postToolUse'].map(event => [event, [{ command }]]));
13
+ }
14
+ const events = host === 'claude' ? ['UserPromptSubmit', 'UserPromptExpansion', 'SessionStart'] : ['UserPromptSubmit', 'SessionStart'];
15
+ return Object.fromEntries(events.map(event => [event, [{
16
+ ...(event === 'SessionStart' ? { matcher: host === 'claude' ? 'startup|resume|compact|clear|fork' : 'startup|resume|compact|clear' } : {}),
17
+ ...(event === 'UserPromptExpansion' ? { matcher: '^(pstack:)?poteto-mode$|^(claude-dev-env:)?session-continuity$' } : {}),
18
+ hooks: [{ type: 'command', command, timeout: 10 }],
19
+ }]]));
20
+ }
21
+
22
+ export function mergeContinuityHooks(existing, host, script) {
23
+ const additions = continuityHookConfiguration(host, script);
24
+ const mergedConfiguration = structuredClone(existing);
25
+ if (host === 'cursor') mergedConfiguration.version = 1;
26
+ mergedConfiguration.hooks ||= {};
27
+ for (const [event, groups] of Object.entries(additions)) {
28
+ const current = mergedConfiguration.hooks[event] || [];
29
+ const command = host === 'cursor' ? groups[0].command : groups[0].hooks[0].command;
30
+ for (const entry of current) {
31
+ for (const hook of entry.hooks || [entry]) {
32
+ if (hook.command?.includes('/session-continuity/continuity.mjs') && hook.command !== command) {
33
+ throw new Error('Another continuity installation owns this host config. Select its profile or remove that installation explicitly.');
34
+ }
35
+ }
36
+ }
37
+ const kept = current.flatMap(entry => {
38
+ if (host === 'cursor') return entry.command === command ? [] : [entry];
39
+ const hooks = (entry.hooks || []).filter(hook => hook.command !== command);
40
+ return hooks.length ? [{ ...entry, hooks }] : [];
41
+ });
42
+ mergedConfiguration.hooks[event] = kept.concat(groups);
43
+ }
44
+ return mergedConfiguration;
45
+ }
46
+
47
+ async function main() {
48
+ const requested = process.argv.slice(2);
49
+ const selected = requested.length ? requested : supportedHosts;
50
+ const unsupported = selected.find(host => !supportedHosts.includes(host));
51
+ if (unsupported) throw new Error(`Unsupported host ${unsupported}`);
52
+ const { resolveInstallRoot } = await import('./resolve-install-root.mjs');
53
+ const roots = resolveInstallRoot();
54
+ const script = join(roots.skillsInstallDirectory, 'session-continuity', 'continuity.mjs');
55
+ if (!existsSync(script) || !existsSync(join(dirname(script), 'SKILL.md'))) {
56
+ throw new Error('Run the full claude-dev-env installer from this checkout first so the companion is in the canonical agents home.');
57
+ }
58
+ const paths = {
59
+ claude: join(roots.managedRoot, 'settings.json'),
60
+ codex: join(dirname(roots.codexRulesInstallDirectory), 'hooks.json'),
61
+ cursor: join(roots.cursorInstallDirectory, 'hooks.json'),
62
+ };
63
+ const plans = selected.map(host => {
64
+ const path = paths[host];
65
+ if (!existsSync(dirname(path))) throw new Error(`Host config directory is absent: ${dirname(path)}`);
66
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
67
+ return { path, existing, content: JSON.stringify(mergeContinuityHooks(existing ? JSON.parse(existing) : {}, host, script), null, 2) + '\n' };
68
+ });
69
+ for (const plan of plans) {
70
+ if (plan.existing === plan.content) { console.log(`Already configured: ${plan.path}`); continue; }
71
+ if (plan.existing !== null) {
72
+ const backup = `${plan.path}.before-session-continuity`;
73
+ if (!existsSync(backup)) writeFileSync(backup, plan.existing, { flag: 'wx', mode: 0o600 });
74
+ }
75
+ const current = existsSync(plan.path) ? readFileSync(plan.path, 'utf8') : null;
76
+ if (current !== plan.existing) throw new Error(`Config changed during setup: ${plan.path}. Rerun setup.`);
77
+ const temporary = `${plan.path}.continuity-${process.pid}.tmp`;
78
+ writeFileSync(temporary, plan.content, { flag: 'wx', mode: 0o600 });
79
+ renameSync(temporary, plan.path);
80
+ if (readFileSync(plan.path, 'utf8') !== plan.content) throw new Error(`Config read-back mismatch: ${plan.path}`);
81
+ console.log(`Configured and read back: ${plan.path}`);
82
+ }
83
+ console.log('Review and trust the new hooks in each host.');
84
+ }
85
+
86
+ if (process.argv[1] && resolve(realpathSync(process.argv[1])) === fileURLToPath(import.meta.url)) {
87
+ main().catch(error => { console.error(error.message); process.exitCode = 1; });
88
+ }
@@ -0,0 +1,55 @@
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 keeps other hooks, sets the file version, and stays idempotent', () => {
18
+ const existing = { hooks: { beforeShellExecution: [{ command: 'existing' }] } };
19
+ const script = '/home/jon/.agents/skills/session-continuity/continuity.mjs';
20
+ const first = mergeContinuityHooks(existing, 'cursor', script);
21
+ assert.equal(first.version, 1);
22
+ assert.equal(first.hooks.beforeShellExecution[0].command, 'existing');
23
+ assert.deepEqual(mergeContinuityHooks(first, 'cursor', script), first);
24
+ assert.equal(existing.hooks.beforeShellExecution.length, 1);
25
+ assert.throws(() => mergeContinuityHooks(first, 'cursor', '/other/session-continuity/continuity.mjs'), /Another continuity installation/);
26
+ });
27
+
28
+ test('Cursor setup registers only documented context-carrying events', () => {
29
+ const configuration = continuityHookConfiguration('cursor', '/home/jon/.agents/skills/session-continuity/continuity.mjs');
30
+ assert.deepEqual(Object.keys(configuration), ['sessionStart', 'beforeSubmitPrompt', 'preCompact', 'postToolUse']);
31
+ for (const entries of Object.values(configuration)) {
32
+ assert.equal(entries.length, 1);
33
+ assert.equal(entries[0].command, 'node "/home/jon/.agents/skills/session-continuity/continuity.mjs" hook cursor');
34
+ }
35
+ });
36
+
37
+ for (const host of ['claude', 'codex']) {
38
+ test(`${host}: setup registers only documented context-loading events`, () => {
39
+ const configuration = continuityHookConfiguration(host, '/home/jon/.agents/skills/session-continuity/continuity.mjs');
40
+ const expected = host === 'claude'
41
+ ? ['UserPromptSubmit', 'UserPromptExpansion', 'SessionStart']
42
+ : ['UserPromptSubmit', 'SessionStart'];
43
+ assert.deepEqual(Object.keys(configuration), expected);
44
+ for (const groups of Object.values(configuration)) {
45
+ assert.equal(groups.length, 1);
46
+ assert.equal(groups[0].hooks[0].type, 'command');
47
+ assert.equal(groups[0].hooks[0].command, `node "/home/jon/.agents/skills/session-continuity/continuity.mjs" hook ${host}`);
48
+ }
49
+ assert.equal(new RegExp(configuration.SessionStart[0].matcher).test('compact'), true);
50
+ if (host === 'claude') {
51
+ assert.equal(new RegExp(configuration.UserPromptExpansion[0].matcher).test('pstack:poteto-mode'), true);
52
+ assert.equal(new RegExp(configuration.UserPromptExpansion[0].matcher).test('explain-poteto-mode'), false);
53
+ }
54
+ });
55
+ }
@@ -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 skips Codex rules; --only core copies them', () => {
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
- runInstaller(homeDirectory, ['--only', 'journal']);
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']);