claude-dev-env 8.87.1 → 8.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/bin/cursor-agents-guidance.mjs +94 -0
- package/bin/cursor-agents-guidance.test.mjs +94 -0
- package/bin/cursor-native-hooks.mjs +128 -0
- package/bin/cursor-native-hooks.test.mjs +78 -0
- package/bin/install-constants.mjs +29 -0
- package/bin/install-deepstack-plugin.mjs +84 -39
- package/bin/install-deepstack-plugin.test.mjs +51 -11
- package/bin/install.cursor-rules.test.mjs +65 -2
- package/bin/install.mjs +35 -0
- package/bin/install.test.mjs +10 -5
- package/docs/agents-guidance/asks.md +49 -0
- package/hooks/blocking/open_tasks_stop_gate.py +210 -0
- package/hooks/blocking/open_tasks_transcript.py +270 -0
- package/hooks/blocking/test_open_tasks_stop_gate.py +297 -0
- package/hooks/blocking/test_open_tasks_transcript.py +54 -0
- package/hooks/features/conduct-gates.md +3 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/cursor_agents_guidance_constants.py +6 -0
- package/hooks/hooks_constants/open_tasks_stop_gate_constants.py +72 -0
- package/hooks/session/cursor_agents_guidance.py +54 -0
- package/hooks/session/test_cursor_agents_guidance.py +24 -0
- package/package.json +1 -1
- package/scripts/policy_lint/adapter_configuration.py +6 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/tests/test_adapter_configuration.py +8 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Constants for the open tasks Stop gate."""
|
|
2
|
+
|
|
3
|
+
HOOK_EVENT_NAME = "Stop"
|
|
4
|
+
SESSION_ID_KEY = "session_id"
|
|
5
|
+
TRANSCRIPT_PATH_KEY = "transcript_path"
|
|
6
|
+
LAST_ASSISTANT_MESSAGE_KEY = "last_assistant_message"
|
|
7
|
+
DECISION_KEY = "decision"
|
|
8
|
+
BLOCK_DECISION = "block"
|
|
9
|
+
REASON_KEY = "reason"
|
|
10
|
+
ALLOW_EXIT_CODE = 0
|
|
11
|
+
UTF8_ENCODING = "utf-8"
|
|
12
|
+
WORD_SEPARATOR = " "
|
|
13
|
+
TASK_LIST_ID_ENVIRONMENT_VARIABLE = "CLAUDE_CODE_TASK_LIST_ID"
|
|
14
|
+
CLAUDE_CONFIG_DIRECTORY_ENVIRONMENT_VARIABLE = "CLAUDE_CONFIG_DIR"
|
|
15
|
+
CLAUDE_HOME_DIRECTORY_NAME = ".claude"
|
|
16
|
+
TASKS_DIRECTORY_NAME = "tasks"
|
|
17
|
+
TASK_FILE_GLOB = "*.json"
|
|
18
|
+
TASK_ID_KEY = "id"
|
|
19
|
+
TASK_SUBJECT_KEY = "subject"
|
|
20
|
+
TASK_STATUS_KEY = "status"
|
|
21
|
+
PENDING_STATUS = "pending"
|
|
22
|
+
INCOMPLETE_STATUSES = frozenset({PENDING_STATUS, "in_progress"})
|
|
23
|
+
RECORD_TYPE_KEY = "type"
|
|
24
|
+
MESSAGE_KEY = "message"
|
|
25
|
+
CONTENT_KEY = "content"
|
|
26
|
+
TEXT_KEY = "text"
|
|
27
|
+
TOOL_NAME_KEY = "name"
|
|
28
|
+
TOOL_INPUT_KEY = "input"
|
|
29
|
+
TOOL_USE_ID_KEY = "tool_use_id"
|
|
30
|
+
TODOS_KEY = "todos"
|
|
31
|
+
TODO_CONTENT_KEY = "content"
|
|
32
|
+
TODO_ID_KEY = "id"
|
|
33
|
+
TODO_MERGE_KEY = "merge"
|
|
34
|
+
ROLE_KEY = "role"
|
|
35
|
+
ASSISTANT_ROLE = "assistant"
|
|
36
|
+
CURSOR_CONVERSATION_ID_KEY = "conversation_id"
|
|
37
|
+
CURSOR_FOLLOWUP_MESSAGE_KEY = "followup_message"
|
|
38
|
+
CURSOR_STATUS_KEY = "status"
|
|
39
|
+
CURSOR_COMPLETED_STATUS = "completed"
|
|
40
|
+
CURSOR_LOOP_COUNT_KEY = "loop_count"
|
|
41
|
+
CURSOR_FOLLOWUP_LIMIT = 5
|
|
42
|
+
REPLY_TEXT_SEPARATOR = "\n"
|
|
43
|
+
TASK_UPDATE_ID_KEY = "taskId"
|
|
44
|
+
TODO_WRITE_TOOL_NAME = "TodoWrite"
|
|
45
|
+
DYNAMIC_TOOL_CALL_NAME = "CallDynamicTool"
|
|
46
|
+
DYNAMIC_TOOL_NAME_KEY = "toolName"
|
|
47
|
+
DYNAMIC_TOOL_ARGUMENTS_KEY = "arguments"
|
|
48
|
+
TASK_CREATE_TOOL_NAME = "TaskCreate"
|
|
49
|
+
TASK_UPDATE_TOOL_NAME = "TaskUpdate"
|
|
50
|
+
TOOL_USE_BLOCK_TYPE = "tool_use"
|
|
51
|
+
TOOL_RESULT_BLOCK_TYPE = "tool_result"
|
|
52
|
+
CODEX_RESPONSE_ITEM_TYPE = "response_item"
|
|
53
|
+
CODEX_FUNCTION_CALL_TYPE = "function_call"
|
|
54
|
+
CODEX_PAYLOAD_KEY = "payload"
|
|
55
|
+
CODEX_ARGUMENTS_KEY = "arguments"
|
|
56
|
+
CODEX_PLAN_TOOL_NAME = "update_plan"
|
|
57
|
+
CODEX_PLAN_KEY = "plan"
|
|
58
|
+
CODEX_STEP_KEY = "step"
|
|
59
|
+
FIRST_ITEM_NUMBER = 1
|
|
60
|
+
TASK_CREATED_PATTERN = r"Task #(\d+) created"
|
|
61
|
+
CREATED_TASK_ID_GROUP_INDEX = 1
|
|
62
|
+
REPORT_LINE_PATTERN = r"^\s*(?:[-*]\s*)?(open|blocked)\b\s*[:\-]?\s*(.*)$"
|
|
63
|
+
REPORTED_TEXT_GROUP_INDEX = 2
|
|
64
|
+
WORD_BOUNDARY_PATTERN = r"\b"
|
|
65
|
+
TASK_NUMBER_REFERENCE_TEMPLATE = "#{task_id}"
|
|
66
|
+
ITEM_LINE_TEMPLATE = "\n- {subject}"
|
|
67
|
+
BLOCK_REASON_PREFIX = "The task list still has incomplete items that this reply does not report:"
|
|
68
|
+
BLOCK_REASON_SUFFIX = (
|
|
69
|
+
"\nKeep working each one now. End the reply with one line per item you cannot finish this turn:"
|
|
70
|
+
" 'Blocked: <item> - <what the user must give>' when only the user can unblock it, or"
|
|
71
|
+
" 'Open: <item> - <the step you take next>' when you are still working it."
|
|
72
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cursor sessionStart hook that puts the installed agent guidance into every session.
|
|
3
|
+
|
|
4
|
+
Cursor keeps user rules in its app settings, and it reads ``~/.cursor/rules``
|
|
5
|
+
only for a workspace inside the home directory, so no file under ``~/.cursor``
|
|
6
|
+
carries global guidance. The installer registers this hook as a native
|
|
7
|
+
``sessionStart`` entry in ``~/.cursor/hooks.json`` with the installed
|
|
8
|
+
``AGENTS.md`` path as its one argument. The hook prints that file as Cursor's
|
|
9
|
+
``additional_context``, which Cursor adds to the session's context::
|
|
10
|
+
|
|
11
|
+
python3 cursor_agents_guidance.py ~/.agents/AGENTS.md
|
|
12
|
+
-> {"additional_context": "# Agent guidance ..."}
|
|
13
|
+
|
|
14
|
+
A missing or unreadable file prints ``{}`` so the session starts without it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
24
|
+
if hooks_root_directory not in sys.path:
|
|
25
|
+
sys.path.insert(0, hooks_root_directory)
|
|
26
|
+
|
|
27
|
+
from hooks_constants.cursor_agents_guidance_constants import (
|
|
28
|
+
CURSOR_CONTEXT_KEY,
|
|
29
|
+
EMPTY_OUTPUT,
|
|
30
|
+
GUIDANCE_PATH_ARGUMENT_INDEX,
|
|
31
|
+
UTF8_ENCODING,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def guidance_output(all_arguments: list[str]) -> str:
|
|
36
|
+
"""Return the SessionStart output for the guidance file named in the arguments."""
|
|
37
|
+
if len(all_arguments) <= GUIDANCE_PATH_ARGUMENT_INDEX:
|
|
38
|
+
return EMPTY_OUTPUT
|
|
39
|
+
try:
|
|
40
|
+
guidance_text = Path(all_arguments[GUIDANCE_PATH_ARGUMENT_INDEX]).read_text(encoding=UTF8_ENCODING)
|
|
41
|
+
except OSError:
|
|
42
|
+
return EMPTY_OUTPUT
|
|
43
|
+
if not guidance_text.strip():
|
|
44
|
+
return EMPTY_OUTPUT
|
|
45
|
+
return json.dumps({CURSOR_CONTEXT_KEY: guidance_text})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> None:
|
|
49
|
+
sys.stdin.read()
|
|
50
|
+
sys.stdout.write(guidance_output(sys.argv))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
SESSION_DIRECTORY = Path(__file__).resolve().parent
|
|
6
|
+
if str(SESSION_DIRECTORY) not in sys.path:
|
|
7
|
+
sys.path.insert(0, str(SESSION_DIRECTORY))
|
|
8
|
+
|
|
9
|
+
import cursor_agents_guidance
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_should_emit_the_guidance_file_as_cursor_additional_context(tmp_path: Path) -> None:
|
|
13
|
+
guidance_path = tmp_path / "AGENTS.md"
|
|
14
|
+
guidance_path.write_text("# Agent guidance\n- **Asks.** Work every ask now.\n", encoding="utf-8")
|
|
15
|
+
printed_output = cursor_agents_guidance.guidance_output(["hook", str(guidance_path)])
|
|
16
|
+
assert json.loads(printed_output) == {"additional_context": guidance_path.read_text(encoding="utf-8")}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_should_emit_an_empty_object_when_the_file_is_missing(tmp_path: Path) -> None:
|
|
20
|
+
assert cursor_agents_guidance.guidance_output(["hook", str(tmp_path / "absent.md")]) == "{}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_should_emit_an_empty_object_without_a_path_argument() -> None:
|
|
24
|
+
assert cursor_agents_guidance.guidance_output(["hook"]) == "{}"
|
package/package.json
CHANGED
|
@@ -139,6 +139,7 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
139
139
|
hooks/blocking/build_eval_before_eval_write_gate.py -> exempt
|
|
140
140
|
hooks/blocking/merge_over_review_offer_gate.py -> exempt
|
|
141
141
|
hooks/blocking/final_reply_gate.py -> exempt
|
|
142
|
+
hooks/blocking/open_tasks_stop_gate.py -> exempt
|
|
142
143
|
hooks/blocking/some_new_blocker.py -> flagged
|
|
143
144
|
|
|
144
145
|
The Bash PreToolUse dispatcher sits under ``blocking/`` for layout reasons
|
|
@@ -179,6 +180,11 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
179
180
|
names open review findings. It reads only the wording of a chat card and
|
|
180
181
|
decides no code or safety policy; the merge review gate holds the merge.
|
|
181
182
|
|
|
183
|
+
The open tasks Stop gate holds the end of a turn while the session's task
|
|
184
|
+
list has items the closing reply neither completes nor reports as
|
|
185
|
+
``Blocked:`` or ``Open:``. It decides when a turn may end and no code or
|
|
186
|
+
safety policy.
|
|
187
|
+
|
|
182
188
|
Args:
|
|
183
189
|
registered_string: One command, path, script, or entrypoint string.
|
|
184
190
|
"""
|
|
@@ -78,6 +78,7 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
|
78
78
|
"blocking/build_eval_before_eval_write_gate.py",
|
|
79
79
|
"blocking/merge_over_review_offer_gate.py",
|
|
80
80
|
"blocking/final_reply_gate.py",
|
|
81
|
+
"blocking/open_tasks_stop_gate.py",
|
|
81
82
|
)
|
|
82
83
|
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
83
84
|
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
@@ -74,6 +74,14 @@ def test_new_final_reply_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
|
74
74
|
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def test_new_open_tasks_stop_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
78
|
+
current_document = _hook_document(
|
|
79
|
+
["hooks/blocking/open_tasks_stop_gate.py"],
|
|
80
|
+
'{"hooks": {}}',
|
|
81
|
+
)
|
|
82
|
+
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
83
|
+
|
|
84
|
+
|
|
77
85
|
def test_new_step_note_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
78
86
|
current_document = _hook_document(
|
|
79
87
|
["hooks/blocking/step_note_gate.py"],
|