claude-dev-env 8.22.0 → 8.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/eli5/SKILL.md +7 -3
- package/.agents/skills/eli5/reference/task-seeds.md +5 -4
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/advisor_rules_prompt_constants.py +43 -0
- package/hooks/session/advisor_rules_prompt.py +76 -0
- package/hooks/session/test_advisor_rules_prompt.py +86 -0
- package/package.json +1 -1
|
@@ -90,11 +90,15 @@ Process steps in order.
|
|
|
90
90
|
artifact, preserve an explicit user path, or create a clear self-contained
|
|
91
91
|
browser-ready HTML artifact when no path exists. Keep the chosen path for the
|
|
92
92
|
rest of the active task or conversation.
|
|
93
|
-
2. **
|
|
93
|
+
2. **Deterministic — list the cases the user named.** When the request lists
|
|
94
|
+
cases, count each variant as its own case and keep the user's numbering,
|
|
95
|
+
such as 3a and 3b for a case that holds a draft and a ready variant. Give
|
|
96
|
+
each case its own card and its own visual.
|
|
97
|
+
3. **Borderline — add the current explanation to that artifact.** Read and apply
|
|
94
98
|
`~/.claude/rules/asd-ste100-language.md` to every sentence. Frame the topic
|
|
95
99
|
for a beginner, use a large useful visual, keep the text minimal, and update
|
|
96
100
|
the same artifact in place.
|
|
97
|
-
|
|
101
|
+
4. **Deterministic — run the digest check on a summary of many changes.** A
|
|
98
102
|
summary of a set of changes, such as the pull requests merged in a day, is a
|
|
99
103
|
digest. Give each change one card marked `data-digest-card="<id>"` that holds
|
|
100
104
|
its picture and its one-line change. Pass every source id to the check:
|
|
@@ -106,7 +110,7 @@ Process steps in order.
|
|
|
106
110
|
The check reports each source id with no card, each card over 25 words, each
|
|
107
111
|
card without a picture, and more than 40 words outside the cards. Fix every
|
|
108
112
|
finding and run it again until it prints `CLEAN`.
|
|
109
|
-
|
|
113
|
+
5. **Judgment — share the updated artifact with the user.**
|
|
110
114
|
|
|
111
115
|
### Examples
|
|
112
116
|
|
|
@@ -5,8 +5,9 @@ the start of the ELI5 process.
|
|
|
5
5
|
|
|
6
6
|
1. Resolve the active HTML artifact path and preserve the stable path.
|
|
7
7
|
2. Create the artifact when no active path exists and keep it self-contained.
|
|
8
|
-
3.
|
|
9
|
-
4.
|
|
10
|
-
5.
|
|
8
|
+
3. List each case the user named, with each variant as its own case.
|
|
9
|
+
4. Apply the ASD-STE100 rule to page and response sentences.
|
|
10
|
+
5. Add the concise beginner explanation and useful large visual in place.
|
|
11
|
+
6. Run `digest_check.py` on a digest with every source id until it prints
|
|
11
12
|
`CLEAN`.
|
|
12
|
-
|
|
13
|
+
7. Share the updated artifact with the user.
|
package/hooks/hooks.json
CHANGED
|
@@ -129,6 +129,11 @@
|
|
|
129
129
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/working_style_prompt.py",
|
|
130
130
|
"timeout": 10
|
|
131
131
|
},
|
|
132
|
+
{
|
|
133
|
+
"type": "command",
|
|
134
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/advisor_rules_prompt.py",
|
|
135
|
+
"timeout": 10
|
|
136
|
+
},
|
|
132
137
|
{
|
|
133
138
|
"type": "command",
|
|
134
139
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/orchestrator_auto_starter.py",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Advisor consult guidance for the SessionStart advisor_rules_prompt hook.
|
|
2
|
+
|
|
3
|
+
Each sentence in ``ALL_ADVISOR_RULE_SENTENCES`` is copied word for word from
|
|
4
|
+
``docs/references/advisor-tool.md``, which stays the one source. A
|
|
5
|
+
synchronization test fails when a sentence drifts from that document.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ADVISOR_DISABLE_ENV_VAR",
|
|
12
|
+
"ADVISOR_MODEL_SETTINGS_KEY",
|
|
13
|
+
"ADVISOR_RULES_HEADER",
|
|
14
|
+
"ADVISOR_RULES_PROMPT",
|
|
15
|
+
"ALL_ADVISOR_RULE_SENTENCES",
|
|
16
|
+
"ALL_ADVISOR_DISABLE_ENV_TRUE_VALUES",
|
|
17
|
+
"CLAUDE_CONFIG_DIR_ENV_VAR",
|
|
18
|
+
"DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME",
|
|
19
|
+
"USER_SETTINGS_FILE_NAME",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
ADVISOR_MODEL_SETTINGS_KEY = "advisorModel"
|
|
23
|
+
ADVISOR_DISABLE_ENV_VAR = "CLAUDE_CODE_DISABLE_ADVISOR_TOOL"
|
|
24
|
+
ALL_ADVISOR_DISABLE_ENV_TRUE_VALUES = frozenset({"1", "true", "yes", "on"})
|
|
25
|
+
CLAUDE_CONFIG_DIR_ENV_VAR = "CLAUDE_CONFIG_DIR"
|
|
26
|
+
DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude"
|
|
27
|
+
USER_SETTINGS_FILE_NAME = "settings.json"
|
|
28
|
+
|
|
29
|
+
ADVISOR_RULES_HEADER = (
|
|
30
|
+
"ADVISOR, when `advisor` is in your tool list. Use your own judgment on when a"
|
|
31
|
+
" consult helps. Starting points from ~/.claude/docs/references/advisor-tool.md:"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
ALL_ADVISOR_RULE_SENTENCES = (
|
|
35
|
+
"Call for design, architecture, and risk questions where you will not touch a file.",
|
|
36
|
+
"On tasks longer than a few steps, aim for an early approach consult and a completion review.",
|
|
37
|
+
"This cadence guides planning and leaves the task free to follow its evidence.",
|
|
38
|
+
"Short reactive tasks may use the single consult that best fits the live decision.",
|
|
39
|
+
"Ask the advisor to hunt for missing requirements, untested behavior, wrong assumptions, unhandled edge cases, evidence gaps, and early completion claims.",
|
|
40
|
+
"Give the advice serious weight.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
ADVISOR_RULES_PROMPT = " ".join((ADVISOR_RULES_HEADER, *ALL_ADVISOR_RULE_SENTENCES))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionStart hook: inject open advisor consult guidance when the advisor is on.
|
|
3
|
+
|
|
4
|
+
The built-in advisor tool brings its own guidance on when to call it. This hook
|
|
5
|
+
adds the team-advisor starting points, such as a consult on a design question,
|
|
6
|
+
and leaves each call to the session's judgment. It emits them only when the user settings carry an
|
|
7
|
+
``advisorModel`` value and ``CLAUDE_CODE_DISABLE_ADVISOR_TOOL`` is not set, so a
|
|
8
|
+
session without the advisor is never told to call it.
|
|
9
|
+
The hook writes nothing and runs no tools itself.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
20
|
+
if _hooks_directory not in sys.path:
|
|
21
|
+
sys.path.insert(0, _hooks_directory)
|
|
22
|
+
|
|
23
|
+
from hooks_constants.advisor_rules_prompt_constants import (
|
|
24
|
+
ADVISOR_DISABLE_ENV_VAR,
|
|
25
|
+
ADVISOR_MODEL_SETTINGS_KEY,
|
|
26
|
+
ADVISOR_RULES_PROMPT,
|
|
27
|
+
ALL_ADVISOR_DISABLE_ENV_TRUE_VALUES,
|
|
28
|
+
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
29
|
+
DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME,
|
|
30
|
+
USER_SETTINGS_FILE_NAME,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def user_settings_path() -> Path:
|
|
35
|
+
"""Return the user settings file under the active Claude config directory."""
|
|
36
|
+
configured_directory = os.environ.get(CLAUDE_CONFIG_DIR_ENV_VAR, "").strip()
|
|
37
|
+
if configured_directory:
|
|
38
|
+
return Path(configured_directory) / USER_SETTINGS_FILE_NAME
|
|
39
|
+
return Path.home() / DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME / USER_SETTINGS_FILE_NAME
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_advisor_disabled_by_environment() -> bool:
|
|
43
|
+
"""Return True when the environment turns the advisor tool off."""
|
|
44
|
+
raw_value = os.environ.get(ADVISOR_DISABLE_ENV_VAR, "").strip().lower()
|
|
45
|
+
return raw_value in ALL_ADVISOR_DISABLE_ENV_TRUE_VALUES
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_advisor_configured(settings_path: Path) -> bool:
|
|
49
|
+
"""Return True when the settings file names a non-empty advisor model."""
|
|
50
|
+
try:
|
|
51
|
+
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
52
|
+
except (OSError, ValueError):
|
|
53
|
+
return False
|
|
54
|
+
if not isinstance(settings, dict):
|
|
55
|
+
return False
|
|
56
|
+
advisor_model = settings.get(ADVISOR_MODEL_SETTINGS_KEY)
|
|
57
|
+
return isinstance(advisor_model, str) and advisor_model.strip() != ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main() -> None:
|
|
61
|
+
"""Emit the advisor guidance as SessionStart additionalContext when the advisor is on."""
|
|
62
|
+
if is_advisor_disabled_by_environment():
|
|
63
|
+
return
|
|
64
|
+
if not is_advisor_configured(user_settings_path()):
|
|
65
|
+
return
|
|
66
|
+
session_start_output = {
|
|
67
|
+
"hookSpecificOutput": {
|
|
68
|
+
"hookEventName": "SessionStart",
|
|
69
|
+
"additionalContext": ADVISOR_RULES_PROMPT,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
sys.stdout.write(json.dumps(session_start_output) + "\n")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Tests for advisor_rules_prompt: SessionStart hook that injects advisor consult rules."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
_SESSION_DIR = Path(__file__).resolve().parent
|
|
12
|
+
_HOOKS_ROOT = _SESSION_DIR.parent
|
|
13
|
+
_ADVISOR_TOOL_DOC = _HOOKS_ROOT.parent / "docs" / "references" / "advisor-tool.md"
|
|
14
|
+
for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
|
|
15
|
+
if each_sys_path_entry not in sys.path:
|
|
16
|
+
sys.path.insert(0, each_sys_path_entry)
|
|
17
|
+
|
|
18
|
+
import advisor_rules_prompt as starter
|
|
19
|
+
|
|
20
|
+
from hooks_constants.advisor_rules_prompt_constants import (
|
|
21
|
+
ADVISOR_DISABLE_ENV_VAR,
|
|
22
|
+
ADVISOR_RULES_PROMPT,
|
|
23
|
+
ALL_ADVISOR_RULE_SENTENCES,
|
|
24
|
+
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run_main() -> str:
|
|
29
|
+
captured_stdout = StringIO()
|
|
30
|
+
with patch("sys.stdout", captured_stdout):
|
|
31
|
+
starter.main()
|
|
32
|
+
return captured_stdout.getvalue()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def config_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
37
|
+
monkeypatch.setenv(CLAUDE_CONFIG_DIR_ENV_VAR, str(tmp_path))
|
|
38
|
+
monkeypatch.delenv(ADVISOR_DISABLE_ENV_VAR, raising=False)
|
|
39
|
+
return tmp_path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _write_settings(config_directory: Path, settings: object) -> None:
|
|
43
|
+
(config_directory / "settings.json").write_text(json.dumps(settings), encoding="utf-8")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestAdvisorRulesPrompt:
|
|
47
|
+
def test_should_emit_rules_when_advisor_model_is_set(self, config_directory: Path) -> None:
|
|
48
|
+
_write_settings(config_directory, {"advisorModel": "fable"})
|
|
49
|
+
hook_output = json.loads(_run_main())["hookSpecificOutput"]
|
|
50
|
+
assert hook_output["hookEventName"] == "SessionStart"
|
|
51
|
+
assert hook_output["additionalContext"] == ADVISOR_RULES_PROMPT
|
|
52
|
+
|
|
53
|
+
def test_should_emit_nothing_when_advisor_model_is_missing(
|
|
54
|
+
self, config_directory: Path
|
|
55
|
+
) -> None:
|
|
56
|
+
_write_settings(config_directory, {"permissions": {}})
|
|
57
|
+
assert _run_main() == ""
|
|
58
|
+
|
|
59
|
+
def test_should_emit_nothing_when_advisor_model_is_blank(self, config_directory: Path) -> None:
|
|
60
|
+
_write_settings(config_directory, {"advisorModel": " "})
|
|
61
|
+
assert _run_main() == ""
|
|
62
|
+
|
|
63
|
+
def test_should_emit_nothing_when_settings_file_is_absent(self, config_directory: Path) -> None:
|
|
64
|
+
assert _run_main() == ""
|
|
65
|
+
|
|
66
|
+
def test_should_emit_nothing_when_settings_file_is_not_json(
|
|
67
|
+
self, config_directory: Path
|
|
68
|
+
) -> None:
|
|
69
|
+
(config_directory / "settings.json").write_text("{not json", encoding="utf-8")
|
|
70
|
+
assert _run_main() == ""
|
|
71
|
+
|
|
72
|
+
def test_should_emit_nothing_when_environment_disables_the_advisor(
|
|
73
|
+
self, config_directory: Path, monkeypatch: pytest.MonkeyPatch
|
|
74
|
+
) -> None:
|
|
75
|
+
_write_settings(config_directory, {"advisorModel": "fable"})
|
|
76
|
+
monkeypatch.setenv(ADVISOR_DISABLE_ENV_VAR, "1")
|
|
77
|
+
assert _run_main() == ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestAdvisorRulesMatchTheDoc:
|
|
81
|
+
@pytest.mark.parametrize("each_rule_sentence", ALL_ADVISOR_RULE_SENTENCES)
|
|
82
|
+
def test_should_quote_each_sentence_from_advisor_tool_doc(
|
|
83
|
+
self, each_rule_sentence: str
|
|
84
|
+
) -> None:
|
|
85
|
+
doc_text = " ".join(_ADVISOR_TOOL_DOC.read_text(encoding="utf-8").split())
|
|
86
|
+
assert each_rule_sentence in doc_text
|