claude-dev-env 7.1.0 → 7.2.1

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.
@@ -15,7 +15,11 @@ Search files instantly on Windows using the Everything command-line interface (e
15
15
 
16
16
  Every search carries a scope: a project path or registry token, an `ext:` filter, a `dm:` date filter, a `size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds. Narrow the search to what you need.
17
17
 
18
- When `es.exe` fails or returns nothing, self-heal first: fall back to the `Glob` tool (name and path patterns) or `Grep` (file contents), and report the outage so the reader knows the index was unavailable. When self-healing also fails, ask the user through `AskUserQuestion` with a short analysis and next-step options.
18
+ When `es.exe` returns Error 8, the Everything IPC client window is missing. Retry the same search twice with a short pause. Do not report that Everything is not running, not loaded, or that the index is unavailable from that first miss. After those retries fail, report that the Everything IPC client did not answer and that the service state was not probed, then fall back to the `Glob` tool (name and path patterns) or `Grep` (file contents) with the same scope.
19
+
20
+ When `es.exe` is missing, or a later search returns no hits, fall back to `Glob` or `Grep`. A failed search is not proof the index is down. When those fallbacks also fail, ask the user through `AskUserQuestion` with a short analysis and next-step options.
21
+
22
+ Direct `es.exe` calls use the same Error 8 retry. The wrapper in `scripts/everything_search.py` already retries Error 8 twice.
19
23
 
20
24
  ## Registry tokens
21
25
 
@@ -139,7 +143,8 @@ Run separate searches for each extension type:
139
143
 
140
144
  ## Best Practices
141
145
 
142
- - Everything must be running (system tray) for es.exe to work
146
+ - `es.exe` talks to the Everything tray client IPC window. The Everything service can be running while Error 8 still fires.
147
+ - On Error 8, retry the same search twice before treating the search as failed.
143
148
  - Run one extension per search for cleaner results
144
149
  - Use `dm:` to filter recent files by the index timestamp
145
150
  - Combine with path to narrow scope
@@ -165,5 +170,5 @@ Everything indexes REAL paths only, not junctions or mapped drives.
165
170
 
166
171
  ## Files
167
172
 
168
- - `scripts/everything_search.py` runs one Everything search without a shell.
169
- - `scripts/test_everything_search.py` verifies registry expansion and command failures.
173
+ - `scripts/everything_search.py` runs one Everything search without a shell, retries Error 8 twice, and reports that the IPC client did not answer and that the service state was not probed when those retries fail.
174
+ - `scripts/test_everything_search.py` verifies registry expansion, command failures, Error 8 retry, and the IPC-client miss message.
@@ -4,6 +4,7 @@ import json
4
4
  import shutil
5
5
  import subprocess
6
6
  import sys
7
+ import time
7
8
  from collections.abc import Mapping, Sequence
8
9
  from pathlib import Path, PurePosixPath, PureWindowsPath
9
10
  from typing import TextIO
@@ -14,6 +15,10 @@ from everything_search_command_constants.config.constants import (
14
15
  EXECUTION_ERROR_EXIT_CODE,
15
16
  INFORMATIONAL_ARGUMENT,
16
17
  INVALID_INPUT_EXIT_CODE,
18
+ IPC_CLIENT_DID_NOT_ANSWER_MESSAGE,
19
+ IPC_WINDOW_NOT_FOUND_EXIT_CODE,
20
+ IPC_WINDOW_RETRY_COUNT,
21
+ IPC_WINDOW_RETRY_DELAY_SECONDS,
17
22
  PROJECT_PATHS_FILE_NAME,
18
23
  REGISTRY_META_KEY,
19
24
  SEARCH_SCOPE_REQUIRED_MESSAGE,
@@ -115,21 +120,32 @@ def _run_search(
115
120
  search_stdout: TextIO,
116
121
  search_stderr: TextIO,
117
122
  ) -> int:
118
- try:
119
- search_run = subprocess.run(
120
- [executable_path, *all_search_arguments],
121
- capture_output=True,
122
- check=False,
123
- encoding=UTF8_ENCODING,
124
- shell=False,
125
- text=True,
126
- )
127
- except OSError as error:
128
- search_stderr.write(f"{error}\n")
129
- return EXECUTION_ERROR_EXIT_CODE
130
- search_stdout.write(search_run.stdout)
131
- search_stderr.write(search_run.stderr)
132
- return search_run.returncode
123
+ ipc_client_did_not_answer_message = IPC_CLIENT_DID_NOT_ANSWER_MESSAGE
124
+ ipc_window_not_found_exit_code = IPC_WINDOW_NOT_FOUND_EXIT_CODE
125
+ ipc_window_retry_count = IPC_WINDOW_RETRY_COUNT
126
+ ipc_window_retry_delay_seconds = IPC_WINDOW_RETRY_DELAY_SECONDS
127
+ last_search_run = None
128
+ for each_attempt_index in range(ipc_window_retry_count + 1):
129
+ try:
130
+ last_search_run = subprocess.run(
131
+ [executable_path, *all_search_arguments],
132
+ capture_output=True,
133
+ check=False,
134
+ encoding=UTF8_ENCODING,
135
+ shell=False,
136
+ text=True,
137
+ )
138
+ except OSError as error:
139
+ search_stderr.write(f"{error}\n")
140
+ return EXECUTION_ERROR_EXIT_CODE
141
+ if last_search_run.returncode != ipc_window_not_found_exit_code:
142
+ search_stdout.write(last_search_run.stdout)
143
+ search_stderr.write(last_search_run.stderr)
144
+ return last_search_run.returncode
145
+ if each_attempt_index < ipc_window_retry_count:
146
+ time.sleep(ipc_window_retry_delay_seconds)
147
+ search_stderr.write(ipc_client_did_not_answer_message)
148
+ return ipc_window_not_found_exit_code
133
149
 
134
150
 
135
151
  def _run_registered_search(
@@ -5,6 +5,12 @@ EXECUTABLE_NAME = "es.exe"
5
5
  EXECUTION_ERROR_EXIT_CODE = 1
6
6
  INFORMATIONAL_ARGUMENT = "-version"
7
7
  INVALID_INPUT_EXIT_CODE = 2
8
+ IPC_CLIENT_DID_NOT_ANSWER_MESSAGE = (
9
+ "Everything IPC client did not answer. Everything service state was not probed.\n"
10
+ )
11
+ IPC_WINDOW_NOT_FOUND_EXIT_CODE = 8
12
+ IPC_WINDOW_RETRY_COUNT = 2
13
+ IPC_WINDOW_RETRY_DELAY_SECONDS = 0.5
8
14
  PROJECT_PATHS_FILE_NAME = "project-paths.json"
9
15
  REGISTRY_META_KEY = "_meta"
10
16
  SEARCH_SCOPE_REQUIRED_MESSAGE = "The first search argument must define a path, project, name, extension, date, or size scope.\n"
@@ -113,12 +113,79 @@ def test_main_passes_through_child_exit_status() -> None:
113
113
  with (
114
114
  patch.object(search, "load_registry", return_value={}),
115
115
  patch.object(search.shutil, "which", return_value="es.exe"),
116
- patch.object(search.subprocess, "run", return_value=completed_process),
116
+ patch.object(
117
+ search.subprocess, "run", return_value=completed_process
118
+ ) as run_process,
117
119
  patch.object(search.sys, "stderr", captured_stderr),
118
120
  ):
119
121
  exit_code = search.main(["config.py"])
120
122
  assert exit_code == 7
121
123
  assert captured_stderr.getvalue() == "failed\n"
124
+ run_process.assert_called_once()
125
+
126
+
127
+ def _ipc_window_error_process() -> search.subprocess.CompletedProcess:
128
+ return search.subprocess.CompletedProcess(
129
+ args=["es.exe", "config.py"],
130
+ returncode=8,
131
+ stdout="",
132
+ stderr=(
133
+ "Error 8: Everything IPC window not found. "
134
+ "Please make sure Everything is running.\n"
135
+ ),
136
+ )
137
+
138
+
139
+ def test_main_retries_ipc_window_error_then_returns_search_hits() -> None:
140
+ successful_process = search.subprocess.CompletedProcess(
141
+ args=["es.exe", "config.py"],
142
+ returncode=0,
143
+ stdout="found path\n",
144
+ stderr="",
145
+ )
146
+ captured_stdout = io.StringIO()
147
+ captured_stderr = io.StringIO()
148
+ with (
149
+ patch.object(search, "load_registry", return_value={}),
150
+ patch.object(search.shutil, "which", return_value="es.exe"),
151
+ patch.object(
152
+ search.subprocess,
153
+ "run",
154
+ side_effect=[_ipc_window_error_process(), successful_process],
155
+ ) as run_process,
156
+ patch.object(search.sys, "stdout", captured_stdout),
157
+ patch.object(search.sys, "stderr", captured_stderr),
158
+ patch("time.sleep"),
159
+ ):
160
+ exit_code = search.main(["config.py"])
161
+ assert exit_code == 0
162
+ assert captured_stdout.getvalue() == "found path\n"
163
+ assert "not loaded" not in captured_stderr.getvalue().casefold()
164
+ assert "not running" not in captured_stderr.getvalue().casefold()
165
+ assert run_process.call_count == 2
166
+
167
+
168
+ def test_main_reports_ipc_client_miss_after_ipc_window_retries_exhaust() -> None:
169
+ captured_stderr = io.StringIO()
170
+ with (
171
+ patch.object(search, "load_registry", return_value={}),
172
+ patch.object(search.shutil, "which", return_value="es.exe"),
173
+ patch.object(
174
+ search.subprocess,
175
+ "run",
176
+ return_value=_ipc_window_error_process(),
177
+ ) as run_process,
178
+ patch.object(search.sys, "stderr", captured_stderr),
179
+ patch("time.sleep"),
180
+ ):
181
+ exit_code = search.main(["config.py"])
182
+ assert exit_code == 8
183
+ stderr_text = captured_stderr.getvalue()
184
+ assert "Everything IPC client did not answer" in stderr_text
185
+ assert "service state was not probed" in stderr_text.casefold()
186
+ assert "Please make sure Everything is running" not in stderr_text
187
+ assert "not loaded" not in stderr_text.casefold()
188
+ assert run_process.call_count == 3
122
189
 
123
190
 
124
191
  def test_main_reports_missing_executable() -> None:
@@ -1,9 +1,12 @@
1
1
  # Session continuity companion
2
2
 
3
- Status: draft implementation. Claude and Codex adapters have repository-level
4
- contract tests. Installed-host activation, reload, and agent compliance remain
5
- unverified. Cursor automatic activation is unsupported by this implementation.
6
- All three hosts remain part of the requested outcome.
3
+ Status: draft implementation. Claude, Codex, and Cursor adapters have
4
+ repository-level contract tests. Installed-host activation, reload, and agent
5
+ compliance remain unverified.
6
+
7
+ Every supported host activates Poteto Mode automatically when a session starts
8
+ and when the host resumes the conversation after compaction. An explicit
9
+ invocation still works and still records the user's own scope.
7
10
 
8
11
  ## The persistence failure
9
12
 
@@ -12,6 +15,10 @@ lose its duration, later corrections, or the instruction to reload it. This
12
15
  companion saves user-scoped requirements under a stable host-session key and
13
16
  returns the current skill contents during supported recovery callbacks.
14
17
 
18
+ Automatic activation removes the first half of that failure. A session that
19
+ never carried an explicit invocation still starts with Poteto Mode loaded, so
20
+ the mode does not depend on the user retyping it after each restart.
21
+
15
22
  ## Ownership and setup
16
23
 
17
24
  Pstack owns `Poteto Mode`, directory `poteto-mode`. Its source description names
@@ -33,7 +40,8 @@ The existing selective `--only core` list does not include the companion in this
33
40
  draft. Use the full install above. Plugin-only installations need the same
34
41
  canonical installed skill before this setup can run.
35
42
 
36
- The setup defaults to `claude codex`. Pass either host name to configure it alone.
43
+ The setup defaults to `claude codex cursor`. Pass one or more host names to
44
+ configure that subset alone.
37
45
  It uses `bin/resolve-install-root.mjs`, including `CLAUDE_CONFIG_DIR` and
38
46
  `CODEX_HOME`. It merges only its own hook groups into Claude `settings.json` and
39
47
  Codex `hooks.json`, preserves other settings, writes a one-time backup before
@@ -51,10 +59,13 @@ that has not been tested. Use builds exposing the documented events below.
51
59
 
52
60
  | Host | Adapter input | Forms recognized by the companion |
53
61
  | --- | --- | --- |
62
+ | Claude Code | `SessionStart` with source `startup`, `resume`, `compact`, `clear`, or `fork` | Automatic session-scope activation, no user text needed |
54
63
  | Claude Code | `UserPromptExpansion`, `expansion_type=slash_command` | Existing `poteto-mode` and `pstack:poteto-mode` command names |
55
64
  | Claude Code | `UserPromptSubmit` | A leading `/poteto-mode` or `/pstack:poteto-mode`; exact natural forms below |
65
+ | Codex | `SessionStart` with source `startup`, `resume`, `compact`, or `clear` | Automatic session-scope activation, no user text needed |
56
66
  | Codex | `UserPromptSubmit` | A leading `$poteto-mode`, `$pstack:poteto-mode`, or `[$pstack:poteto-mode](path)` skill reference; exact natural forms below |
57
- | Cursor | No automatic adapter installed | The inspected prompt and lifecycle hooks do not establish the required timing |
67
+ | Cursor | `sessionStart` | Automatic session-scope activation, returned as `additional_context` |
68
+ | Cursor | `beforeSubmitPrompt` | The same leading command and natural forms; the prompt is never blocked |
58
69
 
59
70
  Natural forms include bare `poteto`, `Use Poteto Mode`, `Activate Poteto Mode`,
60
71
  `Invoke Poteto Mode`, and `Poteto Mode applies for this entire session`. Exact
@@ -65,6 +76,12 @@ Free-form style requests and host UI forms that omit the skill token from the
65
76
  prompt remain unverified. The adapter's recognition is not proof that every host
66
77
  resolves every form as a native skill invocation.
67
78
 
79
+ Automatic activation records one session-scope skill requirement carrying the
80
+ `automatic` flag. A later explicit invocation writes the entry again with the
81
+ user's own evidence quote and scope. An explicit deactivation stops automatic
82
+ recovery for the same host session; a Claude or Codex `clear` starts a fresh
83
+ automatic set under the reused session id.
84
+
68
85
  The companion does not define a `/potato-mode` alias or rename `poteto-mode`.
69
86
  Quotations, fenced examples, indented code, discussion, imported transcripts, and
70
87
  tool-result callbacks do not independently activate the companion. Subsequent
@@ -111,12 +128,14 @@ messages and source comparison snapshots and stay outside the repository.
111
128
 
112
129
  | Situation | Discovery and restoration |
113
130
  | --- | --- |
114
- | First recognized invocation | Hook creates the record, returns its exact path and read-back, and includes the complete companion instructions |
131
+ | Session start with no record | Hook creates an active record holding a session-scope Poteto Mode requirement and returns its path, read-back, and current source |
132
+ | First recognized invocation | Hook creates or updates the record, returns its exact path and read-back, and includes the complete companion instructions |
115
133
  | Later user prompt | Same session key; pending evidence is saved and the companion directs the agent to reconcile it before dependent work |
116
134
  | Claude/Codex startup or resume | `SessionStart` with the same id finds the active record and reads current active skill sources |
117
135
  | Claude/Codex compaction | `SessionStart` with source `compact` reloads the same record and sources |
118
- | New unrelated conversation | A new id has no record and inherits nothing |
119
- | Clear | The same-id record is deactivated; a new id starts empty |
136
+ | Cursor compaction | `preCompact` marks the record for reload; the next `postToolUse` returns the record and sources as `additional_context` |
137
+ | New unrelated conversation | A new id starts a fresh automatic set and inherits no task, rule, or pending message |
138
+ | Clear | The same-id record holds a fresh automatic set |
120
139
  | Explicit handoff | The source command binds a snapshot to a known destination host/id; its next supported start callback discovers that record |
121
140
  | Explicit deactivation | An inactive tombstone suppresses recovery until another explicit invocation creates a fresh active set |
122
141
 
@@ -136,15 +155,20 @@ host-truncated source in full before dependent work. Missing sources produce an
136
155
  explicit unavailable message. Changed sources include current contents and a
137
156
  bounded before/after comparison. Stored snapshots are for comparison only.
138
157
 
139
- Cursor's `beforeSubmitPrompt` returns `continue` and a blocked-user message, not
140
- agent context. Its `preToolUse` can send agent context by denying a pending tool,
141
- and `postToolUse` can inject context after a tool runs. That does not cover a
142
- reply-only invocation before dependent work. `sessionStart` is fire-and-forget
143
- when a composer conversation is created; `preCompact` cannot block or modify
144
- compaction. Startup reminders, first-tool-only gates, or stop-hook follow-ups
145
- would weaken the requested contract. This draft installs none of those as a
146
- substitute. Shared storage accepts a `cursor` namespace for explicit operations;
147
- that is not automatic Cursor support.
158
+ Cursor's documented `sessionStart` output carries `additional_context`, so the
159
+ Cursor adapter loads the record and the current sources when a composer
160
+ conversation is created. Cursor has no post-compaction event that returns agent
161
+ context: `preCompact` runs before compaction and returns only a user message.
162
+ The adapter therefore marks the record during `preCompact` and returns the
163
+ record and sources through the next `postToolUse`, whose documented output
164
+ includes `additional_context`. On Cursor the reload lands with the agent's first
165
+ tool result after compaction rather than before it, which is the earliest
166
+ documented delivery point.
167
+
168
+ Cursor's `beforeSubmitPrompt` output carries `continue` and a user message, not
169
+ agent context. The adapter uses it to save pending user evidence and to mark an
170
+ explicit invocation for the next reload. It always returns `continue: true`, so
171
+ a companion failure never blocks the user's prompt.
148
172
 
149
173
  Automatic prompt capture and source emission are code paths. Selecting actual
150
174
  user rules, checkpointing, loading shortened sources, and following instructions
@@ -153,7 +177,7 @@ compliance merely because a callback ran.
153
177
 
154
178
  ## Validation and remaining installed-host proof
155
179
 
156
- Run the repository contract tests:
180
+ Run the repository contract tests, which cover all three host adapters:
157
181
 
158
182
  ```sh
159
183
  node --test packages/claude-dev-env/.agents/skills/session-continuity/continuity.test.mjs \
@@ -177,10 +201,10 @@ real compaction and a same-conversation restart, supply no record path, and
177
201
  capture discovery plus source reload before dependent work. Repeat with a later
178
202
  correction, a new skill, changed/missing sources, simultaneous sessions,
179
203
  quoted-only mentions, explicit handoff, deactivation, and reactivation. Keep
180
- Poteto's source hash unchanged throughout. Cursor needs a supported pre-work
181
- loading mechanism before equivalent installed-host acceptance can be run.
204
+ Poteto's source hash unchanged throughout. On Cursor, also capture the tool call
205
+ whose `postToolUse` result carries the reload after a compaction.
182
206
 
183
- ## Primary references checked on 2026-09-06
207
+ ## Primary references checked on 2026-09-07
184
208
 
185
209
  - [Pstack Poteto Mode source](https://github.com/cursor/plugins/blob/main/pstack/skills/poteto-mode/SKILL.md)
186
210
  - [Claude Code hooks](https://code.claude.com/docs/en/hooks)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: session-continuity
3
- description: Preserve user-scoped task requirements and named skills in a host-session record. Loaded by the separate Poteto companion hook or explicitly invoked as session-continuity. triggers with poteto-mode, poteto, /poteto-mode, /poteto.
3
+ description: Preserve user-scoped task requirements and named skills in a host-session record, and activate Poteto Mode automatically at session start and after compaction. Loaded by the separate Poteto companion hook or explicitly invoked as session-continuity. triggers with poteto-mode, poteto, /poteto-mode, /poteto.
4
4
  ---
5
5
 
6
6
  # Session continuity
@@ -11,6 +11,16 @@ Preserve the user's instructions at their actual scope. Restore the saved record
11
11
  and the current skill sources before work that depends on them. Keep Poteto Mode
12
12
  and its invocation unchanged.
13
13
 
14
+ ## Automatic activation
15
+
16
+ Each supported host runs this companion when a session starts and when it
17
+ resumes after compaction. With no saved record, the hook writes one holding a
18
+ session-scope `pstack:poteto-mode` requirement marked `automatic`, then returns
19
+ the current Poteto Mode source. Treat that source as the mode the user asked for
20
+ at session scope. A later explicit invocation writes the entry again with
21
+ the user's own quote and scope. An explicit deactivation stops the automatic
22
+ recovery for that host session.
23
+
14
24
  ## On activation or recovery
15
25
 
16
26
  1. Immediately report the exact absolute record path printed by the hook.
@@ -102,13 +112,14 @@ authoritative location is restored or the user changes the requirement.
102
112
  `/session-continuity off` or `Deactivate session continuity.` deactivates the
103
113
  companion record. This does not change Poteto Mode. A later explicit Poteto or
104
114
  companion invocation starts a new active set in the same record location.
105
- Deactivated requirements stay out of recovery.
115
+ Deactivated requirements stay out of recovery, and automatic activation stays
116
+ off for that host session until an explicit invocation or a `clear` arrives.
106
117
 
107
118
  ## Verification limits
108
119
 
109
- Read `README.md` for setup and supported trigger forms. Claude and Codex have
110
- repository-tested hook adapters. Installed-host delivery, source loading, and
111
- agent compliance require separate live evidence. Cursor automatic activation
112
- and recovery are not implemented because the inspected prompt and lifecycle
113
- hooks do not establish the required pre-work delivery contract. A manual storage
114
- command is not evidence of automatic activation in any host.
120
+ Read `README.md` for setup and supported trigger forms. Claude, Codex, and
121
+ Cursor have repository-tested hook adapters. Installed-host delivery, source
122
+ loading, and agent compliance require separate live evidence. On Cursor the
123
+ post-compaction reload arrives with the first tool result after compaction,
124
+ because Cursor documents no post-compaction event that returns agent context.
125
+ A manual storage command is not evidence of automatic activation in any host.
@@ -7,6 +7,9 @@ const skillDirectory = dirname(fileURLToPath(import.meta.url));
7
7
  const hosts = new Set(['claude', 'codex', 'cursor']);
8
8
  const potetoNames = new Set(['poteto-mode', 'pstack:poteto-mode']);
9
9
  const companionNames = new Set(['session-continuity', 'claude-dev-env:session-continuity']);
10
+ const cursorEvents = new Set(['sessionStart', 'beforeSubmitPrompt', 'preCompact', 'postToolUse']);
11
+ const automaticQuote = 'Automatic activation configured in the installed host hook';
12
+ const automaticDuration = 'This host session, including its resumes and compactions';
10
13
  const digest = value => createHash('sha256').update(value).digest('hex');
11
14
  const encode = value => JSON.stringify(value, null, 2);
12
15
  const stateRoot = () => resolve(process.env.CDE_CONTINUITY_ROOT || join(skillDirectory, '..', '..', 'state', 'session-continuity'));
@@ -116,16 +119,31 @@ function sourceSnapshot(source) {
116
119
 
117
120
  function promptEvidence(payload) {
118
121
  const text = requireText(payload.prompt, 'user prompt');
119
- return { id: digest(`${payload.turn_id || ''}\0${text}`), text, authority: 'user-message-evidence' };
122
+ return { id: digest(`${payload.turn_id || payload.generation_id || ''}\0${text}`), text, authority: 'user-message-evidence' };
120
123
  }
121
124
 
122
- function activate(previous, host, session, payload, trigger) {
123
- const record = previous?.status === 'active' ? previous : {
125
+ function emptyRecord(host, session, payload) {
126
+ return {
124
127
  schema: 1, host, session_id: session, status: 'active', revision: 0,
125
- workspace: payload.cwd || '',
128
+ workspace: payload.cwd || (payload.workspace_roots || [])[0] || '',
126
129
  task: { id: randomUUID(), goal: '', boundaries: [], constraints: [], completion: [], completed: false },
127
130
  requirements: [], pending: [], checkpoint: { completed: [], remaining: [] },
128
131
  };
132
+ }
133
+
134
+ function automaticRecord(host, session, payload) {
135
+ const record = emptyRecord(host, session, payload);
136
+ record.requirements.push({
137
+ id: 'skill:pstack:poteto-mode', kind: 'skill', name: 'pstack:poteto-mode', active: true,
138
+ scope: 'session', task_id: null, duration: automaticDuration, automatic: true,
139
+ evidence: { prompt_id: null, quote: automaticQuote },
140
+ ...sourceSnapshot(potetoSource()),
141
+ });
142
+ return record;
143
+ }
144
+
145
+ function activate(previous, host, session, payload, trigger) {
146
+ const record = previous?.status === 'active' ? previous : emptyRecord(host, session, payload);
129
147
  if (record.task.completed) {
130
148
  record.requirements.forEach(requirement => { if (requirement.scope === 'task') requirement.active = false; });
131
149
  record.task = { id: randomUUID(), goal: '', boundaries: [], constraints: [], completion: [], completed: false };
@@ -141,7 +159,7 @@ function activate(previous, host, session, payload, trigger) {
141
159
  ? { source: current.source, sha256: current.sha256, comparison_text: current.comparison_text }
142
160
  : sourceSnapshot(selectedSource);
143
161
  const entry = {
144
- id, kind: 'skill', name: 'pstack:poteto-mode', active: true,
162
+ id, kind: 'skill', name: 'pstack:poteto-mode', active: true, automatic: false,
145
163
  scope, task_id: scope === 'task' ? record.task.id : null,
146
164
  duration: trigger.scope ? trigger.text : current?.duration || 'Current task, until completed or explicitly changed by the user',
147
165
  evidence: { prompt_id: evidence.id, quote: trigger.text },
@@ -200,8 +218,61 @@ function render(record, path, loadSources) {
200
218
  return sections.join('\n\n');
201
219
  }
202
220
 
221
+ const deactivationRequest = prompt => /^(?:[/$](?:claude-dev-env:)?session-continuity off|deactivate session continuity)[.!]?\s*$/i.test(prompt || '');
222
+
223
+ function sessionOpening(host, session, path, previous, payload, reset) {
224
+ if (!reset && previous) {
225
+ if (previous.status !== 'active') return {};
226
+ return { context: render(previous, path, true) };
227
+ }
228
+ try {
229
+ const saved = transact(host, session, () => automaticRecord(host, session, payload));
230
+ return { message: `Activated Poteto Mode for this session and read the record back: ${saved.path}`,
231
+ context: render(saved.record, saved.path, true) };
232
+ } catch (error) {
233
+ return { message: `Session continuity did not activate Poteto Mode automatically: ${error.message}` };
234
+ }
235
+ }
236
+
237
+ function cursorHook(payload) {
238
+ const event = payload.hook_event_name;
239
+ if (!cursorEvents.has(event) || payload.agent_id) return {};
240
+ const session = requireText(payload.conversation_id || payload.session_id, 'host session id');
241
+ const path = recordPath('cursor', session);
242
+ const previous = readRecord(path, 'cursor', session);
243
+ if (event === 'sessionStart') {
244
+ const opening = sessionOpening('cursor', session, path, previous, payload, false);
245
+ return { ...(opening.context ? { additional_context: opening.context } : {}), ...(opening.message ? { user_message: opening.message } : {}) };
246
+ }
247
+ if (event === 'preCompact') {
248
+ if (previous?.status !== 'active') return {};
249
+ transact('cursor', session, record => ({ ...record, reload_pending: true }));
250
+ return { user_message: 'Poteto Mode and the saved session record reload on the next tool result after this compaction.' };
251
+ }
252
+ if (event === 'postToolUse') {
253
+ if (previous?.status !== 'active' || !previous.reload_pending) return {};
254
+ const saved = transact('cursor', session, record => ({ ...record, reload_pending: false }));
255
+ return { additional_context: render(saved.record, saved.path, true) };
256
+ }
257
+ if (deactivationRequest(payload.prompt)) {
258
+ if (previous?.status !== 'active') return { continue: true };
259
+ const saved = transact('cursor', session, record => ({ ...record, status: 'inactive', reload_pending: false, pending: [] }));
260
+ return { continue: true, user_message: `Session continuity deactivated at ${saved.path}. Poteto Mode is unchanged.` };
261
+ }
262
+ const trigger = invocation(payload.prompt);
263
+ if (!trigger && previous?.status !== 'active') return { continue: true };
264
+ const saved = transact('cursor', session, record => {
265
+ if (trigger) return { ...activate(record, 'cursor', session, payload, trigger), reload_pending: true };
266
+ if (record?.status !== 'active') throw new Error('Record deactivated during prompt processing; retry');
267
+ const evidence = promptEvidence(payload);
268
+ if (!record.pending.some(requirement => requirement.id === evidence.id)) record.pending.push(evidence);
269
+ return record;
270
+ });
271
+ return { continue: true, user_message: `Session continuity recorded this message at ${saved.path}. Revision ${saved.record.revision}.` };
272
+ }
273
+
203
274
  function hook(host, payload) {
204
- if (host === 'cursor') throw new Error('Cursor automatic activation is unsupported. beforeSubmitPrompt cannot inject agent context; sessionStart is fire-and-forget and preCompact is observational. No automatic hooks installed.');
275
+ if (host === 'cursor') return cursorHook(payload);
205
276
  const event = payload.hook_event_name;
206
277
  if (payload.agent_id || (payload.role && payload.role !== 'user')) return {};
207
278
  const accepted = ['SessionStart', 'UserPromptSubmit'];
@@ -211,16 +282,13 @@ function hook(host, payload) {
211
282
  const path = recordPath(host, session);
212
283
  const previous = readRecord(path, host, session);
213
284
  if (event === 'SessionStart') {
214
- if (payload.source === 'clear' && previous?.status === 'active') {
215
- transact(host, session, record => ({ ...record, status: 'inactive', pending: [] }));
216
- return { systemMessage: `Session continuity ended by clear: ${path}` };
217
- }
218
285
  const sources = host === 'claude' ? ['startup', 'resume', 'compact', 'clear', 'fork'] : ['startup', 'resume', 'compact', 'clear'];
219
286
  if (!sources.includes(payload.source)) return {};
220
- if (!previous || previous.status !== 'active') return {};
221
- return { hookSpecificOutput: { hookEventName: event, additionalContext: render(previous, path, true) } };
287
+ const opening = sessionOpening(host, session, path, previous, payload, payload.source === 'clear');
288
+ return { ...(opening.message ? { systemMessage: opening.message } : {}),
289
+ ...(opening.context ? { hookSpecificOutput: { hookEventName: event, additionalContext: opening.context } } : {}) };
222
290
  }
223
- if (/^(?:[/$](?:claude-dev-env:)?session-continuity off|deactivate session continuity)[.!]?\s*$/i.test(payload.prompt || '')) {
291
+ if (deactivationRequest(payload.prompt)) {
224
292
  if (!previous || previous.status !== 'active') return {};
225
293
  const saved = transact(host, session, record => ({ ...record, status: 'inactive', pending: [] }));
226
294
  return { systemMessage: `Session continuity deactivated at ${saved.path}. Poteto Mode is unchanged.`,
@@ -364,12 +432,16 @@ try {
364
432
  } catch (error) {
365
433
  if (command === 'hook') {
366
434
  const message = `Session continuity failed: ${error.message}`;
367
- console.log(encode({
368
- ...(['UserPromptSubmit', 'UserPromptExpansion'].includes(hookEvent) ? { decision: 'block', reason: message } : {}),
369
- systemMessage: message,
370
- ...(['SessionStart', 'UserPromptSubmit', 'UserPromptExpansion'].includes(hookEvent)
371
- ? { hookSpecificOutput: { hookEventName: hookEvent, additionalContext: `${message}. Stop dependent work until recovery succeeds.` } } : {}),
372
- }));
435
+ const guidance = `${message}. Stop dependent work until recovery succeeds.`;
436
+ console.log(encode(cursorEvents.has(hookEvent)
437
+ ? { ...(hookEvent === 'beforeSubmitPrompt' ? { continue: true } : {}), user_message: message,
438
+ ...(['sessionStart', 'postToolUse'].includes(hookEvent) ? { additional_context: guidance } : {}) }
439
+ : {
440
+ ...(['UserPromptSubmit', 'UserPromptExpansion'].includes(hookEvent) ? { decision: 'block', reason: message } : {}),
441
+ systemMessage: message,
442
+ ...(['SessionStart', 'UserPromptSubmit', 'UserPromptExpansion'].includes(hookEvent)
443
+ ? { hookSpecificOutput: { hookEventName: hookEvent, additionalContext: guidance } } : {}),
444
+ }));
373
445
  } else console.error(error.message);
374
446
  process.exitCode = command === 'hook' ? 0 : 1;
375
447
  }
@@ -26,7 +26,7 @@ function fixture(t, host) {
26
26
  writeFileSync(second, secondText);
27
27
  const script = join(skill, 'continuity.mjs');
28
28
  const configPath = join(root, host === 'claude' ? 'settings.json' : 'hooks.json');
29
- writeFileSync(configPath, JSON.stringify({ hooks: continuityHookConfiguration(host, script) }));
29
+ writeFileSync(configPath, JSON.stringify({ ...(host === 'cursor' ? { version: 1 } : {}), hooks: continuityHookConfiguration(host, script) }));
30
30
  const env = { ...process.env };
31
31
  delete env.CDE_CONTINUITY_ROOT;
32
32
  delete env.CDE_POTETO_SOURCE;
@@ -37,11 +37,11 @@ function fixture(t, host) {
37
37
  return expectedStatus ? result.stderr : JSON.parse(result.stdout);
38
38
  };
39
39
  const fire = (event, session, fields = {}) => {
40
- const payload = { hook_event_name: event, session_id: session, cwd: root, ...fields };
40
+ const payload = { hook_event_name: event, cwd: root, ...(host === 'cursor' ? { conversation_id: session } : { session_id: session }), ...fields };
41
41
  const config = JSON.parse(readFileSync(configPath, 'utf8'));
42
42
  const groups = config.hooks[event] || [];
43
43
  const matching = groups.filter(group => !group.matcher || new RegExp(group.matcher).test(event === 'SessionStart' ? fields.source : fields.command_name));
44
- return matching.flatMap(group => group.hooks.map(handler => {
44
+ return matching.flatMap(group => (group.hooks || [group]).map(handler => {
45
45
  const result = spawnSync(handler.command, { shell: true, input: JSON.stringify(payload), encoding: 'utf8', env });
46
46
  assert.equal(result.status, 0, result.stderr);
47
47
  return JSON.parse(result.stdout);
@@ -55,7 +55,6 @@ function fixture(t, host) {
55
55
  for (const host of ['claude', 'codex']) {
56
56
  test(`${host}: configured invocation emits complete instructions, writes and reads back, restores without a path`, t => {
57
57
  const f = fixture(t, host);
58
- assert.deepEqual(f.fire('SessionStart', 'session-a', { source: 'startup' }), [{}]);
59
58
  assert.equal(existsSync(f.pathFor('session-a')), false);
60
59
  const prefix = host === 'codex' ? '$pstack:poteto-mode' : '/pstack:poteto-mode';
61
60
  const prompt = `${prefix} for this entire session\nFinish the sample task. Keep changes local. Use second-skill for this task.`;
@@ -86,7 +85,9 @@ for (const host of ['claude', 'codex']) {
86
85
  assert.equal(readFileSync(f.poteto, 'utf8'), potetoText);
87
86
  record = f.read('session-a');
88
87
  assert.equal(record.pending.length, 0);
89
- assert.deepEqual(f.fire('SessionStart', 'unrelated-session', { source: 'startup' }), [{}]);
88
+ const unrelated = f.fire('SessionStart', 'unrelated-session', { source: 'startup' })[0].hookSpecificOutput.additionalContext;
89
+ assert.equal(unrelated.includes('build sample'), false);
90
+ assert.equal(unrelated.includes(secondText), false);
90
91
  });
91
92
 
92
93
  test(`${host}: quoting, discussion, tool output and subagent input do not activate`, t => {
@@ -247,12 +248,81 @@ test('a busy record blocks prompt processing, preserves state, and supports a sa
247
248
  assert.equal(f.read('busy').pending.length, 2);
248
249
  });
249
250
 
250
- test('clear deactivates a reused host id rather than inheriting an unrelated task', t => {
251
+ test('clear drops a reused host id rather than inheriting an unrelated task', t => {
251
252
  const f = fixture(t, 'claude');
252
- f.submit('clear', '/poteto-mode for this entire session');
253
+ f.submit('clear', '/poteto-mode for this entire session\nKeep the sample local.');
254
+ const before = f.read('clear');
253
255
  f.fire('SessionStart', 'clear', { source: 'clear' });
254
- assert.equal(f.read('clear').status, 'inactive');
255
- assert.deepEqual(f.fire('SessionStart', 'clear', { source: 'startup' }), [{}]);
256
+ const record = f.read('clear');
257
+ assert.equal(record.status, 'active');
258
+ assert.notEqual(record.task.id, before.task.id);
259
+ assert.deepEqual(record.pending, []);
260
+ assert.deepEqual(record.requirements.map(entry => entry.id), ['skill:pstack:poteto-mode']);
261
+ assert.equal(record.requirements[0].automatic, true);
262
+ });
263
+
264
+ for (const host of ['claude', 'codex']) {
265
+ test(`${host}: session start and compaction activate Poteto Mode with no explicit invocation`, t => {
266
+ const f = fixture(t, host);
267
+ const started = f.fire('SessionStart', 'auto', { source: 'startup' })[0];
268
+ assert.ok(started.systemMessage.includes(f.pathFor('auto')));
269
+ assert.ok(started.hookSpecificOutput.additionalContext.includes(potetoText));
270
+ const record = f.read('auto');
271
+ assert.equal(record.requirements.length, 1);
272
+ assert.equal(record.requirements[0].scope, 'session');
273
+ assert.equal(record.requirements[0].automatic, true);
274
+ assert.equal(record.requirements[0].source, f.poteto);
275
+ assert.deepEqual(record.pending, []);
276
+ for (const source of ['compact', 'resume']) {
277
+ const later = f.fire('SessionStart', 'auto', { source })[0].hookSpecificOutput.additionalContext;
278
+ assert.ok(later.includes(potetoText));
279
+ }
280
+ assert.equal(f.read('auto').revision, record.revision);
281
+ f.submit('auto', '/session-continuity off');
282
+ assert.deepEqual(f.fire('SessionStart', 'auto', { source: 'compact' }), [{}]);
283
+ assert.deepEqual(f.fire('SessionStart', 'auto', { source: 'startup' }), [{}]);
284
+ });
285
+
286
+ test(`${host}: an unavailable Poteto source stops automatic activation without writing a record`, t => {
287
+ const f = fixture(t, host);
288
+ writeFileSync(f.poteto, '---\nname: Something Else\n---\n');
289
+ const started = f.fire('SessionStart', 'broken', { source: 'startup' })[0];
290
+ assert.match(started.systemMessage, /did not activate Poteto Mode/);
291
+ assert.equal(existsSync(f.pathFor('broken')), false);
292
+ });
293
+ }
294
+
295
+ test('cursor: session start injects Poteto Mode and compaction reloads it on the next tool result', t => {
296
+ const f = fixture(t, 'cursor');
297
+ const started = f.fire('sessionStart', 'cursor-a', { session_id: 'composer-1', composer_mode: 'agent' })[0];
298
+ assert.ok(started.additional_context.includes(potetoText));
299
+ assert.equal(f.read('cursor-a').requirements[0].automatic, true);
300
+ assert.deepEqual(f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' }), [{}]);
301
+ const compacting = f.fire('preCompact', 'cursor-a', { trigger: 'auto' })[0];
302
+ assert.ok(compacting.user_message.includes('Poteto Mode'));
303
+ assert.equal(f.read('cursor-a').reload_pending, true);
304
+ const reloaded = f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' })[0];
305
+ assert.ok(reloaded.additional_context.includes(potetoText));
306
+ assert.ok(reloaded.additional_context.includes(f.pathFor('cursor-a')));
307
+ assert.equal(f.read('cursor-a').reload_pending, false);
308
+ assert.deepEqual(f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' }), [{}]);
309
+ });
310
+
311
+ test('cursor: a prompt records evidence, never blocks submission, and reloads named skills after a tool result', t => {
312
+ const f = fixture(t, 'cursor');
313
+ f.fire('sessionStart', 'cursor-b', { session_id: 'composer-2' });
314
+ const submitted = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: 'Finish the sample task.', generation_id: 'gen-1' })[0];
315
+ assert.equal(submitted.continue, true);
316
+ assert.equal(f.read('cursor-b').pending.length, 1);
317
+ const invoked = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: '/poteto-mode for this entire session', generation_id: 'gen-2' })[0];
318
+ assert.equal(invoked.continue, true);
319
+ assert.equal(f.read('cursor-b').reload_pending, true);
320
+ assert.ok(f.fire('postToolUse', 'cursor-b', { tool_name: 'Read' })[0].additional_context.includes(potetoText));
321
+ const off = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: '/session-continuity off', generation_id: 'gen-3' })[0];
322
+ assert.equal(off.continue, true);
323
+ assert.equal(f.read('cursor-b').status, 'inactive');
324
+ assert.deepEqual(f.fire('sessionStart', 'cursor-b', { session_id: 'composer-2' }), [{}]);
325
+ assert.deepEqual(f.fire('preCompact', 'cursor-b', { trigger: 'manual' }), [{}]);
256
326
  });
257
327
 
258
328
  test('missing host identity and corrupt records produce visible recovery failures', t => {
@@ -2,13 +2,15 @@ import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } fro
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
+ const supportedHosts = ['claude', 'codex', 'cursor'];
6
+
5
7
  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}`);
8
+ if (!supportedHosts.includes(host)) throw new Error(`Unsupported host ${host}`);
10
9
  if (/[\r\n"`$%]/.test(script)) throw new Error('Hook script path contains shell expansion characters');
11
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
+ }
12
14
  const events = host === 'claude' ? ['UserPromptSubmit', 'UserPromptExpansion', 'SessionStart'] : ['UserPromptSubmit', 'SessionStart'];
13
15
  return Object.fromEntries(events.map(event => [event, [{
14
16
  ...(event === 'SessionStart' ? { matcher: host === 'claude' ? 'startup|resume|compact|clear|fork' : 'startup|resume|compact|clear' } : {}),
@@ -20,19 +22,22 @@ export function continuityHookConfiguration(host, script) {
20
22
  export function mergeContinuityHooks(existing, host, script) {
21
23
  const additions = continuityHookConfiguration(host, script);
22
24
  const mergedConfiguration = structuredClone(existing);
25
+ if (host === 'cursor') mergedConfiguration.version = 1;
23
26
  mergedConfiguration.hooks ||= {};
24
27
  for (const [event, groups] of Object.entries(additions)) {
25
28
  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
+ 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) {
29
33
  throw new Error('Another continuity installation owns this host config. Select its profile or remove that installation explicitly.');
30
34
  }
31
35
  }
32
36
  }
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 }] : [];
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 }] : [];
36
41
  });
37
42
  mergedConfiguration.hooks[event] = kept.concat(groups);
38
43
  }
@@ -41,15 +46,20 @@ export function mergeContinuityHooks(existing, host, script) {
41
46
 
42
47
  async function main() {
43
48
  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)), '');
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}`);
46
52
  const { resolveInstallRoot } = await import('./resolve-install-root.mjs');
47
53
  const roots = resolveInstallRoot();
48
54
  const script = join(roots.skillsInstallDirectory, 'session-continuity', 'continuity.mjs');
49
55
  if (!existsSync(script) || !existsSync(join(dirname(script), 'SKILL.md'))) {
50
56
  throw new Error('Run the full claude-dev-env installer from this checkout first so the companion is in the canonical agents home.');
51
57
  }
52
- const paths = { claude: join(roots.managedRoot, 'settings.json'), codex: join(dirname(roots.codexRulesInstallDirectory), 'hooks.json') };
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
+ };
53
63
  const plans = selected.map(host => {
54
64
  const path = paths[host];
55
65
  if (!existsSync(dirname(path))) throw new Error(`Host config directory is absent: ${dirname(path)}`);
@@ -70,7 +80,7 @@ async function main() {
70
80
  if (readFileSync(plan.path, 'utf8') !== plan.content) throw new Error(`Config read-back mismatch: ${plan.path}`);
71
81
  console.log(`Configured and read back: ${plan.path}`);
72
82
  }
73
- console.log('Review and trust the new hooks in each host. Cursor remains unsupported; no Cursor settings changed.');
83
+ console.log('Review and trust the new hooks in each host.');
74
84
  }
75
85
 
76
86
  if (process.argv[1] && resolve(realpathSync(process.argv[1])) === fileURLToPath(import.meta.url)) {
@@ -14,11 +14,24 @@ test('setup preserves other hooks and settings, is idempotent, and rejects compe
14
14
  assert.throws(() => continuityHookConfiguration('claude', '/path/with$expansion/continuity.mjs'), /expansion/);
15
15
  });
16
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);
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
+ }
22
35
  });
23
36
 
24
37
  for (const host of ['claude', 'codex']) {
@@ -1353,6 +1353,23 @@ test('mergeHooksIntoSettings is idempotent when run twice against an already-upd
1353
1353
  });
1354
1354
 
1355
1355
 
1356
+ test('shipped SessionEnd hook timeouts stay within the Codex runtime limit', () => {
1357
+ const shippedHooksConfig = JSON.parse(
1358
+ readFileSync(new URL('../hooks/hooks.json', import.meta.url), 'utf8')
1359
+ );
1360
+ const allSessionEndHooks = (shippedHooksConfig.hooks.SessionEnd || [])
1361
+ .flatMap(eachMatcherGroup => eachMatcherGroup.hooks);
1362
+
1363
+ assert.ok(allSessionEndHooks.length > 0, 'shipped hooks.json must register SessionEnd hooks');
1364
+ for (const eachSessionEndHook of allSessionEndHooks) {
1365
+ assert.ok(
1366
+ eachSessionEndHook.timeout <= 3,
1367
+ `${eachSessionEndHook.command} exceeds the Codex SessionEnd runtime limit`,
1368
+ );
1369
+ }
1370
+ });
1371
+
1372
+
1356
1373
  test('shipped hooks.json keeps only nonblocking policy handlers and lifecycle hooks', () => {
1357
1374
  const shippedHooksConfig = JSON.parse(
1358
1375
  readFileSync(new URL('../hooks/hooks.json', import.meta.url), 'utf8')
@@ -0,0 +1 @@
1
+ """Retired search hook entrypoint kept inert for stale command references."""
@@ -0,0 +1,46 @@
1
+ """Regression tests for the inert retired search hook entrypoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
12
+ _ENTRYPOINT_PATH = _BLOCKING_DIRECTORY / "content_search_to_zoekt_redirector.py"
13
+ _HOOKS_MANIFEST_PATH = _BLOCKING_DIRECTORY.parent / "hooks.json"
14
+
15
+
16
+ def test_retired_entrypoint_allows_valid_search_events_silently() -> None:
17
+ all_search_events = (
18
+ {"tool_name": "Bash", "tool_input": {"command": "rg --files packages"}},
19
+ {
20
+ "tool_name": "Grep",
21
+ "tool_input": {"pattern": "zoekt", "path": "packages"},
22
+ },
23
+ {
24
+ "tool_name": "Search",
25
+ "tool_input": {"pattern": "zoekt", "path": "packages"},
26
+ },
27
+ )
28
+
29
+ for each_search_event in all_search_events:
30
+ completed_process = subprocess.run(
31
+ [sys.executable, str(_ENTRYPOINT_PATH)],
32
+ check=False,
33
+ input=json.dumps(each_search_event),
34
+ capture_output=True,
35
+ text=True,
36
+ encoding="utf-8",
37
+ )
38
+ assert completed_process.returncode == 0
39
+ assert completed_process.stdout == ""
40
+ assert completed_process.stderr == ""
41
+
42
+
43
+ def test_shipped_hook_manifest_omits_retired_entrypoint() -> None:
44
+ hooks_manifest_text = _HOOKS_MANIFEST_PATH.read_text(encoding="utf-8")
45
+
46
+ assert _ENTRYPOINT_PATH.name not in hooks_manifest_text
package/hooks/hooks.json CHANGED
@@ -84,12 +84,12 @@
84
84
  {
85
85
  "type": "command",
86
86
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/session_end_cleanup.py",
87
- "timeout": 15
87
+ "timeout": 3
88
88
  },
89
89
  {
90
90
  "type": "command",
91
91
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/session_edit_tracker_cleanup.py",
92
- "timeout": 10
92
+ "timeout": 3
93
93
  }
94
94
  ]
95
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "7.1.0",
3
+ "version": "7.2.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@ Three tools are equally sanctioned; pick by what you know:
19
19
  | A name or path pattern | The harness `Glob` tool |
20
20
  | Text inside files | The harness `Grep` tool |
21
21
 
22
- When `es.exe` fails or returns nothing, fall back to `Glob` or `Grep` without pausing, and report the outage so the reader knows the index was unavailable. Ask the user only after all three tools fail.
22
+ When `es.exe` returns Error 8, retry the same search twice before any fallback. Error 8 is a missing IPC client window, not proof the index is down. After those retries fail, report that the Everything IPC client did not answer and that the service state was not probed, then fall back to `Glob` or `Grep` with the same scope. When `es.exe` is missing, or a later search returns no hits, fall back to `Glob` or `Grep`. Ask the user only after all three tools fail.
23
23
 
24
24
  `skills/everything-search/SKILL.md` holds the full `es.exe` operator reference: `ext:`, `dm:`, `size:`, wildcards, OR/AND/NOT, output flags, and the junction and drive-mapping note.
25
25